@bongos/core 1.19.305 → 1.19.307

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1657,7 +1657,6 @@ function checkWriteRoutesValidated() {
1657
1657
 
1658
1658
  // ---- runner ----------------------------------------------------------------
1659
1659
 
1660
-
1661
1660
  // ── Check 19 — the ADR 0174 vocabulary wall (goal 1000068 R07, task 1003008) ──
1662
1661
  //
1663
1662
  // "Role" means ONE thing in this codebase: a craft — Artist, Ideator, or Builder.
@@ -1899,6 +1898,7 @@ const CHECKS = [
1899
1898
  checkGoalMembershipVocabulary, // Check 19b — ADR 0177 / task 1003013: the FOURTH "role" (goal membership)
1900
1899
  checkWriteRoutesValidated, // Check 18 — BV1 R12 / task 1999: every body-reading write route validates (ADR 0118)
1901
1900
  require('./fitness-ratchets.js').checkQualityRatchets, // Check 20 — task 1003129: quality budgets only tighten (mechanics + knip CI step live in fitness-ratchets.js)
1901
+ require('./doc-cli-guard.js').checkDocNamesRealCli, // Check 22 — task 1003165 / G01: a doc that names a CLI invocation must match the real CLI
1902
1902
  require('./control-manifest.js').checkControlsExist, // Check 21 — task 1003131: goal-1000076 controls assert their own existence (the ADR 0022 assurance-of-assurance)
1903
1903
  ];
1904
1904
 
@@ -0,0 +1,150 @@
1
+ #!/usr/bin/env node
2
+ // scripts/gds/ship-honesty.js — a ship must not lose the builder's prose, and
3
+ // must not lie about how far it got (task 1003168 / G04, goal 1000077;
4
+ // criterion guards-ship-cannot-lie).
5
+ //
6
+ // TWO fixes live here because they are one incident. A ship was invoked with
7
+ // handoff notes containing backticks; bash command-substituted them before node
8
+ // ran, truncating the argument list. The task resolved with EMPTY notes, landed
9
+ // at 'completed', and the process died before grading with a bare
10
+ // `fatal: <stack>`. Two failures compounded: the prose was lost (nothing in the
11
+ // ledger about a session that happened), and the status could not say which leg
12
+ // died — 'completed' is ALSO what a grade-failed task looks like.
13
+ //
14
+ // Split out of ship.js, which is ~4600 lines and far over the size ratchet, so
15
+ // adding this there would have regressed excess_lines_total.
16
+
17
+ 'use strict';
18
+
19
+ const { argText } = require('./cli-lib.js');
20
+
21
+ // resolveProse(args, specs) — resolve every prose argument for one invocation
22
+ // through cli-lib.argText, refusing the combinations that would silently lose
23
+ // text. Returns a { [bareName]: string } map, or exits 2 having explained why.
24
+ //
25
+ // The extra rule argText cannot enforce alone: stdin can only be consumed ONCE,
26
+ // so two `-file -` arguments in the same command would hand the first reader
27
+ // everything and the second an empty string. Refuse instead — a ship whose notes
28
+ // silently came back empty is the exact incident this whole task is about
29
+ // (learning 1000197).
30
+ function resolveProse(args, specs) {
31
+ const out = {};
32
+ const stdinUsers = [];
33
+ for (const name of specs) {
34
+ const r = argText(`--${name}`, args);
35
+ if (r.error) {
36
+ console.error(r.error);
37
+ process.exit(2);
38
+ }
39
+ if (r.source === 'stdin') stdinUsers.push(`--${name}-file`);
40
+ out[name] = r.value;
41
+ }
42
+ if (stdinUsers.length > 1) {
43
+ console.error(`${stdinUsers.join(' and ')} both read stdin, which can only be consumed once — `
44
+ + 'the second would silently come back empty. Put at least one in a real file.');
45
+ process.exit(2);
46
+ }
47
+ return out;
48
+ }
49
+
50
+ // ---------- The ship must not lie about how far it got (G04, task 1003168) ----
51
+ //
52
+ // THE INCIDENT. A ship died after the claim resolved and before the grade
53
+ // recorded. resolveClaim had already landed the task at 'completed', so the
54
+ // process printed `fatal: <stack>` and exited — and 'completed' is exactly what
55
+ // a task ALSO looks like when its grade ran and failed. The status could not
56
+ // distinguish "the pipeline stopped here" from "the pipeline got further and the
57
+ // grade said no", and the work went unrecovered because nobody could tell.
58
+ //
59
+ // The cure is not more status values: it is that the process which knows where it
60
+ // died must SAY so on the way out. shipProgress records the last leg that
61
+ // completed; fatalRecoveryLines turns it into an instruction.
62
+ let shipProgress = { stage: 'preflight', taskId: null };
63
+
64
+ // The one writer. Called by ship.js as each leg completes.
65
+ function markShipProgress(stage, taskId) {
66
+ shipProgress = { stage, taskId };
67
+ }
68
+
69
+ // Pure so the wording is pinned by a DB-free test (the smokeGateMessages
70
+ // precedent) — a recovery message that drifts out of date is worse than none.
71
+ //
72
+ // WHY EVERY STAGE POINTS AT A PLAIN RE-RUN, never --regrade: shipMain on a task
73
+ // with no active claim routes by the task's CURRENT status (evaluateResumeAction)
74
+ // — 'completed' resumes the grade, 'confirmed' resumes the merge. --regrade only
75
+ // accepts 'completed' and REFUSES a 'confirmed' task with "its grade passed;
76
+ // nothing to re-grade". A builder reading a crash does not know which leg died,
77
+ // which is the entire problem, so the advice must be the command that works from
78
+ // either — and a plain re-run of a 'completed' task calls regradeMain() anyway.
79
+ function fatalRecoveryLines(progress = shipProgress) {
80
+ const id = progress.taskId;
81
+ const rerun = id ? `node scripts/gds/ship.js ${id}` : 'node scripts/gds/ship.js <task-id>';
82
+ const head = 'The ship did NOT finish. Where it stopped, and what recovers it:';
83
+ switch (progress.stage) {
84
+ case 'resolved':
85
+ return [
86
+ head,
87
+ ` Task #${id} is at 'completed' — the claim resolved, the grade did NOT record.`,
88
+ ' No credits awarded. Your commits are safe; nothing was lost but the grade.',
89
+ ` Resume: ${rerun}`,
90
+ " (A plain re-run resumes the stalled leg. Do NOT reach for --regrade: it only",
91
+ " accepts 'completed', so it refuses outright if the ship actually got further.)",
92
+ ];
93
+ case 'graded':
94
+ return [
95
+ head,
96
+ ` Task #${id} is at 'confirmed' — it graded and PASSED; the merge/deploy did not finish.`,
97
+ ' Credits land on the merge, so they have not been awarded yet.',
98
+ ` Resume: ${rerun}`,
99
+ ' (--regrade would refuse this one — its grade already passed.)',
100
+ ];
101
+ case 'landed':
102
+ return [
103
+ 'The ship LANDED; the failure above is in the after-work:',
104
+ ` Task #${id} reached 'shipped' — the merge landed and credits were awarded.`,
105
+ ' Broadcast / card / cleanup did not complete, which does not undo the ship.',
106
+ ` Safe to re-run if you want the after-work retried: ${rerun}`,
107
+ ];
108
+ case 'preflight':
109
+ default:
110
+ return [
111
+ head,
112
+ ' The claim was NOT resolved — it is still active and the task is unchanged.',
113
+ ` Fix the error above and re-run: ${rerun}`,
114
+ ];
115
+ }
116
+ }
117
+
118
+ // task 1002605 (G-18): the pre-resolve smoke gate's two message sets, pure so the
119
+ // wording is pinned by a DB-free unit test (tests/ship_smoke_gate.mjs). A smoke
120
+ // FAIL used to fall silently through into the claim resolve — claim consumed,
121
+ // grading skipped with no message, task parked at 'completed', exit 0 — the
122
+ // choke-point map's likeliest "my task vanished" shape. `abort` is the default
123
+ // (printed BEFORE the resolve; the claim stays active); `parked` is the
124
+ // --allow-smoke-fail continuation (printed where the grader would have run, so
125
+ // the builder knows exactly where the task stopped and how to resume).
126
+ function smokeGateMessages(taskId) {
127
+ return {
128
+ abort: [
129
+ 'Ship ABORTED: the production smoke test FAILED — see the FAIL line(s) above.',
130
+ 'No claim resolved, no credits awarded — your claim is still active and nothing changed.',
131
+ 'The smoke suite hits LIVE endpoints (your change is not deployed yet), so this usually means production is briefly degraded — not that your change broke it.',
132
+ ` - Re-run once it recovers: node scripts/gds/ship.js ${taskId}`,
133
+ ` - Ship anyway (deliberate): node scripts/gds/ship.js ${taskId} --allow-smoke-fail`,
134
+ ` (parks the task at 'completed' ungraded; a plain re-run resumes it)`,
135
+ ],
136
+ parked: [
137
+ '--allow-smoke-fail: smoke FAILED and the ship deliberately stops here.',
138
+ `Task #${taskId} is parked at 'completed' — ungraded, unconfirmed, no credits awarded.`,
139
+ `Resume when production is healthy: node scripts/gds/ship.js ${taskId}`,
140
+ ' (A plain re-run routes to whichever leg stalled. --regrade only accepts a task at',
141
+ " 'completed', so it refuses if the ship got further — task 1003168.)",
142
+ ],
143
+ };
144
+ }
145
+
146
+ module.exports = {
147
+ resolveProse, markShipProgress, fatalRecoveryLines, smokeGateMessages,
148
+ _resetForTest: () => { shipProgress = { stage: 'preflight', taskId: null }; },
149
+ _peekForTest: () => shipProgress,
150
+ };
@@ -12,6 +12,7 @@
12
12
  // the task stays at 'confirmed' for /merge-mode to land later.
13
13
  //
14
14
  // Usage:
15
+ // node scripts/gds/ship.js <task-id> --notes-file notes.md --summary-file summary.txt
15
16
  // node scripts/gds/ship.js <task-id> --notes "handoff markdown" --summary "value-summary line"
16
17
  // node scripts/gds/ship.js <task-id> --no-merge # stop after confirm; skip auto-merge
17
18
  // node scripts/gds/ship.js <task-id> --db-only # deliberate zero-commit / direct-SQL ship (task 391):
@@ -62,6 +63,8 @@ const os = require('node:os');
62
63
  const path = require('node:path');
63
64
  const readline = require('node:readline');
64
65
  const { cliClient, requireSession, loadSession, loadSessionSync, printOnboardingNudge, arg, rewardPolicy } = require('./cli-lib');
66
+ // task 1003168 (G04): how a ship reports prose + an unfinished run — see that file.
67
+ const { resolveProse, markShipProgress, fatalRecoveryLines, smokeGateMessages } = require('./ship-honesty.js');
65
68
 
66
69
  // The instance's reward policy, as reported by the server on GET /me (ADR 0146 /
67
70
  // task 1002469). Cached module-level because the credits print + the completion
@@ -2235,32 +2238,6 @@ function runSmokeAsync() {
2235
2238
  });
