@bongos/core 1.19.306 → 1.19.308
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.
- package/.bongos-core.json +39 -19
- package/.claude/skills/builder-ship/SKILL.md +21 -9
- package/.claude/skills/fix-task/SKILL.md +1 -1
- package/docs/recipes/claude-design-loop.md +1 -1
- package/docs/recipes/core-release-pipeline.md +32 -0
- package/docs/recipes/figma-design-loop.md +1 -1
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/scripts/gds/cli-lib.js +53 -0
- package/scripts/gds/go-live.js +666 -0
- package/scripts/gds/ship-honesty.js +150 -0
- package/scripts/gds/ship.js +23 -32
- package/src/module-api.js +5 -1
- package/tests/go_live.mjs +362 -0
- package/tests/route_rank_check.mjs +117 -9
- package/tests/ship_cannot_lie.mjs +168 -0
- package/tests/ship_smoke_gate.mjs +9 -3
|
@@ -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
|
|
431
|
-
// would prove nothing about the ELEVENTH — the next route
|
|
432
|
-
// this scanner doesn't handle. So count instead: every
|
|
433
|
-
// discovered route file must yield exactly one
|
|
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
|
-
//
|
|
436
|
-
//
|
|
437
|
-
//
|
|
438
|
-
|
|
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
|
|
558
|
+
const d = censusCount(text);
|
|
451
559
|
const c = rrc.classifyText(text).length;
|
|
452
560
|
declared += d;
|
|
453
561
|
classified += c;
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
// tests/ship_cannot_lie.mjs — G04, task 1003168, goal 1000077, criterion
|
|
2
|
+
// guards-ship-cannot-lie.
|
|
3
|
+
//
|
|
4
|
+
// TWO fixes, one incident, so one test file.
|
|
5
|
+
//
|
|
6
|
+
// (1) Prose reaches ship.js through a FILE, never only through the shell.
|
|
7
|
+
// Backticks in handoff notes are command-substituted by bash BEFORE node
|
|
8
|
+
// runs: the span executes, its output replaces it, and the remaining
|
|
9
|
+
// arguments are truncated. That shipped a task with empty notes and no
|
|
10
|
+
// summary — the ledger recorded nothing about a session that happened
|
|
11
|
+
// (learning 1000197).
|
|
12
|
+
//
|
|
13
|
+
// (2) A ship that dies mid-pipeline must say where it stopped. resolveClaim
|
|
14
|
+
// lands the task at 'completed' before the grade runs, and 'completed' is
|
|
15
|
+
// ALSO what a grade-failed task looks like — so a bare `fatal: <stack>`
|
|
16
|
+
// left nobody able to tell how far it got.
|
|
17
|
+
import { strict as assert } from 'node:assert';
|
|
18
|
+
import { test } from 'node:test';
|
|
19
|
+
import { createRequire } from 'node:module';
|
|
20
|
+
|
|
21
|
+
const require = createRequire(import.meta.url);
|
|
22
|
+
const { argText } = require('../scripts/gds/cli-lib.js');
|
|
23
|
+
const { fatalRecoveryLines, markShipProgress, _peekForTest } = require('../scripts/gds/ship-honesty.js');
|
|
24
|
+
|
|
25
|
+
// Injected IO so no case touches the filesystem.
|
|
26
|
+
const io = {
|
|
27
|
+
readFile: (f) => {
|
|
28
|
+
if (f === 'notes.md') return 'real notes with `backticks` in them';
|
|
29
|
+
if (f === 'bom.md') return 'after a BOM';
|
|
30
|
+
throw new Error('ENOENT: no such file');
|
|
31
|
+
},
|
|
32
|
+
readStdin: () => 'from stdin',
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
// ---- (1) prose from a file -------------------------------------------------
|
|
36
|
+
test('THE INCIDENT: a file argument carries backticks through untouched', () => {
|
|
37
|
+
// The shell never sees this content, so the backticks survive verbatim —
|
|
38
|
+
// that is the entire point of the flag.
|
|
39
|
+
const r = argText('--notes', ['--notes-file', 'notes.md'], io);
|
|
40
|
+
assert.equal(r.source, 'file');
|
|
41
|
+
assert.match(r.value, /`backticks`/);
|
|
42
|
+
assert.equal(r.error, null);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test('inline still works, for a short summary with no awkward characters', () => {
|
|
46
|
+
const r = argText('--summary', ['--summary', 'one line'], io);
|
|
47
|
+
assert.deepEqual([r.value, r.source, r.error], ['one line', 'inline', null]);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test('BOTH given is REFUSED, never silently resolved', () => {
|
|
51
|
+
// Precedence would mean one of the caller's two inputs vanished with no word
|
|
52
|
+
// said — the same quiet loss the flag exists to end.
|
|
53
|
+
const r = argText('--notes', ['--notes', 'a', '--notes-file', 'notes.md'], io);
|
|
54
|
+
assert.ok(r.error, 'both-given must be an error');
|
|
55
|
+
assert.match(r.error, /both/i);
|
|
56
|
+
assert.equal(r.value, '', 'a refusal must not hand back a half-resolved value');
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test('a dash reads stdin', () => {
|
|
60
|
+
const r = argText('--notes', ['--notes-file', '-'], io);
|
|
61
|
+
assert.deepEqual([r.value, r.source], ['from stdin', 'stdin']);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test('an unreadable file is an error, not silently empty', () => {
|
|
65
|
+
// Silently-empty is precisely how the original incident hid itself.
|
|
66
|
+
const r = argText('--notes', ['--notes-file', 'gone.md'], io);
|
|
67
|
+
assert.ok(r.error);
|
|
68
|
+
assert.match(r.error, /could not read/);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test('a missing value for the file flag is caught, including a following flag', () => {
|
|
72
|
+
for (const args of [['--notes-file'], ['--notes-file', '--summary', 'x']]) {
|
|
73
|
+
const r = argText('--notes', args, io);
|
|
74
|
+
assert.ok(r.error, `${JSON.stringify(args)} must error`);
|
|
75
|
+
assert.match(r.error, /needs a path/);
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test('a PowerShell BOM is stripped', () => {
|
|
80
|
+
// PS 5.1's `>` redirection writes a UTF-8 BOM; without stripping it the first
|
|
81
|
+
// character of every note would be U+FEFF.
|
|
82
|
+
const r = argText('--notes', ['--notes-file', 'bom.md'], io);
|
|
83
|
+
assert.equal(r.value, 'after a BOM');
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test('neither flag yields empty string, not null — callers concatenate it', () => {
|
|
87
|
+
const r = argText('--notes', [], io);
|
|
88
|
+
assert.deepEqual([r.value, r.source, r.error], ['', 'none', null]);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
// ---- (2) the ship cannot lie about how far it got --------------------------
|
|
92
|
+
test('every stage names the task, its real status, and a recovery command', () => {
|
|
93
|
+
for (const stage of ['preflight', 'resolved', 'graded', 'landed']) {
|
|
94
|
+
const lines = fatalRecoveryLines({ stage, taskId: 4242 });
|
|
95
|
+
assert.ok(Array.isArray(lines) && lines.length >= 3, `${stage} must say something`);
|
|
96
|
+
for (const l of lines) assert.equal(typeof l, 'string', `${stage} lines are printable as-is`);
|
|
97
|
+
assert.ok(lines.some((l) => l.includes('4242')), `${stage} must name the task`);
|
|
98
|
+
assert.ok(
|
|
99
|
+
lines.some((l) => l.includes('node scripts/gds/ship.js 4242')),
|
|
100
|
+
`${stage} must name the command that recovers it`,
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
test('pre-resolve says the claim is INTACT — the opposite of the post-resolve case', () => {
|
|
106
|
+
const text = fatalRecoveryLines({ stage: 'preflight', taskId: 7 }).join('\n');
|
|
107
|
+
assert.match(text, /NOT resolved/);
|
|
108
|
+
assert.match(text, /still active/);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
test("post-resolve names 'completed' AND that the grade did not record", () => {
|
|
112
|
+
// The distinction the status itself cannot make.
|
|
113
|
+
const text = fatalRecoveryLines({ stage: 'resolved', taskId: 7 }).join('\n');
|
|
114
|
+
assert.match(text, /'completed'/);
|
|
115
|
+
assert.match(text, /grade did NOT record/);
|
|
116
|
+
assert.match(text, /No credits/i);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
test("post-grade names 'confirmed' and that the grade PASSED", () => {
|
|
120
|
+
const text = fatalRecoveryLines({ stage: 'graded', taskId: 7 }).join('\n');
|
|
121
|
+
assert.match(text, /'confirmed'/);
|
|
122
|
+
assert.match(text, /PASSED/);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
test('the landed stage does NOT claim the ship failed', () => {
|
|
126
|
+
// It reached 'shipped'; only the after-work broke. Saying "the ship did not
|
|
127
|
+
// finish" there would be the same lying-status problem in reverse.
|
|
128
|
+
const lines = fatalRecoveryLines({ stage: 'landed', taskId: 7 });
|
|
129
|
+
assert.match(lines[0], /LANDED/);
|
|
130
|
+
assert.ok(!/did NOT finish/.test(lines.join('\n')));
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
test('NO stage recommends --regrade as the recovery', () => {
|
|
134
|
+
// A plain re-run routes by the task's CURRENT status, so it resumes whichever
|
|
135
|
+
// leg stalled. --regrade accepts only 'completed' and refuses a task that got
|
|
136
|
+
// to 'confirmed' — and the builder reading a crash cannot know which it is.
|
|
137
|
+
for (const stage of ['preflight', 'resolved', 'graded', 'landed']) {
|
|
138
|
+
const text = fatalRecoveryLines({ stage, taskId: 7 });
|
|
139
|
+
for (const l of text) {
|
|
140
|
+
assert.ok(
|
|
141
|
+
!/^\s*Resume:?\s+node .*--regrade/.test(l) && !/re-run:\s+node .*--regrade/.test(l),
|
|
142
|
+
`${stage} must not prescribe --regrade: ${l}`,
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
test('an unknown stage degrades to the safe pre-resolve message', () => {
|
|
149
|
+
// A future leg added without a case here must not produce a confident wrong
|
|
150
|
+
// claim about credits or status.
|
|
151
|
+
const text = fatalRecoveryLines({ stage: 'something-new', taskId: 7 }).join('\n');
|
|
152
|
+
assert.match(text, /NOT resolved/);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
test('a missing taskId still yields a usable command', () => {
|
|
156
|
+
const text = fatalRecoveryLines({ stage: 'resolved', taskId: null }).join('\n');
|
|
157
|
+
assert.match(text, /ship\.js <task-id>/);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
test('markShipProgress is the single writer, and ship.js re-exports the reader', () => {
|
|
161
|
+
// Wiring proof: the extraction must not have left ship.js reading a stale copy.
|
|
162
|
+
markShipProgress('graded', 99);
|
|
163
|
+
assert.deepEqual(_peekForTest(), { stage: 'graded', taskId: 99 });
|
|
164
|
+
assert.match(fatalRecoveryLines().join('\n'), /#99/, 'the default arg reads live progress');
|
|
165
|
+
const shipCli = require('../scripts/gds/ship.js');
|
|
166
|
+
assert.equal(typeof shipCli.fatalRecoveryLines, 'function', 'ship.js still exposes it');
|
|
167
|
+
assert.equal(shipCli.fatalRecoveryLines, fatalRecoveryLines, 'same function, not a copy');
|
|
168
|
+
});
|
|
@@ -14,7 +14,7 @@ import { strict as assert } from 'node:assert';
|
|
|
14
14
|
import { createRequire } from 'node:module';
|
|
15
15
|
|
|
16
16
|
const require = createRequire(import.meta.url);
|
|
17
|
-
const { smokeGateMessages } = require('../scripts/gds/ship.js');
|
|
17
|
+
const { smokeGateMessages } = require('../scripts/gds/ship-honesty.js');
|
|
18
18
|
|
|
19
19
|
let passed = 0;
|
|
20
20
|
let failed = 0;
|
|
@@ -61,8 +61,14 @@ t('parked: names the deliberate stop, the exact status, and that nothing was pai
|
|
|
61
61
|
assert.match(parkedText, /no credits/i);
|
|
62
62
|
});
|
|
63
63
|
|
|
64
|
-
t('parked: names the resume path (--regrade)', () => {
|
|
65
|
-
|
|
64
|
+
t('parked: names the resume path (a plain re-run, not --regrade)', () => {
|
|
65
|
+
// task 1003168 (G04): this used to say --regrade. A plain re-run routes by the
|
|
66
|
+
// task's CURRENT status, so it resumes whichever leg stalled; --regrade accepts
|
|
67
|
+
// only 'completed' and refuses a task that got as far as 'confirmed'. Since the
|
|
68
|
+
// builder reading this cannot know which leg died, the advice must be the
|
|
69
|
+
// command that works from either.
|
|
70
|
+
assert.match(parkedText, /ship\.js 1234$|ship\.js 1234\s/m);
|
|
71
|
+
assert.ok(!/ship\.js 1234 --regrade/.test(parkedText), 'must not recommend --regrade');
|
|
66
72
|
});
|
|
67
73
|
|
|
68
74
|
t('both message sets are non-empty arrays of strings (printable as-is)', () => {
|