Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

## [0.20.0] - 2026-09-17

### Changed
- Mutation-report tiers are keyed by repository-relative paths.

### Fixed
- The VM entry point no longer loads the unused `fs` module.

## [0.19.0] - 2026-09-16

### Changed
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
# XChain Platform Virtual Machine (VM)

<p align="center">
<img src="https://img.shields.io/badge/version-0.19.0-blue" alt="Version">
<img src="https://img.shields.io/badge/version-0.20.0-blue" alt="Version">
<img src="https://img.shields.io/badge/tests-2%2C557%2B%20passing-brightgreen" alt="Tests">
<img src="https://img.shields.io/badge/node-%3E%3D22-green" alt="Node">
<img src="https://img.shields.io/badge/license-AGPL--3.0--or--later-blue" alt="License">
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "xchain-vm",
"description": "XChain Platform Virtual Machine (VM) runtime - deterministic smart contract execution in sandboxed V8 isolates",
"version": "0.19.0",
"version": "0.20.0",
"ciPlatform": "linux",
"license": "AGPL-3.0-or-later",
"main": "src/index.js",
Expand Down
47 changes: 32 additions & 15 deletions scripts/mutation-report.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,20 +36,37 @@ const DEFAULT_CUSTOM_JSON = path.join(ROOT, 'reports', 'mutation', 'custom-muta
const OUTPUT_MD = path.join(ROOT, 'reports', 'mutation', 'MUTATION_SUMMARY.md');

// Tier classification matching XCHAIN_VM_MUTATION_TESTING_PLAN.md
//
// Keyed by repo-relative path (not bare filename): the entry-file splits
// under src/index/, src/gateway/ and src/gateway_emit/ report at the same
// tier as the entry they were carved out of, instead of falling through to
// the Unknown/80 default a bare-filename key would give every part file.
const TIERS = {
'index.js': { tier: 'Critical', target: 95 },
'metering.js': { tier: 'Critical', target: 95 },
'sandbox.js': { tier: 'Critical', target: 95 },
'gas.js': { tier: 'Critical', target: 95 },
'gateway.js': { tier: 'High', target: 90 },
'gateway_emit.js': { tier: 'High', target: 90 },
'state.js': { tier: 'High', target: 90 },
'math.js': { tier: 'High', target: 90 },
'collector.js': { tier: 'Medium', target: 85 },
'validator.js': { tier: 'Medium', target: 85 },
'syntax.js': { tier: 'Medium', target: 85 },
'isolate.js': { tier: 'Low', target: 80 },
'errors.js': { tier: 'Low', target: 80 }
'src/index.js': { tier: 'Critical', target: 95 },
'src/index/block_lifecycle.js': { tier: 'Critical', target: 95 },
'src/index/constants.js': { tier: 'Critical', target: 95 },
'src/index/contract_wrapper.js': { tier: 'Critical', target: 95 },
'src/index/error_results.js': { tier: 'Critical', target: 95 },
'src/index/gateway_injection.js': { tier: 'Critical', target: 95 },
'src/index/install_methods.js': { tier: 'Critical', target: 95 },
'src/index/lint_and_metering.js': { tier: 'Critical', target: 95 },
'src/index/manifest.js': { tier: 'Critical', target: 95 },
'src/metering.js': { tier: 'Critical', target: 95 },
'src/sandbox.js': { tier: 'Critical', target: 95 },
'src/gas.js': { tier: 'Critical', target: 95 },
'src/gateway.js': { tier: 'High', target: 90 },
'src/gateway/accessors.js': { tier: 'High', target: 90 },
'src/gateway/contract_stake.js': { tier: 'High', target: 90 },
'src/gateway_emit.js': { tier: 'High', target: 90 },
'src/gateway_emit/param_validation.js': { tier: 'High', target: 90 },
'src/gateway_emit/same_chain.js': { tier: 'High', target: 90 },
'src/state.js': { tier: 'High', target: 90 },
'src/math.js': { tier: 'High', target: 90 },
'src/collector.js': { tier: 'Medium', target: 85 },
'src/validator.js': { tier: 'Medium', target: 85 },
'src/syntax.js': { tier: 'Medium', target: 85 },
'src/isolate.js': { tier: 'Low', target: 80 },
'src/errors.js': { tier: 'Low', target: 80 }
};

const TIER_ORDER = { Critical: 0, High: 1, Medium: 2, Low: 3, Unknown: 4 };
Expand Down Expand Up @@ -156,7 +173,7 @@ function computeFileStats(filePath, fileData) {

const scorable = killed + survived + timeout + noCoverage;
const score = scorable > 0 ? ((killed + timeout) / scorable * 100) : 0;
const tierInfo = TIERS[filename] || { tier: 'Unknown', target: 80 };
const tierInfo = TIERS[filePath] || { tier: 'Unknown', target: 80 };
const status = score >= tierInfo.target ? 'PASS' : 'FAIL';

return { filename, filePath, scorable, killed, survived, timeout, noCoverage, ignored, compileError, score, tierInfo, status };
Expand Down Expand Up @@ -299,7 +316,7 @@ function generateMarkdown(mergedFiles) {
const survivedList = [];
for (const [filePath, fileData] of Object.entries(mergedFiles)) {
const filename = path.basename(filePath);
const tierInfo = TIERS[filename] || { tier: 'Unknown', target: 80 };
const tierInfo = TIERS[filePath] || { tier: 'Unknown', target: 80 };
for (const m of fileData.mutants) {
if (m.status === 'Survived' || m.status === 'NoCoverage') {
survivedList.push({ filename, filePath, tierInfo, mutant: m });
Expand Down
1 change: 0 additions & 1 deletion src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@

const crypto = require('crypto');
const ivm = require('isolated-vm');
const fs = require('fs');

const IsolateManager = require('./isolate.js');
const GasTracker = require('./gas.js');
Expand Down
46 changes: 46 additions & 0 deletions test/unit/meta/scripts_mutation_report.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,22 @@ const vm = require('vm');
const SRC = path.join(__dirname, '../../../scripts/mutation-report.js');
const source = fs.readFileSync(SRC, 'utf8').replace(/^#!.*\n/, '');

// main() self-executes on load and can process.exit(1), so the
// static-contract tests below never require() this file. To exercise the
// real TIERS lookup and computeFileStats() without that risk (or any
// fs.writeFileSync side effect), patch out the trailing main() call and run
// the rest of the source in a fresh vm context.
function loadReportInternals() {
const patched = source.replace(/\nmain\(\);\s*$/, '\nmodule.exports = { TIERS, computeFileStats };\n');
assert.notStrictEqual(patched, source, 'expected to patch out the trailing main() call');

const sandbox = { module: { exports: {} }, require, __dirname: path.dirname(SRC), __filename: SRC, console, process };
sandbox.exports = sandbox.module.exports;
vm.createContext(sandbox);
new vm.Script(patched, { filename: 'mutation-report.js' }).runInContext(sandbox);
return sandbox.module.exports;
}

describe('scripts/mutation-report (static contract)', function () {
it('is syntactically valid JavaScript (compiles without executing)', function () {
assert.doesNotThrow(() => new vm.Script(source, { filename: 'mutation-report.js' }));
Expand All @@ -47,4 +63,34 @@ describe('scripts/mutation-report (static contract)', function () {
`report generator must not require ${mod}`);
}
});

it('keeps every pre-existing entry file at its declared tier', function () {
const { TIERS } = loadReportInternals();
assert.strictEqual(TIERS['src/index.js'].tier, 'Critical');
assert.strictEqual(TIERS['src/index.js'].target, 95);
assert.strictEqual(TIERS['src/gateway.js'].tier, 'High');
assert.strictEqual(TIERS['src/gateway.js'].target, 90);
assert.strictEqual(TIERS['src/gateway_emit.js'].tier, 'High');
assert.strictEqual(TIERS['src/gateway_emit.js'].target, 90);
});

it('keys mutation tiers on the repo-relative path, so an entry-file split part inherits its entry tier', function () {
const { computeFileStats } = loadReportInternals();

// A part file carved out of the vm entry (src/index/*.js) inherits
// the entry's Critical tier instead of falling to the Unknown/80
// default a bare-filename lookup would give it.
const indexPart = computeFileStats('src/index/install_methods.js', { mutants: [] });
assert.strictEqual(indexPart.tierInfo.tier, 'Critical');
assert.strictEqual(indexPart.tierInfo.target, 95);

// A part file carved out of the gateway entries (src/gateway/*.js,
// src/gateway_emit/*.js) inherits the gateway High tier.
const gatewayPart = computeFileStats('src/gateway/accessors.js', { mutants: [] });
assert.strictEqual(gatewayPart.tierInfo.tier, 'High');
assert.strictEqual(gatewayPart.tierInfo.target, 90);
const gatewayEmitPart = computeFileStats('src/gateway_emit/same_chain.js', { mutants: [] });
assert.strictEqual(gatewayEmitPart.tierInfo.tier, 'High');
assert.strictEqual(gatewayEmitPart.tierInfo.target, 90);
});
});
Loading