Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
156 commits
Select commit Hold shift + click to select a range
0466d87
feat(bridge): recognize the XBRIDGE action and its escrow roles
jdogresorg Sep 12, 2026
aee3803
chore(deps): move audited transitive packages to their patched releases
jdogresorg Sep 12, 2026
7686daa
fix(decoder): harden reorg-halt clearing, dispenser grace and carriers
jdogresorg Sep 13, 2026
83be9ea
docs: follow the indexer's renamed divergence metrics module
jdogresorg Sep 13, 2026
ed85709
test: pin what the unit tier collects and what this repo vendors
jdogresorg Sep 13, 2026
6dfcb83
chore: record the dead-code sweep, which deletes nothing
jdogresorg Sep 13, 2026
7ab7adb
chore: land the shared eslint preset and a lint script
jdogresorg Sep 13, 2026
91b9a28
refactor: give the service one home for its environment reads
jdogresorg Sep 13, 2026
0d15660
refactor: snake_case the source names and give the protocol rules one…
jdogresorg Sep 13, 2026
2437d5c
refactor: drop the underscore prefix from fourteen method names
jdogresorg Sep 13, 2026
1c06915
test: one naming scheme for the suites, and a home for what is not a …
jdogresorg Sep 13, 2026
7762e9d
refactor: one logger, one export shape, and requires at the top
jdogresorg Sep 13, 2026
0700d34
docs: put back the comment lines the removal sweep took
jdogresorg Sep 13, 2026
9df70d0
refactor: give the node-facing modules a home and move the operator t…
jdogresorg Sep 13, 2026
6599d75
docs: merge back what the reference scrub took, and make the main cla…
jdogresorg Sep 13, 2026
652ffa9
docs: correct the patch module's name in the image build comment
jdogresorg Sep 13, 2026
fa99e5b
repoint this repo's own references to sibling renames
jdogresorg Sep 13, 2026
e31ce52
docs: follow the indexer's moved file paths
jdogresorg Sep 14, 2026
838affc
docs: follow the rest of the indexer's moved file paths
jdogresorg Sep 14, 2026
a8b1a14
chore(coins): refresh the vendored registry from canonical
jdogresorg Sep 14, 2026
8bc5bd4
test(pins): declare the layout pass's suite renames against the AT1 pin
jdogresorg Sep 14, 2026
2aafbb5
refactor(clear-reorg-halt): hoist the db.js require to the top of the…
jdogresorg Sep 14, 2026
5d9ec03
docs(XChainDecoder): add plain-language lines above two boot-time db …
jdogresorg Sep 14, 2026
caf1422
chore(pins): re-pin the vendored coins registry at the hub canonical
jdogresorg Sep 14, 2026
9235177
refactor(config): read the stall, RPC timeout and shutdown knobs thro…
jdogresorg Sep 14, 2026
96857a9
refactor(clear-reorg-halt): load the service .env at the top of the file
jdogresorg Sep 14, 2026
c9542d0
refactor(chain): move the bufferutils patch pair into src/chain
jdogresorg Sep 14, 2026
dde9f51
refactor(clear-reorg-halt): rename the entry point to src/clear_reorg…
jdogresorg Sep 14, 2026
4f82bd6
fix(clear-reorg-halt): keep the old src/clear-reorg-halt.js path working
jdogresorg Sep 14, 2026
b91946a
style(decoder): keep XChainDecoder.js at its line count and state the…
jdogresorg Sep 14, 2026
cbb7cd2
ci: run the identity pin as its own tier of ci-full.sh
jdogresorg Sep 14, 2026
0fc0283
chore(coins): refresh the vendored registry from canonical
jdogresorg Sep 14, 2026
4604f72
chore(pins): re-pin the vendored coins registry at the refreshed cano…
jdogresorg Sep 14, 2026
d0ba56c
fix(decoder): park on a reorg halt instead of exiting into the restar…
jdogresorg Sep 14, 2026
bb58f85
fix(sibling): find the indexer BATCH handler whether it is a file or …
jdogresorg Sep 14, 2026
f79b545
fix(decoder): write the clear-reorg-halt audit row when the halt id i…
jdogresorg Sep 14, 2026
d22606e
test(fuzz): seed the dispenser parsing harness so a failure reproduces
jdogresorg Sep 14, 2026
976b6a0
ci: check the identity pin in the drift-guards job on push and pull r…
jdogresorg Sep 14, 2026
4fea540
feat(pins): record one pinned test file split into several and grade …
jdogresorg Sep 14, 2026
83850b9
test(chaos): split node availability tests by behavior
jdogresorg Sep 14, 2026
c2addf4
test(chaos): split RPC timeout tests by behavior
jdogresorg Sep 14, 2026
e866a65
test(chaos): split database exhaustion tests by behavior
jdogresorg Sep 14, 2026
cde4c70
test(chaos): split transaction failure tests by behavior
jdogresorg Sep 14, 2026
4bf0629
test(chaos): split malformed mempool tests by behavior
jdogresorg Sep 14, 2026
458f6a8
test(chaos): split chain reorganization tests by behavior
jdogresorg Sep 14, 2026
1841d8c
test(chaos): split concurrent instance tests by behavior
jdogresorg Sep 14, 2026
549cd70
test(chaos): split signal handling tests by behavior
jdogresorg Sep 14, 2026
ce30671
test(chaos): split rejection tests by behavior
jdogresorg Sep 14, 2026
8ac5bd4
test(chaos): split output failure tests by behavior
jdogresorg Sep 14, 2026
295a035
test(e2e): split dispenser lifecycle tests by behavior
jdogresorg Sep 14, 2026
2755ca9
test(e2e): split the error handling suite by behavior
jdogresorg Sep 14, 2026
9f5f91a
test(e2e): split the indexer contract suite by behavior
jdogresorg Sep 14, 2026
aa56029
test(e2e): split multi-block processing tests by behavior
jdogresorg Sep 14, 2026
41d4505
test(fuzz): split block decoder checks into bounded suites
jdogresorg Sep 14, 2026
2ccaa6d
test(fuzz): split dispenser parsing checks into bounded suites
jdogresorg Sep 14, 2026
4db13c7
test(fuzz): extract mixed pipeline fixture construction
jdogresorg Sep 14, 2026
039e686
test(fuzz): split remove-obfuscation suites by behavior
jdogresorg Sep 14, 2026
4b4e0fd
test(integration): split dispenser suites by behavior
jdogresorg Sep 14, 2026
9db2251
test(integration): split indexer contract suites by behavior
jdogresorg Sep 14, 2026
2739b41
test(integration): split malformed-data suites by behavior
jdogresorg Sep 14, 2026
a465c7e
test(integration): split OP_RETURN suites by behavior
jdogresorg Sep 14, 2026
17baee2
test(security): split action validation suites by behavior
jdogresorg Sep 14, 2026
0e518d5
test(security): split connection handling suites by behavior
jdogresorg Sep 14, 2026
ece3eef
test(security): split deobfuscation suites by behavior
jdogresorg Sep 14, 2026
d103709
test(security): split dispenser validation suite by behavior
jdogresorg Sep 15, 2026
929ea2f
test(security): split error sanitization suites by behavior
jdogresorg Sep 15, 2026
1cd6e33
test(security): split SQL parameterization suite by behavior
jdogresorg Sep 15, 2026
13fd201
test(smoke): split API ping suite by behavior
jdogresorg Sep 15, 2026
3de9a6c
test(smoke): extract database initialization helpers
jdogresorg Sep 15, 2026
417027c
test(smoke): split module loading suite by behavior
jdogresorg Sep 15, 2026
c35a8c6
test(auxpow): split reassembly suites by behavior
jdogresorg Sep 15, 2026
120b7e1
test(auxpow): split strip parity suite by behavior
jdogresorg Sep 15, 2026
8ed83e3
test(batch): split payment output capture suites by behavior
jdogresorg Sep 15, 2026
612b407
test(bet): split action gate suite by behavior
jdogresorg Sep 15, 2026
f7ce99b
test(block): extract previous hash suite setup
jdogresorg Sep 15, 2026
0a00809
test(connector): split extra coverage suite by RPC behavior
jdogresorg Sep 15, 2026
220154a
test(connector): split RPC review suites by behavior
jdogresorg Sep 15, 2026
9800e09
test(boundary): split deobfuscation suite by behavior
jdogresorg Sep 15, 2026
81fc4bf
test(boundary): split satoshi conversion suite by value class
jdogresorg Sep 15, 2026
5661c75
test(chain): split genesis pin suite by behavior
jdogresorg Sep 15, 2026
b221d7a
test(chain): split chain identity gate suites by behavior
jdogresorg Sep 15, 2026
a19d33d
test(decoder): split chunk commit fetch suite by behavior
jdogresorg Sep 15, 2026
e8a8c95
test(protocol): split compiled push conformance suites by behavior
jdogresorg Sep 15, 2026
4b6cff0
test(chain): split crypto network suites by behavior
jdogresorg Sep 15, 2026
9983eae
test(db): split connection release suite by behavior
jdogresorg Sep 15, 2026
019b6b1
test(api): split live heartbeat suite by behavior
jdogresorg Sep 15, 2026
099dd71
test(decoder): split stability suites and extract builders
jdogresorg Sep 15, 2026
d12e0b1
test(decoder): split stale-tip suites by behavior
jdogresorg Sep 15, 2026
154e695
test(db): split dispenser database suites by behavior
jdogresorg Sep 15, 2026
09e1ac6
test(dispenser): split grace activation conformance suite
jdogresorg Sep 15, 2026
aa6d971
test(dispenser): split expiry activation conformance suite
jdogresorg Sep 15, 2026
95625a9
test(dispenser): split expiry realignment suite by behavior
jdogresorg Sep 15, 2026
f99052f
test(chain): split litecoin block suite by behavior
jdogresorg Sep 15, 2026
d5fa93f
test(mempool): split isolation suite by behavior
jdogresorg Sep 15, 2026
d232bde
test(migrate): split operator CLI suite by behavior
jdogresorg Sep 15, 2026
d261c72
test(migrate): split startup precondition suite
jdogresorg Sep 15, 2026
a933580
test(status): split node catching-up suites by behavior
jdogresorg Sep 15, 2026
8c9a8c6
test(status): split node reachability suites by behavior
jdogresorg Sep 15, 2026
9a74653
test(connector): split RPC failover suites by behavior
jdogresorg Sep 15, 2026
805a4de
test(decoder): split parse quarantine suites by behavior
jdogresorg Sep 15, 2026
462d978
test(decoder): split deobfuscation suites by behavior
jdogresorg Sep 15, 2026
d0808bb
test(reorg): split restart depth suites by behavior
jdogresorg Sep 15, 2026
9650ac0
test(reorg): split halt clearing suites by behavior
jdogresorg Sep 15, 2026
7e1ade6
test(decoder): split latent reorg halt reporting by behavior
jdogresorg Sep 15, 2026
1483f78
test(decoder): split action alias round trips by behavior
jdogresorg Sep 15, 2026
de68fa6
test(security): split dependency advisory checks by behavior
jdogresorg Sep 15, 2026
348c6c6
test(shutdown): split graceful shutdown checks by behavior
jdogresorg Sep 15, 2026
4efdc27
test(db): split SQL quote escape checks by behavior
jdogresorg Sep 15, 2026
87861e0
test(tiers): split gate map checks by behavior
jdogresorg Sep 15, 2026
ed22782
test(util): split utility checks by behavior
jdogresorg Sep 15, 2026
c2c23fa
test(decoder): split block decoder checks by behavior
jdogresorg Sep 15, 2026
d426b3f
test(security): scan every connector part for full error-object logging
jdogresorg Sep 15, 2026
159ef74
test(chain): scan every connector part for the POST choke point
jdogresorg Sep 15, 2026
1b53b93
test(chain): re-inline the coinbase skip helper for cross-repo parity
jdogresorg Sep 15, 2026
8941004
refactor(chain): split the blockchain connector class into parts besi…
jdogresorg Sep 15, 2026
f065726
refactor(protocol): split batch sub-command capture into parts beside it
jdogresorg Sep 15, 2026
406012a
refactor(decoder): split oversized decoder functions
jdogresorg Sep 15, 2026
978c45d
chore(protocol): carry the repo SPDX header into the two split-out ba…
jdogresorg Sep 15, 2026
b19a0ff
test(batch): split batch conformance suites by behavior
jdogresorg Sep 15, 2026
2adc091
test(batch): split sub-command output capture activation checks by be…
jdogresorg Sep 15, 2026
ad46b40
chore(pins): declare three decoder test-file splits
jdogresorg Sep 15, 2026
fc3a60c
ci: require a real checkout for each declared sibling
jdogresorg Sep 15, 2026
2eb2a46
test(decoder): split decoder unit suites by behavior
jdogresorg Sep 15, 2026
f58bd7e
test(decoder): split conformance and RPC hardening suites by behavior
jdogresorg Sep 15, 2026
8c679b5
test(decoder): split db and dispenser cancel-grace suites by behavior
jdogresorg Sep 15, 2026
1b09722
test(decoder): split action-decoding e2e and parse-transaction fuzz s…
jdogresorg Sep 15, 2026
42f7028
test(decoder): split parse-transaction unit suite by behavior
jdogresorg Sep 15, 2026
f76610b
test(decoder): split migration runner unit suite by behavior
jdogresorg Sep 15, 2026
93417ff
test(decoder): split dispenser lifecycle mirror and oracle fee output…
jdogresorg Sep 15, 2026
d79ad31
test(decoder): split taproot envelope unit suite by behavior
jdogresorg Sep 15, 2026
ad96ac0
chore(pins): declare the thirteen decoder test-file splits from this …
jdogresorg Sep 15, 2026
b3e9d3e
test(chain): read the AuxPoW twin at the utxo tracker's split codec path
jdogresorg Sep 15, 2026
c365c18
test(batch): split whole-batch rejection corpus checks by behavior
jdogresorg Sep 15, 2026
ff3db77
test(connector): split aux-pow and concurrency suites by behavior
jdogresorg Sep 15, 2026
795a920
test(dispenser): split combinatorial scenario suite by behavior
jdogresorg Sep 15, 2026
af3a164
test(boundary): split script type checks by behavior
jdogresorg Sep 15, 2026
88a5086
test(db): split database query checks by behavior
jdogresorg Sep 15, 2026
8796722
test(batch): split dispenser registration checks by behavior
jdogresorg Sep 15, 2026
e102d4b
test: keep the dispenser registration split helper under its support …
jdogresorg Sep 15, 2026
915151c
chore(pins): declare the 6 test-file splits in suite-title-splits.json
jdogresorg Sep 15, 2026
c94b50c
docs: repoint prose citations of xchain-indexer unit suites to their …
jdogresorg Sep 15, 2026
7b576cc
test: split the taproot envelope and reachability suites into same-ti…
jdogresorg Sep 15, 2026
d1759b9
test: split the batch dispenser registration and whole-batch rejectio…
jdogresorg Sep 15, 2026
acff845
fix(clear-reorg-halt): let --dry-run run without --reason
jdogresorg Sep 15, 2026
dbf8dd4
refactor(api): move the probe log, reachability fields, batch guard a…
jdogresorg Sep 15, 2026
5f8be16
refactor(db): split the database class into parts beside it
jdogresorg Sep 15, 2026
12ad3e1
refactor(api): split startApi and the health method into parts beside…
jdogresorg Sep 15, 2026
5346790
refactor(decoder): move status, chain integrity, source, envelope, di…
jdogresorg Sep 15, 2026
24dd61f
refactor(decoder): split the decoder constructor into field-group ini…
jdogresorg Sep 15, 2026
aba6400
refactor(decoder): move decoder_metrics into src/metrics/
jdogresorg Sep 15, 2026
4c05f73
refactor(decoder): move verifyReorg and its halt record into parts be…
jdogresorg Sep 15, 2026
60f74cd
refactor(decoder): move parseTransaction into steps beside the class
jdogresorg Sep 15, 2026
ea67231
refactor(decoder): move start and its block loop into parts beside th…
jdogresorg Sep 15, 2026
c5be447
test(dispenser): repoint the field-offsets guard at the indexer's dis…
jdogresorg Sep 16, 2026
fc50980
test(activation): read the indexer's flag-day rows from the registry …
jdogresorg Sep 16, 2026
a9f0314
release: v0.19.0
jdogresorg Sep 16, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
12 changes: 11 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -71,12 +71,22 @@ jobs:
console.log("consensus pin conformance OK (testnet, regtest)");
'

