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
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

## [0.20.0] - 2026-09-17

### Fixed
- Reachability metadata no longer declares dynamic edges that this repository does not have.

## [0.19.0] - 2026-09-16

### Added
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 Encoder

<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-1%2C787%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
73 changes: 36 additions & 37 deletions bin/reachability.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@
* so the two tools can never disagree about what a reference is.
*
* A file outside all four is unreferenced across the platform and is the only
* shape a dead-code sweep deletes outright.
* shape a restructure deletes outright.
*
* THE SIBLING REACH INCLUDES THE PLATFORM TOOLING, and it has to. The map tool
* sweeps the `xchain-*` siblings by default and treats the surrounding tree's
Expand Down Expand Up @@ -124,41 +124,37 @@ function toolingSweepDirs(siblingsRoot) {
* Each entry names the site that builds the path and what it resolves to, so a
* reader can check the claim instead of trusting the table.
*/
const DYNAMIC_EDGES = [
{
from: 'src/consensus_rules_digest.js',
// loadGateValues requires './<module>.js' for every SHARED_GATES row, so
// the gate carriers are held by the digest and not by any literal. The
// list is read from the module rather than restated, because a restated
// copy is a second registry that drifts.
toList: () => {
const { SHARED_GATES } = require('../src/consensus_rules_digest.js');
return SHARED_GATES.map(([mod]) => `src/${mod}.js`);
},
why: 'the consensus-rules digest requires every SHARED_GATES module by computed path',
},
{
from: 'src/db/index.js',
// The mixin install loop calls require(file) over its MIXIN_FILES rows, so
// not one literal in the file names a mixin and all of src/db/ reads
// unreachable without this edge. The list is read out of the declaration
// rather than restated here, because a restated copy is a second registry
// that drifts away from the one the loop actually walks.
toList: () => {
const declared = fs.readFileSync(path.join(REPO_ROOT, 'src/db/index.js'), 'utf8');
const block = /const MIXIN_FILES = \[([\s\S]*?)\];/.exec(declared);
if (!block) {
throw new Error('src/db/index.js no longer declares MIXIN_FILES: the mixin edge cannot be read');
}
const rows = Array.from(block[1].matchAll(/(['"])([^'"]+)\1/g))
.map((m) => resolveRequire('src/db/index.js', m[2]))
.filter(Boolean);
if (!rows.length) throw new Error('MIXIN_FILES declares no resolvable mixin: the edge is stale');
return rows;
},
why: 'the Database mixin install loop requires every MIXIN_FILES row by computed path',
},
];
// Both entries below were copied out of xchain-indexer/bin/reachability.js by the
// twin-copier and never adapted: this repo has neither src/consensus_rules_digest.js
// (SHARED_GATES) nor a src/db/index.js that installs mixins from a MIXIN_FILES list
// (confirmed by grep: no computed `require(...)` of any kind appears under this
// repo's src/, only literal specifiers). Declaring edges from files this repo does
// not have made the summary report `declared dynamic edges: 2` while the walk
// applied them zero times (edgesFrom only fires when the CURRENT file is the
// declared `from`, so a `from` outside fileSet is never visited), which is
// confidence the tool had not earned. This repo has no computed require to
// declare, so the list is empty; assertDynamicEdgesResolve below is what makes a
// future stale or copy-pasted entry fail loudly instead of repeating this.
const DYNAMIC_EDGES = [];

/**
* A declared dynamic edge whose `from` file is not in this repo's tracked tree
* cannot ever fire (edgesFrom below only applies an edge while walking its exact
* `from` file), so the walk would silently treat it as zero edges applied while
* the summary still counted it as one of N declared. That is exactly how this file
* spent an unknown span reporting `declared dynamic edges: 2` for edges resolving
* nowhere. Checked eagerly, by name, so a stale or mis-copied declaration fails the
* run instead of passing as an unearned confidence figure.
*/
function assertDynamicEdgesResolve(fileSet) {
for (const edge of DYNAMIC_EDGES) {
if (!fileSet.has(edge.from)) {
throw new Error(`DYNAMIC_EDGES declares an edge from '${edge.from}', which is not in this `
+ `repo's tracked tree, so it can never apply and would otherwise pass as one of `
+ `${DYNAMIC_EDGES.length} declared edges. Fix or remove it in bin/reachability.js (${edge.why}).`);
}
}
}

const SOURCE_EXT = ['.js'];

Expand Down Expand Up @@ -324,6 +320,7 @@ function entriesUnder(prefixes, fileSet) {
function analyse(opts) {
const all = trackedFiles();
const fileSet = new Set(all.filter((f) => f.endsWith('.js')));
assertDynamicEdgesResolve(fileSet);
const sources = Array.from(fileSet).filter((f) => f.startsWith('src/')).sort();

const runtimeEntryList = runtimeEntries(fileSet);
Expand Down Expand Up @@ -442,4 +439,6 @@ function main() {

if (require.main === module) main();

module.exports = { analyse, closure, runtimeEntries, resolveRequire, toolingSweepDirs, DYNAMIC_EDGES };
module.exports = {
analyse, closure, runtimeEntries, resolveRequire, toolingSweepDirs, DYNAMIC_EDGES, assertDynamicEdgesResolve,
};
66 changes: 66 additions & 0 deletions bin/test/reachability_dynamic_edges.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
/*********************************************************************
*
* Copyright © 2025-2026 Dankest, LLC
* Based on XChain Platform by Dankest, LLC - https://dankest.llc
*
* SPDX-License-Identifier: AGPL-3.0-or-later
*
* This file is part of XChain Platform. Licensed under the GNU Affero
* General Public License v3.0 or later; see LICENSE.md.
*
**********************************************************************
*
* bin/reachability.js used to declare two DYNAMIC_EDGES (src/consensus_rules_digest.js
* and src/db/index.js) copied verbatim out of xchain-indexer's twin, neither of which
* this repo has. edgesFrom() only applies a declared edge while it is walking the exact
* file the edge names as `from`, so a `from` this repo never tracks is never visited and
* the edge quietly applies zero times, while the summary still reported
* `declared dynamic edges: 2`, confidence the tool had not earned. This suite drives the
* fix: DYNAMIC_EDGES is empty because this repo has no computed require, and a future
* stale declaration fails loudly instead of repeating the silent pass.
*
* Outside test/ on purpose, matching bin/test/suite_title_split_map.test.js: run it
* directly.
*
* npx mocha --no-config --timeout 30000 bin/test/reachability_dynamic_edges.test.js
*
********************************************************************/

'use strict';

const assert = require('assert');

const { analyse, DYNAMIC_EDGES, assertDynamicEdgesResolve } = require('../reachability.js');

describe('xchain-encoder bin/reachability.js: DYNAMIC_EDGES only names paths this repo has', () => {
it('declares no dynamic edge, because this repo has no computed require', () => {
assert.deepStrictEqual(DYNAMIC_EDGES, []);
});

it('reports dynamicEdgesDeclared: 0 in the summary, not a stale count', () => {
const report = analyse({ siblings: false });
assert.strictEqual(report.summary.dynamicEdgesDeclared, 0);
});

it('does not throw against this repo\'s own tracked tree', () => {
assert.doesNotThrow(() => assertDynamicEdgesResolve(new Set(['src/api.js'])));
});

it('fails loudly, by name, when a declared edge names a file the tree does not have', () => {
// Mutates the real exported DYNAMIC_EDGES array (module-cached, so this is
// the same array analyse() reads) rather than a re-implemented copy of the
// check, then restores it in `finally` so no other test in this process
// sees the injected entry.
const bogus = { from: 'src/does_not_exist.js', why: 'a stale or copy-pasted declaration' };
DYNAMIC_EDGES.push(bogus);
try {
assert.throws(
() => assertDynamicEdgesResolve(new Set(['src/api.js'])),
/src\/does_not_exist\.js/,
);
} finally {
DYNAMIC_EDGES.pop();
}
assert.deepStrictEqual(DYNAMIC_EDGES, [], 'DYNAMIC_EDGES must be restored empty after the injected edge');
});
});
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-encoder",
"description": "xchain-encoder encodes XChain Platform ACTION commands into blockchain transactions.",
"version": "0.19.0",
"version": "0.20.0",
"license": "AGPL-3.0-or-later",
"repository": {
"type": "git",
Expand Down
Loading