2236
2239
  }
2237
2240
 
2238
- // task 1002605 (G-18): the pre-resolve smoke gate's two message sets, pure so the
2239
- // wording is pinned by a DB-free unit test (tests/ship_smoke_gate.mjs). A smoke
2240
- // FAIL used to fall silently through into the claim resolve — claim consumed,
2241
- // grading skipped with no message, task parked at 'completed', exit 0 — the
2242
- // choke-point map's likeliest "my task vanished" shape. `abort` is the default
2243
- // (printed BEFORE the resolve; the claim stays active); `parked` is the
2244
- // --allow-smoke-fail continuation (printed where the grader would have run, so
2245
- // the builder knows exactly where the task stopped and how to resume).
2246
- function smokeGateMessages(taskId) {
2247
- return {
2248
- abort: [
2249
- 'Ship ABORTED: the production smoke test FAILED — see the FAIL line(s) above.',
2250
- 'No claim resolved, no credits awarded — your claim is still active and nothing changed.',
2251
- 'The smoke suite hits LIVE endpoints (your change is not deployed yet), so this usually means production is briefly degraded — not that your change broke it.',
2252
- ` - Re-run once it recovers: node scripts/gds/ship.js ${taskId}`,
2253
- ` - Ship anyway (deliberate): node scripts/gds/ship.js ${taskId} --allow-smoke-fail`,
2254
- ` (parks the task at 'completed' ungraded; resume later with --regrade)`,
2255
- ],
2256
- parked: [
2257
- '--allow-smoke-fail: smoke FAILED and the ship deliberately stops here.',
2258
- `Task #${taskId} is parked at 'completed' — ungraded, unconfirmed, no credits awarded.`,
2259
- `Resume when production is healthy: node scripts/gds/ship.js ${taskId} --regrade`,
2260
- ],
2261
- };
2262
- }
2263
-
2264
2241
  // ---------- Step 3: merge + deploy chain ----------