# Identity pin: bin/pins/identity.json records the sha256 of the vendored
# coin files and the two twin fixtures. Re-hash the tree against it and
# fail on any moved, missing or unreadable file, so a stale pin cannot
# reach develop or master green through a direct push or a release pull
# request. bin/ci-full.sh runs the same command before a push; the tool
# uses only node builtins, so this job needs no install.
- name: Identity pin (vendored coins, twin fixtures)
working-directory: xchain-decoder
run: node bin/pin-identity.js --check

# Docker-gated tiers: the integration and e2e suites each bring up their own
# throwaway regtest node and MariaDB with docker compose, so they cannot run
# inside `npm run ci`. They get their own job on a runner that has docker, so a
# slow or flaky venue cannot hold up the fast tier's signal. The map of which
# tier is gated where lives in test/tier-manifest.json and is enforced by
# test/unit/tierManifest.test.js.
# test/unit/tier_manifest.test.js.
docker-suites:
runs-on: ubuntu-latest
timeout-minutes: 30
Expand Down
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

## [0.19.0] - 2026-09-16

### Added
- Consensus: the XBRIDGE action name and its escrow role addresses are recognized.

### Changed
- Audited transitive packages move to their patched releases (lockfile only).
- Restructured under the platform code-structure standard (feature directories, snake_case files, split test suites, restored comments); consensus identity byte-identical and pinned.

