diff --git a/CHANGELOG.md b/CHANGELOG.md index b67a935..33be304 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/README.md b/README.md index ffef213..3c53454 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ # XChain Platform Encoder

- Version + Version Tests Node License diff --git a/bin/reachability.js b/bin/reachability.js index 6e44ae5..68c6ae9 100644 --- a/bin/reachability.js +++ b/bin/reachability.js @@ -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 @@ -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 './.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']; @@ -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); @@ -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, +}; diff --git a/bin/test/reachability_dynamic_edges.test.js b/bin/test/reachability_dynamic_edges.test.js new file mode 100644 index 0000000..e82ef72 --- /dev/null +++ b/bin/test/reachability_dynamic_edges.test.js @@ -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'); + }); +}); diff --git a/package-lock.json b/package-lock.json index 229d974..de1c2b4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "xchain-encoder", - "version": "0.19.0", + "version": "0.20.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "xchain-encoder", - "version": "0.19.0", + "version": "0.20.0", "license": "AGPL-3.0-or-later", "dependencies": { "axios": "^1.18.1", diff --git a/package.json b/package.json index 8c44764..e0ecac8 100644 --- a/package.json +++ b/package.json @@ -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",