2265
2242
 
2266
2243
  function currentBranch() {
@@ -3696,11 +3673,14 @@ async function shipMain() {
3696
3673
  const args = process.argv.slice(2);
3697
3674
  const taskId = Number(args[0]);
3698
3675
  if (!Number.isFinite(taskId)) {
3699
- console.error('usage: node scripts/gds/ship.js <task-id> --notes "..." --summary "..." [--surprise "text" | --surprise none | --no-surprise] [--no-merge] [--db-only] [--verified] [--allow-empty] [--allow-smoke-fail] [--base <ref>]');
3676
+ console.error('usage: node scripts/gds/ship.js <task-id> (--notes-file <path|-> | --notes "...") (--summary-file <path|-> | --summary "...") [--surprise "text" | --surprise none | --no-surprise] [--no-merge] [--db-only] [--verified] [--allow-empty] [--allow-smoke-fail] [--base <ref>]');
3677
+ console.error(' prefer the -file forms for prose: a shell command-substitutes backticks in an inline value before node sees it (learning 1000197).');
3700
3678
  process.exit(2);
3701
3679
  }
3702
- const notes = arg('--notes', args) || '';
3703
- const summary = arg('--summary', args) || '';
3680
+ // G04: prose from a FILE or inline, never shell-mangled in silence.
3681
+ const prose = resolveProse(args, ['notes', 'summary']);
3682
+ const notes = prose.notes;
3683
+ const summary = prose.summary;
3704
3684
  const skipMerge = args.includes('--no-merge');
3705
3685
  const force = args.includes('--force');
3706
3686
  // task 391 (idea #37): a deliberate zero-commit / direct-SQL ship. Without it,
@@ -4114,6 +4094,8 @@ async function shipMain() {
4114
4094
 
4115
4095
  const taskStatus = r.data.taskStatus || 'shipped (legacy response)';
4116
4096
  console.log(`Task #${r.data.taskId} is now: ${taskStatus}`);
4097
+ // Claim CONSUMED, task at 'completed' — a crash from here needs fatalRecoveryLines.
4098
+ markShipProgress('resolved', taskId);
4117
4099
 
4118
4100
  // ---- Step 2: subagent quality grader ----
4119
4101
  // Runs when local verification passed AND resolveClaim landed at completed.
@@ -4241,6 +4223,10 @@ async function shipMain() {
4241
4223
  // ---- Step 3: confirmed→shipped tail (merge + deploy → memory/session sync →
4242
4224
  // Done → onboarding nudge → completion card), shared with regradeMain via
4243
4225
  // finishShip (#1093/C9). ----
4226
+ // Graded. If it passed, only the merge remains — a different crash story.
4227
+ if (postGradeStatus === 'confirmed' || postGradeStatus === 'shipped') {
4228
+ markShipProgress(postGradeStatus === 'shipped' ? 'landed' : 'graded', taskId);
4229
+ }
4244
4230
  await finishShip({
4245
4231
  taskId, summary, advanceToMerge, skipMerge, postGradeStatus, doneLine: 'Done.', dbOnly,
4246
4232
  // task 1270: data for the deterministic completion card.
@@ -4283,14 +4269,15 @@ async function regradeMain() {
4283
4269
  const args = process.argv.slice(2);
4284
4270
  const taskId = Number(args[0]);
4285
4271
  if (!Number.isFinite(taskId)) {
4286
- console.error('usage: node scripts/gds/ship.js <task-id> --regrade [--summary "..."] [--response "..."] [--no-merge] [--force] [--verified] [--base <ref>]');
4272
+ console.error('usage: node scripts/gds/ship.js <task-id> --regrade [--summary-file <path|-> | --summary "..."] [--response-file <path|-> | --response "..."] [--no-merge] [--force] [--verified] [--base <ref>]');
4287
4273
  process.exit(2);
4288
4274
  }
4289
- const summaryArg = arg('--summary', args) || '';
4275
+ const regradeProse = resolveProse(args, ['summary', 'response']);
4276
+ const summaryArg = regradeProse.summary;
4290
4277
  // task 1002662: the builder's response to the prior round's findings — how
4291
4278
  // each was fixed, tracked (a task ref), or why it's out of scope. Rides to
4292
4279
  // the panel fenced, under the delta re-grade policy.
4293
- const responseArg = arg('--response', args) || '';
4280
+ const responseArg = regradeProse.response;
4294
4281
  const skipMerge = args.includes('--no-merge');
4295
4282
  const force = args.includes('--force');
4296
4283
  // task 1002422: honor --base on regrade/resume too, so `--regrade --base <ref>`
@@ -4609,6 +4596,7 @@ module.exports = {
4609
4596
  // / non-ci → false) is unit-tested with no gh/network (env-driven via
4610
4597
  // OTB_DEPLOY_MODE + OTB_PUSH_VIA_SERVER).
4611
4598
  broadcastHandledByServer,
4599
+ fatalRecoveryLines, // G04 re-export (definition in ship-honesty.js)
4612
4600
  };
4613
4601
 
4614
4602
  if (require.main === module) {
@@ -4617,6 +4605,9 @@ if (require.main === module) {
4617
4605
  const main = process.argv.slice(2).includes('--regrade') ? regradeMain : shipMain;
4618
4606
  main().catch((err) => {
4619
4607
  console.error('fatal:', err);
4608
+ // G04: never exit on a stack trace alone — say which leg died.
4609
+ console.error('');
4610
+ for (const line of fatalRecoveryLines()) console.error(line);
4620
4611
  process.exit(1);
4621
4612
  });
4622
4613
  }
package/src/module-api.js CHANGED
@@ -1037,8 +1037,12 @@ const { validateOrRespond, LIMITS, parseId, asyncHandler, corsPublicGet, parsePa
1037
1037
  // landed since 1.19.303 with no explicit bump. run 32660527575. (task 1002620)
1038
1038
  // 1.19.305 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
1039
1039
  // landed since 1.19.304 with no explicit bump. run 32660902666. (task 1002620)
1040
+ // 1.19.306 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
1041
+ // landed since 1.19.305 with no explicit bump. run 32662000590. (task 1002620)
1042
+ // 1.19.307 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
1043
+ // landed since 1.19.306 with no explicit bump. run 32663876767. (task 1002620)
1040
1044
  // ---------------------------------------------------------------------------
1041
- const CORE_VERSION = '1.19.305'; // CI auto-patch carrier (ADR 0161); see changelog above
1045
+ const CORE_VERSION = '1.19.307'; // CI auto-patch carrier (ADR 0161); see changelog above
1042
1046
 
1043
1047
  // A namespaced logger so a module's log lines are attributable + consistent.
1044
1048
  // Usage: const log = api.logger('dev-box'); log.info('mounted');
@@ -0,0 +1,195 @@
1
+ // tests/doc_cli_guard.mjs — G01, task 1003165, goal 1000077, criterion
2
+ // guards-doc-names-a-command.
3
+ //
4
+ // This test's whole reason for existing is that the LAST test of this kind was
5
+ // useless. Task 1002931 shipped a skill telling agents to run
6
+ // `capture.js --full --fields-file`, and its test passed — because it asserted
7
+ // the skill file CONTAINED those words, never that the words worked. So the
8
+ // suite was green while the documented command silently ignored both flags.
9
+ //
10
+ // Therefore: every case below drives the check to a VERDICT on injected input.
11
+ // The negative cases prove it actually fails; a guard that has never been seen
12
+ // to fail is not evidence of anything.
13
+ import assert from 'node:assert/strict';
14
+ import { test } from 'node:test';
15
+ import { createRequire } from 'node:module';
16
+ import path from 'node:path';
17
+ import { fileURLToPath } from 'node:url';
18
+
19
+ const require = createRequire(import.meta.url);
20
+ const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
21
+ const g = require(path.join(ROOT, 'scripts', 'gds', 'doc-cli-guard.js'));
22
+
23
+ // A tiny injected world: doc text and script source, by path.
24
+ const world = (docs, scripts) => ({
25
+ readDoc: (p) => (p in docs ? docs[p] : null),
26
+ readScript: (p) => (p in scripts ? scripts[p] : null),
27
+ allow: new Map(),
28
+ });
29
+
30
+ test('the live tree passes', () => {
31
+ const r = g.checkDocNamesRealCli();
32
+ assert.equal(r.hardFail, false, `unexpected violations:\n ${r.violations.join('\n ')}`);
33
+ assert.equal(r.ok, true);
34
+ });
35
+
36
+ test('the live scan actually reads the repo — not a vacuous pass', () => {
37
+ // The failure mode this pins: a scan that enumerates nothing reports "clean".
38
+ const r = g.checkDocNamesRealCli();
39
+ assert.match(r.note, /(\d+) documented/);
40
+ const invocations = Number(r.note.match(/^(\d+) documented/)[1]);
41
+ assert.ok(invocations > 50, `expected the real repo to yield many invocations, got ${invocations}`);
42
+ });
43
+
44
+ test('an empty enumeration FAILS rather than passing quietly', () => {
45
+ const r = g.checkDocNamesRealCli({ files: [] });
46
+ assert.equal(r.hardFail, true);
47
+ assert.match(r.violations[0], /scan defect/);
48
+ });
49
+
50
+ // ---- the regression that motivated the check ------------------------------
51
+ test('THE 1002931 CASE: a doc naming a flag the script does not have is caught', () => {
52
+ const { violations } = g.scanDocs(['skill.md'], world(
53
+ { 'skill.md': 'Run `node scripts/gds/capture.js --full --fields-file f.json`.' },
54
+ // capture.js as it was on the OTHER machine: no --full, no --fields-file.
55
+ { 'scripts/gds/capture.js': "const text = process.argv.slice(2).join(' ');" },
56
+ ));
57
+ assert.equal(violations.length, 2, violations.join('\n'));
58
+ assert.ok(violations.some((v) => v.includes('--full')));
59
+ assert.ok(violations.some((v) => v.includes('--fields-file')));
60
+ assert.match(violations[0], /silently IGNORED/);
61
+ });
62
+
63
+ test('the same doc PASSES once the script really carries the flags', () => {
64
+ const { violations } = g.scanDocs(['skill.md'], world(
65
+ { 'skill.md': 'Run `node scripts/gds/capture.js --full --fields-file f.json`.' },
66
+ { 'scripts/gds/capture.js': "const full = args.includes('--full'); const ff = arg('--fields-file');" },
67
+ ));
68
+ assert.deepEqual(violations, []);
69
+ });
70
+
71
+ test('a doc naming a script that does not exist is caught', () => {
72
+ const { violations } = g.scanDocs(['skill.md'], world(
73
+ { 'skill.md': 'run `node scripts/gds/art-stage.js` to publish' }, {},
74
+ ));
75
+ assert.equal(violations.length, 1);
76
+ assert.match(violations[0], /does not exist/);
77
+ });
78
+
79
+ // ---- no false positives (the five that killed the first draft) ------------
80
+ test('every real argv idiom in this repo is accepted', () => {
81
+ // Draft 1 matched only arg()/hasFlag() with the flag as first argument, and
82
+ // called all five of these broken. They are all correct code.
83
+ const idioms = {
84
+ 'argv.indexOf': "const i = argv.indexOf('--development');",
85
+ 'hasFlag(args, x)': "const apply = hasFlag(args, '--development');",
86
+ 'argVal(args, x)': "const v = argVal(args, '--development');",
87
+ 'strict equality': "else if (a === '--development') on = true;",
88
+ 'Set.has': "const on = ARGS.has('--development');",
89
+ 'usage text only': '// node scripts/gds/x.js --development <path>',
90
+ };
91
+ for (const [label, src] of Object.entries(idioms)) {
92
+ const { violations } = g.scanDocs(['d.md'], world(
93
+ { 'd.md': 'run `node scripts/gds/x.js --development p.json`' },
94
+ { 'scripts/gds/x.js': src },
95
+ ));
96
+ assert.deepEqual(violations, [], `${label} must be accepted`);
97
+ }
98
+ });
99
+
100
+ test('a LONGER flag in the source does not satisfy a shorter documented one', () => {
101
+ // The direction that actually needs the trailing guard: the source contains
102
+ // only `--client-id`, and a bare substring search for `--client` would find
103
+ // it INSIDE that and wave the doc through. Written the other way round this
104
+ // case passes with or without the guard, so it proves nothing — the first
105
+ // draft of this test had it backwards and a mutation run caught it.
106
+ const { violations } = g.scanDocs(['d.md'], world(
107
+ { 'd.md': 'run `node scripts/gds/x.js --client abc`' },
108
+ { 'scripts/gds/x.js': "const c = argVal(args, '--client-id');" },
109
+ ));
110
+ assert.equal(violations.length, 1, 'a longer flag must not satisfy a shorter one');
111
+ assert.match(violations[0], /--client\b/);
112
+ });
113
+
114
+ test('and the exact documented flag is still accepted alongside a longer sibling', () => {
115
+ const { violations } = g.scanDocs(['d.md'], world(
116
+ { 'd.md': 'run `node scripts/gds/x.js --client abc`' },
117
+ { 'scripts/gds/x.js': "argVal(args, '--client-id'); argVal(args, '--client');" },
118
+ ));
119
+ assert.deepEqual(violations, []);
120
+ });
121
+
122
+ test('flags do not leak across two invocations in one doc', () => {
123
+ // b.js carries --beta, a.js does not; only a.js/--alpha may be flagged.
124
+ const { violations } = g.scanDocs(['d.md'], world(
125
+ { 'd.md': 'first `node scripts/gds/a.js --alpha`\nthen `node scripts/gds/b.js --beta`' },
126
+ { 'scripts/gds/a.js': 'nothing here', 'scripts/gds/b.js': "arg('--beta')" },
127
+ ));
128
+ assert.equal(violations.length, 1, violations.join('\n'));
129
+ assert.match(violations[0], /a\.js.*--alpha/);
130
+ });
131
+
132
+ test('universal plumbing flags are not reported', () => {
133
+ const { violations } = g.scanDocs(['d.md'], world(
134
+ { 'd.md': 'run `node scripts/gds/x.js --quiet --json --help --dry-run`' },
135
+ { 'scripts/gds/x.js': 'no flags at all' },
136
+ ));
137
+ assert.deepEqual(violations, []);
138
+ });
139
+
140
+ // ---- the allowlist -------------------------------------------------------
141
+ test('an allowlisted invocation is tolerated — and covers its own flags', () => {
142
+ const allow = new Map([['d.md::scripts/gds/gone.js', 'ships with an excluded host module']]);
143
+ const { violations } = g.scanDocs(['d.md'], {
144
+ readDoc: () => 'run `node scripts/gds/gone.js --whatever`',
145
+ readScript: () => null,
146
+ allow,
147
+ });
148
+ assert.deepEqual(violations, []);
149
+ });
150
+
151
+ test('every live allowlist entry carries a justification', () => {
152
+ assert.ok(g.DOC_CLI_ALLOW.size > 0, 'the allowlist documents the real exceptions');
153
+ for (const [key, reason] of g.DOC_CLI_ALLOW) {
154
+ assert.match(key, /::/, `${key} must be <doc>::<token>`);
155
+ assert.ok(String(reason).trim().length > 30, `${key} needs a real reason, not a stub`);
156
+ }
157
+ });
158
+
159
+ // ---- the residual gap the criterion names: "parsed", not merely present ----
160
+ test('a flag surviving ONLY in a usage comment warns without failing', () => {
161
+ // The criterion asks that a documented flag be PARSED. Presence is all a static
162
+ // check can prove, so the leftover case — implementation deleted, usage banner
163
+ // left behind — is surfaced as a warning. It must NOT be a violation: some
164
+ // scripts legitimately name a flag only in their banner and handle it via a
165
+ // helper, and failing on that is what sank the first draft.
166
+ const { violations, warnings } = g.scanDocs(['d.md'], world(
167
+ { 'd.md': 'run `node scripts/gds/x.js --gone`' },
168
+ { 'scripts/gds/x.js': '// usage: x.js --gone <path>\nconst nothing = 1;' },
169
+ ));
170
+ assert.deepEqual(violations, [], 'a comment-only mention must not fail the build');
171
+ assert.equal(warnings.length, 1);
172
+ assert.match(warnings[0], /only in comments/);
173
+ });
174
+
175
+ test('a flag present in real code produces NO warning', () => {
176
+ const { violations, warnings } = g.scanDocs(['d.md'], world(
177
+ { 'd.md': 'run `node scripts/gds/x.js --live`' },
178
+ { 'scripts/gds/x.js': "// usage: x.js --live\nconst on = args.includes('--live');" },
179
+ ));
180
+ assert.deepEqual(violations, []);
181
+ assert.deepEqual(warnings, []);
182
+ });
183
+
184
+ test('stripLineComments keeps code and drops both comment forms', () => {
185
+ const out = g.stripLineComments("const a = '--keep'; // --dropped\n/* --alsodropped */\n");
186
+ assert.ok(out.includes('--keep'));
187
+ assert.ok(!out.includes('--dropped'));
188
+ assert.ok(!out.includes('--alsodropped'));
189
+ });
190
+
191
+ test('a URL is not mistaken for a comment', () => {
192
+ // `//` inside a string must not truncate the line and hide a real flag.
193
+ const out = g.stripLineComments("const u = 'https://x.test/?f=--real';");
194
+ assert.ok(out.includes('--real'), 'a protocol slash-slash must not eat the line');
195
+ });
@@ -427,19 +427,127 @@ test('LIVE: checkRouteRanks() on src/bongos/routes/ has zero unknowns', () => {
427
427
  assert.equal(res.hardFail, false);
428
428
  });
429
429
 
430
- // The census invariant (BV1.R109b). Asserting that the 10 known multi-line paths parse
431
- // would prove nothing about the ELEVENTH — the next route someone declares in a form
432
- // this scanner doesn't handle. So count instead: every `router.<method>(` line in every
433
- // discovered route file must yield exactly one classified route.
430
+ // The census invariant (BV1.R109b, re-derived in task 1002671). Asserting that the 10
431
+ // known multi-line paths parse would prove nothing about the ELEVENTH — the next route
432
+ // someone declares in a form this scanner doesn't handle. So count instead: every
433
+ // `router.<method>(` call in every discovered route file must yield exactly one
434
+ // classified route.
434
435
  //
435
- // The census regex below is HAND-WRITTEN here on purpose and NOT imported from the
436
- // module. Importing ROUTE_DECL_RE would move both sides of the comparison together and
437
- // prove nothing (the oracle trap tests/route_permission_check.mjs documents).
438
- const CENSUS_DECL_RE = /^\s*router\.(?:get|post|patch|put|delete)\s*\(/;
436
+ // ORACLE INDEPENDENCE (the 2026-07-30 review's HIGH finding, confirmed): the first cut
437
+ // of this census was a verbatim copy of the scanner's own ROUTE_DECL_RE — hand-written,
438
+ // but shape-identical, so any widening of the scanner would be pasted into the census
439
+ // and both sides would move together. The census below is derived on a structurally
440
+ // different axis instead:
441
+ // • token-level, not line-level — comments and string/template/regex literals are
442
+ // stripped first by a tiny tokenizer, then calls are counted in the whole text;
443
+ // • not line-anchored — an inline/chained/mid-line declaration counts here even
444
+ // though the scanner's `^\s*` form cannot see it;
445
+ // • whitespace-tolerant across the member access (`router . get (` counts).
446
+ // A unilateral change to the scanner's recognizer now shows up as a census mismatch
447
+ // and forces a DELIBERATE census-side decision, instead of going green by paste.
448
+ //
449
+ // The tokenizer is deliberately small and fail-closed: if it ever misreads a future
450
+ // route file, the equality below goes red and a human looks — that is the correct
451
+ // failure mode for an audit oracle (never silently green).
452
+ function stripNonCode(src) {
453
+ let out = '';
454
+ const n = src.length;
455
+ const tplBraceDepth = []; // one entry per open template-literal interpolation
456
+ let i = 0;
457
+ const KEYWORDS_BEFORE_REGEX = new Set(['return', 'typeof', 'case', 'in', 'of', 'instanceof', 'new', 'delete', 'void', 'do', 'else', 'yield', 'await']);
458
+ const regexCanFollow = () => {
459
+ const t = out.trimEnd();
460
+ if (t === '') return true;
461
+ const last = t[t.length - 1];
462
+ if ('(,=:[!&|?{};+-*%~^<>'.includes(last)) return true;
463
+ const word = /[A-Za-z_$][\w$]*$/.exec(t);
464
+ return !!(word && KEYWORDS_BEFORE_REGEX.has(word[0]));
465
+ };
466
+ // Consume string/template BODY starting at src[i] (opening quote already emitted
467
+ // and consumed). For templates, stops early at `${` and pushes an interpolation
468
+ // frame — the main loop then lexes the interpolation as code until its `}`.
469
+ const skipStringBody = (quote) => {
470
+ while (i < n && src[i] !== quote) {
471
+ if (src[i] === '\\') { out += '..'; i += 2; continue; }
472
+ if (quote === '`' && src[i] === '$' && src[i + 1] === '{') { out += '${'; i += 2; tplBraceDepth.push(0); return 'interp'; }
473
+ out += (src[i] === '\n' ? '\n' : '.'); i++;
474
+ }
475
+ out += quote; i++; // closing quote (or EOF)
476
+ return 'closed';
477
+ };
478
+ const skipString = (quote) => {
479
+ out += quote; i++;
480
+ return skipStringBody(quote);
481
+ };
482
+ while (i < n) {
483
+ const c = src[i], d = src[i + 1];
484
+ if (c === '/' && d === '/') { while (i < n && src[i] !== '\n') i++; continue; }
485
+ if (c === '/' && d === '*') { i += 2; while (i < n && !(src[i] === '*' && src[i + 1] === '/')) { if (src[i] === '\n') out += '\n'; i++; } i += 2; out += ' '; continue; }
486
+ if (c === '/' && regexCanFollow()) { // regex literal (handles quotes inside, e.g. /[&<>"']/)
487
+ out += '/'; i++;
488
+ let inClass = false;
489
+ while (i < n && (inClass || src[i] !== '/')) {
490
+ if (src[i] === '\\') { out += '..'; i += 2; continue; }
491
+ if (src[i] === '[') inClass = true;
492
+ else if (src[i] === ']') inClass = false;
493
+ out += '.'; i++;
494
+ }
495
+ out += '/'; i++;
496
+ while (i < n && /[a-z]/i.test(src[i])) { out += src[i]; i++; } // flags
497
+ continue;
498
+ }
499
+ if (c === '\'' || c === '"' || c === '`') { skipString(c); continue; }
500
+ if (tplBraceDepth.length) { // inside a template interpolation: brace-track back to the template
501
+ if (c === '{') tplBraceDepth[tplBraceDepth.length - 1]++;
502
+ if (c === '}') {
503
+ if (tplBraceDepth[tplBraceDepth.length - 1] === 0) {
504
+ tplBraceDepth.pop(); out += '}'; i++;
505
+ skipStringBody('`'); // resume the enclosing template's body (may re-enter an interpolation)
506
+ continue;
507
+ }
508
+ tplBraceDepth[tplBraceDepth.length - 1]--;
509
+ }
510
+ }
511
+ out += c; i++;
512
+ }
513
+ return out;
514
+ }
515
+
516
+ // HAND-WRITTEN on purpose and NOT imported from the module (the oracle trap
517
+ // tests/route_permission_check.mjs documents) — and, unlike the pre-1002671 census,
518
+ // deliberately NOT shape-identical to the scanner's ROUTE_DECL_RE either.
519
+ const CENSUS_CALL_RE = /\brouter\s*\.\s*(?:get|post|patch|put|delete)\s*\(/g;
520
+ // A computed member declaration (`router[method](…)`) is invisible to every static
521
+ // recognizer on both sides. Fail loud instead of silently under-counting.
522
+ const CENSUS_DYNAMIC_RE = /\brouter\s*\[/;
439
523
  // A floor, so a change that silently guts BOTH sides (e.g. discoverRouteFiles stops
440
524
  // finding module routes) reds instead of trivially "matching" at a smaller number.
441
525
  const ROUTE_CENSUS_FLOOR = 258;
442
526
 
527
+ function censusCount(text) {
528
+ const code = stripNonCode(String(text));
529
+ assert.ok(!CENSUS_DYNAMIC_RE.test(code),
530
+ 'route file uses a computed router[method](…) declaration — invisible to the rank auditor; declare it statically');
531
+ return (code.match(CENSUS_CALL_RE) || []).length;
532
+ }
533
+
534
+ test('census tokenizer: strips comments, strings, and quote-bearing regex literals', () => {
535
+ assert.equal(censusCount(`// router.get('/a', h)\nrouter.post('/b', h);`), 1);
536
+ assert.equal(censusCount(`const s = "router.get('/a'"; router.get('/b', h);`), 1);
537
+ assert.equal(censusCount(`s.replace(/[&<>"']/g, esc); router.get('/x', h);`), 1);
538
+ assert.equal(censusCount('const t = `x ${router.get(\'/live\', h)} y`;'), 1);
539
+ });
540
+
541
+ test('census independence: counts declarations the line-anchored scanner cannot see', () => {
542
+ // Inline (mid-line) declaration: census sees it, scanDeclarations does not. If the
543
+ // scanner ever learns this form, this fixture forces the census comparison to be
544
+ // revisited deliberately rather than drifting in lockstep.
545
+ const text = `if (flag) router.get('/inline', h);`;
546
+ assert.equal(censusCount(text), 1);
547
+ assert.equal(rrc.classifyText(text).length, 0,
548
+ 'fixture stale: the scanner now classifies inline declarations — update the census comment');
549
+ });
550
+
443
551
  test('LIVE census: every router.<method>( declaration classifies to exactly one route [BV1.R109b]', () => {
444
552
  const fs = require('node:fs');
445
553
  let declared = 0;
@@ -447,7 +555,7 @@ test('LIVE census: every router.<method>( declaration classifies to exactly one
447
555
  const shortfall = [];
448
556
  for (const { abs, rel } of rrc.discoverRouteFiles()) {
449
557
  const text = fs.readFileSync(abs, 'utf8');
450
- const d = text.split('\n').filter((l) => CENSUS_DECL_RE.test(l)).length;
558
+ const d = censusCount(text);
451
559
  const c = rrc.classifyText(text).length;
452
560
  declared += d;
453
561
  classified += c;