## [0.18.0] - 2026-09-11

### Fixed
Expand Down
4 changes: 2 additions & 2 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,9 @@ COPY ./src /XChainDecoder/src
# readUInt64 throws "RangeError: value out of range" for output values above
# ~9.007e15 (2^53), which Dogecoin mainnet exceeds (>~90.07M DOGE in one output).
# Mirrors xchain-utxo-tracker's identical patch. Belt-and-braces: the same patch
# is also applied in-process at require time (src/applyBufferutilsPatch.js), so
# is also applied in-process at require time (src/chain/apply_bufferutils_patch.js), so
# non-Docker runs and node_modules refreshes are covered even without this COPY.
COPY ./src/bufferutils.js /XChainDecoder/node_modules/bitcoinjs-lib/src/bufferutils.js
COPY ./src/chain/bufferutils.js /XChainDecoder/node_modules/bitcoinjs-lib/src/bufferutils.js
# No .env is baked in: configuration reaches the container as environment
# (xchain-node at `docker run`, docker-compose.yml via env_file). An optional
# `COPY ./.en[v]` glob here builds only under BuildKit.
Expand Down
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@
# XChain Platform Decoder

<p align="center">
<img src="https://img.shields.io/badge/version-0.18.0-blue" alt="Version">
<img src="https://img.shields.io/badge/tests-2%2C077%2B%20passing-brightgreen" alt="Tests">
<img src="https://img.shields.io/badge/version-0.19.0-blue" alt="Version">
<img src="https://img.shields.io/badge/tests-2%2C118%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">
</p>
Expand Down Expand Up @@ -113,7 +113,7 @@ neither source sets one, so these defaults hold on an unconfigured box:
| `npm run migrate` | Apply pending database migrations (auto + manual; `--file <name>` scopes to specific migration(s)) |
| `npm run ci` | The full no-external-services gate: unit, security, smoke, regression, chaos, and a 100-iteration fuzz pass (about a minute) |
| `npm run test:smoke` | Smoke tests (58 tests, no external services) |
| `npm run test:unit` | Unit tests (1,596 tests, no external services) |
| `npm run test:unit` | Unit tests (1,637 tests, no external services) |
| `npm run test:security` | Security tests (83 tests, no external services) |
| `npm run test:integration` | Integration tests (30 tests; brings up its own throwaway regtest node and MariaDB, requires Docker) |
| `npm run test:e2e` | End-to-end tests (72 tests; brings up its own throwaway regtest node and MariaDB on separate ports, requires Docker) |
Expand Down
16 changes: 13 additions & 3 deletions bin/ci-full.sh
Original file line number Diff line number Diff line change
Expand Up @@ -70,10 +70,13 @@ run_tier() {
fi
}
need_sib() {
local s
local s missing
for s in "$@"; do
if [ ! -d "$SIB/$s" ]; then
echo "ci:full: MISSING SIBLING $SIB/$s" >&2
missing=""
[ -e "$SIB/$s/package.json" ] || missing="$missing package.json"
[ -e "$SIB/$s/.git" ] || missing="$missing .git"
if [ -n "$missing" ]; then
echo "ci:full: MISSING SIBLING $SIB/$s (missing:$missing)" >&2
echo "ci:full: GitHub CI checks this sibling out and runs steps against it," >&2
echo "ci:full: so skipping here would gate green on a subset. Declare it in" >&2
echo "ci:full: .ci-siblings (venue) or clone it beside this repo (hand run)." >&2
Expand Down Expand Up @@ -107,6 +110,13 @@ run_tier "drift: coin consensus-pin conformance" node -e '
console.log("consensus pin conformance OK (testnet, regtest)");
'

# --- identity pin (this gate only; no ci.yml job runs it) ------------------
# bin/pins/identity.json holds the sha256 of the vendored coin files and the
# two twin fixtures. Nothing else reads it, so this tier re-hashes the tree
# against it and fails on any moved, missing or unreadable file instead of
# letting the pin go stale.
run_tier "identity pin (vendored coins, twin fixtures)" node bin/pin-identity.js --check

# --- job: docker-suites ----------------------------------------------------
# Both tiers own their venue lifecycle inside their npm script (compose up
# --wait, mocha, down -v on any exit), so this transcribes the two run-steps
Expand Down
177 changes: 177 additions & 0 deletions bin/comment-run-pairs.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
#!/usr/bin/env node
/*********************************************************************
*
* 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.
*
**********************************************************************
*
* Which comments did the removal sweep REWRITE, and what did the rewrite take
* with it?
*
* WHY A LINE COUNT CANNOT ANSWER THIS. A restore pass puts back the comment
* runs that are GONE, and a coverage floor proves it did. Neither sees the
* other half of the loss: the sweep also scrubbed internal references out of
* lines it KEPT, and some of those rewrites took the explanation along with
* the reference. The file ends up with a comment in the same place, shorter,
* saying less, and every line-based check reads it as present.
*
* WHY THE COMPARISON IS RUN-LEVEL AND NOT LINE-LEVEL. Rewrapping a paragraph
* at a different width changes every line in it, so line pairing reports a
* pure rewrap as a total loss and buries the real cases. A RUN is a block of
* consecutive comment lines, which is the unit a writer actually edits.
*
* HOW RUNS ARE PAIRED. By token overlap: the Jaccard index of the two runs'
* word sets, with a floor of 0.3. Above the floor the two runs are the same
* comment, edited. Below it they are different comments, and the before-run
* counts as deleted (which the restore pass already handles) rather than as
* rewritten.
*
* WHAT IS FLAGGED. A paired run whose AFTER side carries fewer content words
* than its BEFORE side. The words it lost are printed so a reader can judge
* whether they were the reference (correctly gone) or the explanation around
* it (wrongly gone, and to be merged back WITHOUT the reference).
*
* The verdict is a human's. This tool finds the candidates and shows both
* texts; it never edits a file, because deciding what a sentence was for is
* exactly the part that cannot be automated.
*
* USAGE
* node bin/comment-run-pairs.js --before <sha> --after <sha> [--json]
* node bin/comment-run-pairs.js --before <sha> --after <sha> --file <path>
*
********************************************************************/

'use strict';

const { execFileSync } = require('node:child_process');

const JACCARD_FLOOR = 0.3;

function git(args) {
return execFileSync('git', args, { maxBuffer: 256 * 1024 * 1024 }).toString('utf8');
}

/** The same comment-line definition the coverage gate uses. */
function commentMask(lines) {
const mask = new Array(lines.length).fill(false);
let inBlock = false;
for (let i = 0; i < lines.length; i += 1) {
const t = lines[i].trim();
if (inBlock) { mask[i] = true; if (t.includes('*/')) inBlock = false; continue; }
if (t.startsWith('//')) { mask[i] = true; continue; }
if (t.startsWith('/*')) { mask[i] = true; if (!t.includes('*/')) inBlock = true; }
}
return mask;
}

/** Content words: whitespace tokens carrying a letter or a digit, markers off. */
function words(body) {
return body
.join(' ')
.replace(/\/\*+|\*+\/|^\s*\*|\/\//g, ' ')
.split(/\s+/)
.map((w) => w.replace(/^[^A-Za-z0-9]+|[^A-Za-z0-9]+$/g, ''))
.filter((w) => /[A-Za-z0-9]/.test(w));
}

/** Every comment run in a file, with the code line it sits above. */
function runs(text) {
const lines = text.split('\n');
const mask = commentMask(lines);
const out = [];
let i = 0;
while (i < lines.length) {
if (!mask[i]) { i += 1; continue; }
const start = i;
while (i < lines.length && mask[i]) i += 1;
let j = i;
while (j < lines.length && lines[j].trim() === '') j += 1;
out.push({ body: lines.slice(start, i), anchor: j < lines.length ? lines[j].trim() : null, line: start + 1 });
}
return out;
}

function jaccard(a, b) {
const sa = new Set(a.map((w) => w.toLowerCase()));
const sb = new Set(b.map((w) => w.toLowerCase()));
if (!sa.size && !sb.size) return 1;
let shared = 0;
for (const w of sa) if (sb.has(w)) shared += 1;
return shared / (sa.size + sb.size - shared);
}

function fileList(before, after) {
return git(['diff', '--name-only', `${before}..${after}`]).split('\n').filter(Boolean);
}

function blob(sha, file) {
try { return git(['show', `${sha}:${file}`]); } catch (e) { return null; }
}

function main() {
const argv = process.argv.slice(2);
const arg = (name) => { const i = argv.indexOf(name); return i === -1 ? null : argv[i + 1]; };
const before = arg('--before');
const after = arg('--after');
const only = arg('--file');
const asJson = argv.includes('--json');
if (!before || !after) { process.stderr.write('usage: comment-run-pairs.js --before <sha> --after <sha> [--file <path>] [--json]\n'); return 2; }

const flagged = [];
let pairs = 0;
const files = only ? [only] : fileList(before, after);
for (const file of files) {
const b = blob(before, file);
const a = blob(after, file);
if (b === null || a === null) continue;

const bRuns = runs(b).map((r) => ({ ...r, words: words(r.body) }));
const aRuns = runs(a).map((r) => ({ ...r, words: words(r.body) }));
const taken = new Set();

for (const br of bRuns) {
let best = null;
let bestScore = 0;
for (let k = 0; k < aRuns.length; k += 1) {
if (taken.has(k)) continue;
const score = jaccard(br.words, aRuns[k].words);
if (score > bestScore) { bestScore = score; best = k; }
}
if (best === null || bestScore < JACCARD_FLOOR) continue;
taken.add(best);
pairs += 1;
const ar = aRuns[best];
if (ar.words.length >= br.words.length) continue;
const after_ = new Set(ar.words.map((w) => w.toLowerCase()));
const lost = br.words.filter((w) => !after_.has(w.toLowerCase()));
if (!lost.length) continue;
flagged.push({
file,
beforeLine: br.line,
afterLine: ar.line,
similarity: Number(bestScore.toFixed(2)),
lostWords: lost.length,
lost,
beforeText: br.body.join('\n'),
afterText: ar.body.join('\n'),
});
}
}

if (asJson) { process.stdout.write(`${JSON.stringify({ before, after, pairs, flagged }, null, 2)}\n`); return 0; }
for (const f of flagged) {
process.stdout.write(`\n${f.file}:${f.beforeLine} -> :${f.afterLine} similarity ${f.similarity}, ${f.lostWords} word(s) lost\n`);
process.stdout.write(` BEFORE\n${f.beforeText.split('\n').map((l) => ` ${l}`).join('\n')}\n`);
process.stdout.write(` AFTER\n${f.afterText.split('\n').map((l) => ` ${l}`).join('\n')}\n`);
}
process.stdout.write(`\n${pairs} paired run(s), ${flagged.length} that lost words\n`);
return 0;
}

process.exit(main());
2 changes: 1 addition & 1 deletion bin/coverage-thresholds.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"comment": "Coverage floors for the CI coverage job (regression floors, ~1-1.5 points below measured, not tier targets; raise as coverage climbs). Mirrored into the coverage:check npm script in package.json and guarded by test/unit/coverage-thresholds-sync.test.js. Re-measured 2026-08-14 WITH the declared siblings checked out, which is what the coverage job now does: 89.30 lines/statements, 86.97 branches, 78.75 functions over 1289 unit tests (1248 without siblings).",
"comment": "Coverage floors for the CI coverage job (regression floors, ~1-1.5 points below measured, not tier targets; raise as coverage climbs). Mirrored into the coverage:check npm script in package.json and guarded by test/unit/coverage_thresholds_sync.test.js. Re-measured 2026-08-14 WITH the declared siblings checked out, which is what the coverage job now does: 89.30 lines/statements, 86.97 branches, 78.75 functions over 1289 unit tests (1248 without siblings).",
"lines": 87.8,
"statements": 87.8,
"branches": 85.4,
Expand Down
60 changes: 60 additions & 0 deletions bin/indexer_handler_source.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
/*********************************************************************
*
* 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. A commercial
* license (without AGPL source-disclosure terms) is available -
* contact legal@dankest.llc.
*
**********************************************************************
*
* Finds one xchain-indexer action handler in a sibling checkout, whichever shape it has.
*
* WHY. A handler is one file, src/actions/<name>.js, until the indexer's file-size work
* splits it into src/actions/<name>/ with the entry at index.js and the logic in parts
* beside it, leaving NO flat file behind (the sdk pre-flight drift gate refuses
* to pin a handler directory while a flat <name>.js sits next to it, since require() would
* resolve the flat file first). Anything here that names the flat path then reads a file
* that is not there: the sync tool reports a missing sibling, and a source-text mirror
* check reports the indexer as registering nothing.
*
* WHAT. entry() is the path to require, source() the handler's whole text: the entry plus
* every part beside it, so a literal or a registration that moved into a part is still
* read. A checkout that still carries the flat file reads exactly as it always did.
*
********************************************************************/

'use strict';

const fs = require('fs');
const path = require('path');

/** The path to require for handler <name> under an indexer checkout root. */
function entry(indexerRoot, name) {
const flat = path.join(indexerRoot, 'src', 'actions', name + '.js');
if (fs.existsSync(flat)) return flat;
const inDirectory = path.join(indexerRoot, 'src', 'actions', name, 'index.js');
return fs.existsSync(inDirectory) ? inDirectory : flat;
}

/** Every file of handler <name>, sorted, entry included. Empty when the handler is absent. */
function files(indexerRoot, name) {
const dir = path.join(indexerRoot, 'src', 'actions', name);
if (fs.existsSync(dir) && fs.statSync(dir).isDirectory())
return fs.readdirSync(dir).filter((f) => f.endsWith('.js')).sort()
.map((f) => path.join(dir, f));
const flat = path.join(indexerRoot, 'src', 'actions', name + '.js');
return fs.existsSync(flat) ? [flat] : [];
}

/** The handler's source text, entry and parts newline-joined, or null when it is absent. */
function source(indexerRoot, name) {
const list = files(indexerRoot, name);
return list.length ? list.map((f) => fs.readFileSync(f, 'utf8')).join('\n') : null;
}

module.exports = { entry, files, source };
Loading
Loading