@bongos/core 1.19.305 → 1.19.306

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 CHANGED
@@ -2,22 +2,22 @@
2
2
  "artifact": "bongos-core",
3
3
  "manifest_schema": 1,
4
4
  "generator": "scripts/gds/package-core.js",
5
- "core_version": "1.19.305",
6
- "core_contract": "1.19.305",
7
- "source_commit": "6dc5a84be60b26f11d572eeccf10b8092c7352ae",
5
+ "core_version": "1.19.306",
6
+ "core_contract": "1.19.306",
7
+ "source_commit": "bfb69ddea49ebcce6d1d0608dd8e1fc50cc755e2",
8
8
  "source_ref": "HEAD",
9
- "built_at": "2026-08-23T19:20:49.275Z",
9
+ "built_at": "2026-08-23T19:41:29.546Z",
10
10
  "redaction": {
11
11
  "model": "docs-redacted+functional-verbatim",
12
12
  "docs_redacted": 322,
13
13
  "agent_docs_stubbed": 22,
14
- "functional_verbatim": 1414,
14
+ "functional_verbatim": 1416,
15
15
  "rules": 3,
16
16
  "gate_literals": 3,
17
17
  "gate": "passed"
18
18
  },
19
- "file_count": 1758,
20
- "tree_sha256": "3a7cd2627a967fe3dbe7edf3dc916b4d728170b826669b64ae457d5e92ac10d1",
19
+ "file_count": 1760,
20
+ "tree_sha256": "a2c3f65e69e891c0076d0b10268d3549cfddf8d76d1f320274e86fd833d56013",
21
21
  "files": [
22
22
  {
23
23
  "path": ".claude/skills/blocker-review/SKILL.md",
@@ -212,12 +212,12 @@
212
212
  {
213
213
  "path": ".claude/skills/otb-tile-generate/SKILL.md",
214
214
  "mode": "0000644",
215
- "sha256": "cab6aee8bce317982447625d021d862e79c8363a1259d60d109de13331afa1bd"
215
+ "sha256": "53bfcf6c531c0aeefc1696b470ba6a746f4490501ab092766f42c501f06d453d"
216
216
  },
217
217
  {
218
218
  "path": ".claude/skills/paint/SKILL.md",
219
219
  "mode": "0000644",
220
- "sha256": "a0ef068b967e8560ead467c790543f707353bee740ce0a7fa0f01c67445119dd"
220
+ "sha256": "54af43e9417eaa369d9f166e0bbc8567c4540e064504e67ef32186fe31b2f766"
221
221
  },
222
222
  {
223
223
  "path": ".claude/skills/planning-session/SKILL.md",
@@ -4577,12 +4577,12 @@
4577
4577
  {
4578
4578
  "path": "package-lock.json",
4579
4579
  "mode": "0000644",
4580
- "sha256": "4678a5bfd751ffe8ac5c3283eb25af2e011ccb14d32032bf373c7c77bcb61a3d"
4580
+ "sha256": "ab53e6d02f437a24895dd3d04b3e8ef39ddf4257b8c4b2e784a84dd10820adc4"
4581
4581
  },
4582
4582
  {
4583
4583
  "path": "package.json",
4584
4584
  "mode": "0000644",
4585
- "sha256": "5ff86f97c1df0bf8d224512adc339aac2859db0a65e40e7783032b0128f7cbbd"
4585
+ "sha256": "c98acd264e43f570b82f0c7c72efb952d6e68758794f4c49a78db06cf44a867f"
4586
4586
  },
4587
4587
  {
4588
4588
  "path": "public-docs/index.html",
@@ -5189,6 +5189,11 @@
5189
5189
  "mode": "0000644",
5190
5190
  "sha256": "7e279cd4cb8fb0458b8b3be7b0d694e7afcc96eec926abf68b8ccd6afb65429f"
5191
5191
  },
5192
+ {
5193
+ "path": "scripts/gds/doc-cli-guard.js",
5194
+ "mode": "0000644",
5195
+ "sha256": "e7feb497a7b568273d1a2c16c2a65036c60017d163d792d05d3c43addcfd7d8f"
5196
+ },
5192
5197
  {
5193
5198
  "path": "scripts/gds/docs-entropy.js",
5194
5199
  "mode": "0000644",
@@ -5227,7 +5232,7 @@
5227
5232
  {
5228
5233
  "path": "scripts/gds/fitness.js",
5229
5234
  "mode": "0000644",
5230
- "sha256": "e4399cd9d5283ff23b8c5baf9e7b22a4663cab3f6e0e477b138f279cda50bd72"
5235
+ "sha256": "71f7b70fb6b7b2251c5a335b90faf0b63b51c614829a247679b6f25209549264"
5231
5236
  },
5232
5237
  {
5233
5238
  "path": "scripts/gds/gate-review.js",
@@ -6237,7 +6242,7 @@
6237
6242
  {
6238
6243
  "path": "src/module-api.js",
6239
6244
  "mode": "0000644",
6240
- "sha256": "a59a54dc09613fc6d95a2efa1262b6d8099fabfb1ef90289462b81accd9c7fea"
6245
+ "sha256": "31f8e9579aa3e015fc2c1f819d5d84076db27e831bfb3419ca48141cbcd84038"
6241
6246
  },
6242
6247
  {
6243
6248
  "path": "src/module-loader/catalog.js",
@@ -7019,6 +7024,11 @@
7019
7024
  "mode": "0000644",
7020
7025
  "sha256": "d9fd04cc3f2fcb3b4a9945c1e743c44445a216a10648fa619f74716b381468f3"
7021
7026
  },
7027
+ {
7028
+ "path": "tests/doc_cli_guard.mjs",
7029
+ "mode": "0000644",
7030
+ "sha256": "4e68c67489daa51167b8d738a36f7c0187016b342e8a57731d14241563262878"
7031
+ },
7022
7032
  {
7023
7033
  "path": "tests/docs_entropy.mjs",
7024
7034
  "mode": "0000644",
@@ -30,7 +30,7 @@ Runs `modules/art-pipeline/pipeline/orchestrator.py` for the requested tiles. Th
30
30
 
31
31
  **View it on your sandbox without placing it in-game** (#1262): every tile you've generated — native + zoomed, with its score and critique — is served at **`https://sandbox-<login>.example.com/art`** (locally: `http://localhost:<port>/art`). This is the generated-art *gallery* (`modules/art-pipeline/generated/preview.html`), so you can show the builder what they made even when the tile isn't placed in the world yet. The atlas-rebuild in-game step below is only needed once you actually want it *in the playable world*.
32
32
 
33
- 6. **See it in-game on your sandbox** (#1067): run `node scripts/gds/art-stage.js` — it rebuilds the atlas the client loads, surfaces it on your sandbox live preview (`sandbox-<login>.example.com`, ADR 0044), and prints the URL + affected tiles. With the auto-reload (task 1065) an already-open tab refreshes itself. (The family/refresh runs also accept `--stage` to do this automatically: `family_orchestrator.py <id> --publish --stage`, `refresh_all.py --stage`.) Only a genuinely NEW base-tile *type* still needs a `public/game/world/tilePalette.js` mapping edit; an updated existing tile just needs the atlas rebuild above.
33
+ 6. **See it in-game on your sandbox** (#1067): run `node scripts/gds/art-stage.js` (**ships with the `art-pipeline` host module, not the portable core** — absent from a core-only checkout, where this step doesn't apply) — it rebuilds the atlas the client loads, surfaces it on your sandbox live preview (`sandbox-<login>.example.com`, ADR 0044), and prints the URL + affected tiles. With the auto-reload (task 1065) an already-open tab refreshes itself. (The family/refresh runs also accept `--stage` to do this automatically: `family_orchestrator.py <id> --publish --stage`, `refresh_all.py --stage`.) Only a genuinely NEW base-tile *type* still needs a `public/game/world/tilePalette.js` mapping edit; an updated existing tile just needs the atlas rebuild above.
34
34
 
35
35
  ## Constraints
36
36
 
@@ -40,7 +40,7 @@ Ground every asset in the locked theme so nothing drifts:
40
40
  1. **Establish the visual target first, not in prose.** Confirm the subject (an existing vocab id, or a new in-world prop/plant/animal/accessory) and, if helpful, show the reference cluster it should sit beside. One or two lines, then move to making.
41
41
  2. **Generate via the pipeline — never freelance.** Drive `/otb-tile-generate` (which runs `modules/art-pipeline/pipeline/orchestrator.py`: generate → post-process to 32×32 → score against rubric → retry up to 5 tiers → save to `modules/art-pipeline/generated/tiles/<id>.png`, logging cost). Check the budget first (`python3 modules/art-pipeline/pipeline/cost_ledger.py status`; hard stop at $80) — but report it as one line.
42
42
  3. **SHOW the result.** Display the final accepted PNG and its rubric score. The gallery serves every tile (native + zoomed, with score + critique) at `sandbox-<login>.example.com/art` — point them there and/or render the image inline. **Default visibility: show the final accepted asset and any failures. Show the intermediate retry attempts only if the artist asks ("show me the attempts").**
43
- 4. **See it in the world.** When they want it placed, run `node scripts/gds/art-stage.js` — it rebuilds the atlas and surfaces the asset live in the game on their sandbox (auto-reloads an open tab). Screenshot/point at the in-world view. (A genuinely new base-tile *type* also needs a `public/game/world/tilePalette.js` mapping edit; an updated existing tile just needs the rebuild.)
43
+ 4. **See it in the world.** When they want it placed, run `node scripts/gds/art-stage.js` (**ships with the `art-pipeline` host module, not the portable core** — on a core-only checkout the file is absent and this step doesn't apply) — it rebuilds the atlas and surfaces the asset live in the game on their sandbox (auto-reloads an open tab). Screenshot/point at the in-world view. (A genuinely new base-tile *type* also needs a `public/game/world/tilePalette.js` mapping edit; an updated existing tile just needs the rebuild.)
44
44
  5. **Feedback is visual → a durable rule.** When the artist reacts ("too saturated", "trees too modern", "weather the marble"), don't just tweak once — run `/otb-feedback-capture` to write the note into `modules/art-pipeline/template/style_guide.md` as a dated rule, then regenerate and **show the new result**. The loop is see → react → see again, and the style guide gets smarter each pass.
45
45
  6. **Use `/otb-design-review`** when you want a rubric-grounded second opinion on whether an asset truly hits the bar — surface its numbers, not a paragraph.
46
46
 
package/package-lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.19.305",
3
+ "version": "1.19.306",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@bongos/core",
9
- "version": "1.19.305",
9
+ "version": "1.19.306",
10
10
  "license": "AGPL-3.0-or-later",
11
11
  "dependencies": {
12
12
  "express": "^4.21.2",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.19.305",
3
+ "version": "1.19.306",
4
4
  "description": "Cloud Bongos — the AI-first build platform core (GDS + platform surfaces + module system), installed as a versioned dependency (ADR 0108).",
5
5
  "license": "AGPL-3.0-or-later",
6
6
  "main": "src/platform-server.js",
@@ -0,0 +1,245 @@
1
+ #!/usr/bin/env node
2
+ // scripts/gds/doc-cli-guard.js — a doc that NAMES a CLI invocation must match the
3
+ // real CLI (task 1003165 / G01, goal 1000077; criterion guards-doc-names-a-command).
4
+ //
5
+ // WHY. Task 1002931 shipped a skill instructing `capture.js --full --fields-file`
6
+ // while those flags lived on an unlanded branch. Its test passed, because the test
7
+ // pinned the skill's PROSE — it asserted the file said the words, never that the
8
+ // words worked. So the skill confidently told agents to run a command that
9
+ // silently ignored both flags and filed the wrong thing. That is the whole class:
10
+ // AN UNRECOGNISED FLAG IS NOT AN ERROR. `arg('--x')`/`hasFlag('--x')` return null
11
+ // and the command proceeds, so a doc can name a flag that does nothing and every
12
+ // test stays green (learning 1000198). It happened TWICE in one session: the
13
+ // second time /workspace ran an older capture.js and the same documented flags
14
+ // were dropped on the floor again.
15
+ //
16
+ // WHAT IT CHECKS, deliberately narrow: for every `node scripts/gds/<x>.js` in a
17
+ // tracked .md, (a) the script exists, and (b) each `--flag` written next to it
18
+ // appears in that script's source. Nothing else — this is not a prose linter.
19
+ //
20
+ // STATIC ONLY, NEVER EXECUTED. The reason matters: running every documented CLI
21
+ // would be slow, would need a session, and a command that blocks on a prompt
22
+ // would hang CI forever — the exact failure R151's TTY gate exists to prevent.
23
+ //
24
+ // It lives in its OWN FILE rather than in fitness.js because fitness.js is already
25
+ // over the size ratchet (config/fitness-baselines.json) — the control-manifest.js
26
+ // precedent. fitness.js requires it in CHECKS.
27
+
28
+ 'use strict';
29
+
30
+ const fs = require('fs');
31
+ const path = require('path');
32
+ const { execFileSync } = require('node:child_process');
33
+
34
+ const REPO_ROOT = path.resolve(__dirname, '..', '..');
35
+
36
+ // ---------------------------------------------------------------------------
37
+ // Check 22 — a doc that NAMES a CLI invocation must match the real CLI
38
+ // (task 1003165 / G01, goal 1000077).
39
+ //
40
+ // WHY. Task 1002931 shipped a skill instructing `capture.js --full --fields-file`
41
+ // while those flags lived on an unlanded branch. Its test passed, because the test
42
+ // pinned the skill's PROSE — it asserted the file said the words, never that the
43
+ // words worked. So the skill confidently told agents to run a command that
44
+ // silently ignored both flags and filed the wrong thing. That is the whole class:
45
+ // AN UNRECOGNISED FLAG IS NOT AN ERROR. `arg('--x')`/`hasFlag('--x')` just return
46
+ // null and the command proceeds, so a doc can name a flag that does nothing and
47
+ // every test stays green (learning 1000198).
48
+ //
49
+ // It happened TWICE in one session: the second time, /workspace ran an older
50
+ // capture.js and the same documented flags were dropped on the floor again.
51
+ //
52
+ // WHAT IT CHECKS, deliberately narrow: for every `node scripts/gds/<x>.js` in a
53
+ // tracked .md, (a) the script exists, and (b) each `--flag` written next to it is
54
+ // parsed by that script. Nothing else — this is not a linter for prose.
55
+ //
56
+ // STATIC ONLY, NEVER EXECUTED. The task says so and the reason matters: running
57
+ // every documented CLI from a fitness check would be slow, would need a session,
58
+ // and a command that blocks on a prompt would hang CI forever — the exact failure
59
+ // R151's TTY gate exists to prevent.
60
+ // Key is `<doc path>::<token>`, where token is either the script path (the whole
61
+ // invocation is tolerated) or `--flag` (just that flag). Every entry carries its
62
+ // reason on the same line: an unexplained entry is a silent hole, and the point of
63
+ // this check is that a doc's claims are checkable.
64
+ const DOC_CLI_ALLOW = new Map([
65
+ ['.claude/skills/paint/SKILL.md::scripts/gds/art-stage.js',
66
+ 'art-stage.js ships with the art-pipeline HOST module, which publish-manifest.js excludes from core — absent from a core-only checkout by design; the skill says so inline.'],
67
+ ['.claude/skills/otb-tile-generate/SKILL.md::scripts/gds/art-stage.js',
68
+ 'same as paint/SKILL.md — the art staging CLI travels with the excluded art-pipeline module, not the portable core.'],
69
+ ]);
70
+
71
+ // `node scripts/gds/foo.js` … up to the end of the line/segment, so the flags that
72
+ // follow travel with the script that owns them.
73
+ const DOC_CLI_RE = /node\s+(scripts\/gds\/[A-Za-z0-9._-]+\.js)([^\n`]*)/g;
74
+ // A long flag only. Short flags and bare words are ignored: too many false
75
+ // positives from prose, and the failure this guards is long-flag drift.
76
+ const DOC_FLAG_RE = /(?<![\w-])--([A-Za-z][A-Za-z0-9-]*)/g;
77
+
78
+ // Flags every CLI inherits from shared arg plumbing rather than naming itself.
79
+ const UNIVERSAL_FLAGS = new Set(['quiet', 'help', 'dry-run', 'json']);
80
+
81
+ // Does `script` know about `--flag` AT ALL?
82
+ //
83
+ // This deliberately asks the WEAK question — does the literal appear anywhere in
84
+ // the source — and not the strong one ("is it wired into arg parsing"). The first
85
+ // draft of this check asked the strong question, matching `arg('--x')` and
86
+ // `hasFlag('--x')`, and produced FIVE false positives out of nine hits on its
87
+ // first real run: triage.js parses via `argv.indexOf('--development')`,
88
+ // provision.js via `hasFlag(args, '--apply')`, oauth-secret.js via
89
+ // `argVal(args, '--client-id')`, status.js via `a === '--widget'`, and
90
+ // validate-design.js via `ARGS.has('--strict')`. Every one of those is correct
91
+ // code that the strong check called broken.
92
+ //
93
+ // The set of ways to read an argv in JavaScript is open-ended, so a check that
94
+ // enumerates them will keep growing false positives forever — and a fitness check
95
+ // that cries wolf gets muted, which costs more than the drift it was catching.
96
+ // The weak question is ONE-DIRECTIONAL and therefore sound: a script whose source
97
+ // never contains the string `--full` cannot possibly honour `--full`, whatever
98
+ // idiom it uses. That is exactly the failure this exists for (learning 1000198:
99
+ // /workspace ran a capture.js from before those flags existed, and both were
100
+ // silently dropped). A stale usage comment naming a removed flag slips through —
101
+ // accepted, deliberately: guessing which mentions are "real" is how the strong
102
+ // version went wrong.
103
+ function scriptMentionsFlag(src, flag) {
104
+ const f = flag.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
105
+ // Trailing guard so `--client` does not match `--client-id`.
106
+ return new RegExp(`--${f}(?![\\w-])`).test(src);
107
+ }
108
+
109
+ // The scan CORE. IO is injected — `readDoc(path)` and `readScript(path)` both
110
+ // return source or null — so a test can exercise a DRIFTED doc without writing
111
+ // a broken file into the tree (the control-manifest.js `evaluate` precedent).
112
+ // A check that cannot be shown to fail is not evidence of anything, which is
113
+ // precisely how task 1002931's prose-pinning test let the bad skill through.
114
+ function scanDocs(docs, { readDoc, readScript, allow = DOC_CLI_ALLOW } = {}) {
115
+ const violations = [];
116
+ const warnings = [];
117
+ let invocations = 0;
118
+ const srcCache = new Map();
119
+ const script = (rel) => {
120
+ if (!srcCache.has(rel)) srcCache.set(rel, readScript(rel));
121
+ return srcCache.get(rel);
122
+ };
123
+
124
+ for (const doc of docs) {
125
+ const text = readDoc(doc);
126
+ if (text === null) continue;
127
+ DOC_CLI_RE.lastIndex = 0;
128
+ let m;
129
+ while ((m = DOC_CLI_RE.exec(text))) {
130
+ const scriptRel = m[1];
131
+ const tail = m[2] || '';
132
+ invocations += 1;
133
+ const src = script(scriptRel);
134
+ if (src === null) {
135
+ if (!allow.has(`${doc}::${scriptRel}`)) {
136
+ violations.push(
137
+ `${doc} → \`node ${scriptRel}\` names a script that does not exist. `
138
+ + 'An agent following this doc gets MODULE_NOT_FOUND. Fix the path, or allowlist it in DOC_CLI_ALLOW with a reason.'
139
+ );
140
+ }
141
+ continue;
142
+ }
143
+ // A whole-invocation allowlist covers its flags too — the script isn't here to check them against.
144
+ if (allow.has(`${doc}::${scriptRel}`)) continue;
145
+ DOC_FLAG_RE.lastIndex = 0;
146
+ let f;
147
+ while ((f = DOC_FLAG_RE.exec(tail))) {
148
+ const flag = f[1];
149
+ if (UNIVERSAL_FLAGS.has(flag)) continue;
150
+ if (allow.has(`${doc}::--${flag}`)) continue;
151
+ if (scriptMentionsFlag(src, flag)) {
152
+ // The criterion asks that the flag be PARSED, not merely present. The
153
+ // hard check can only prove presence (see scriptMentionsFlag), so the
154
+ // residual gap — a flag surviving in a usage comment after the code that
155
+ // read it was deleted — is reported as a WARNING, not a failure. Warning
156
+ // rather than failing is the deliberate choice: a usage banner is the
157
+ // ONLY place some scripts name a flag they genuinely handle via a helper,
158
+ // so failing here would resurrect the false positives that sank draft 1.
159
+ if (!scriptMentionsFlag(stripLineComments(src), flag)) {
160
+ warnings.push(
161
+ `${doc} → \`node ${scriptRel} --${flag}\`: ${scriptRel} names \`--${flag}\` only in comments/usage text, `
162
+ + 'never in code — it may be a flag whose implementation was deleted. Verify it is still handled.'
163
+ );
164
+ }
165
+ } else {
166
+ violations.push(
167
+ `${doc} → \`node ${scriptRel} --${flag}\`: ${scriptRel} never mentions \`--${flag}\` anywhere in its source, so it cannot honour it. `
168
+ + 'An unrecognised flag is silently IGNORED, not an error — so the documented command does the wrong thing while every test stays green. '
169
+ + 'Add the flag, fix the doc, or allowlist it in DOC_CLI_ALLOW with a reason.'
170
+ );
171
+ }
172
+ }
173
+ }
174
+ }
175
+ return { violations, warnings, invocations };
176
+ }
177
+
178
+ // Drop `//` line comments and `/* */` blocks — enough to tell "named in code" from
179
+ // "named only in a usage banner". Not a JS parser, and it does not need to be: a
180
+ // false split here downgrades a warning, never a failure.
181
+ function stripLineComments(src) {
182
+ return String(src)
183
+ .replace(/\/\*[\s\S]*?\*\//g, '')
184
+ .split('\n')
185
+ .map((l) => l.replace(/(^|\s)\/\/.*$/, '$1'))
186
+ .join('\n');
187
+ }
188
+
189
+ // Tracked markdown, via an argv ARRAY and never a shell string: `git ls-files
190
+ // '*.md'` as a string goes through cmd.exe on Windows, where the quotes are
191
+ // literal, git matches nothing, and the scan silently reports on zero files
192
+ // (task 1002526).
193
+ function trackedMarkdown() {
194
+ const out = execFileSync('git', ['ls-files', '*.md'], {
195
+ cwd: REPO_ROOT, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'],
196
+ });
197
+ return String(out || '').split('\n').map((f) => f.trim()).filter(Boolean);
198
+ }
199
+
200
+ const readFileOrNull = (rel) => {
201
+ try { return fs.readFileSync(path.join(REPO_ROOT, rel), 'utf8'); } catch { return null; }
202
+ };
203
+
204
+ const NAME = 'a doc that names a CLI matches the real CLI (G01)';
205
+
206
+ function checkDocNamesRealCli({ files = null } = {}) {
207
+ const docs = files || trackedMarkdown();
208
+ // A lint that reports on nothing is worse than one that fails (the
209
+ // docs-entropy precedent) — this repo tracks hundreds of .md files, so an
210
+ // empty enumeration is a broken scan, not a clean result.
211
+ if (!docs.length) {
212
+ return {
213
+ name: NAME, ok: false, hardFail: true,
214
+ violations: ['scan defect — enumerated 0 tracked markdown files; a broken enumeration, not a clean result.'],
215
+ warnings: [], note: 'static scan of documented `node scripts/gds/*.js` invocations.',
216
+ };
217
+ }
218
+
219
+ const { violations, warnings, invocations } = scanDocs(docs, {
220
+ readDoc: readFileOrNull, readScript: readFileOrNull,
221
+ });
222
+
223
+ return {
224
+ name: NAME,
225
+ ok: violations.length === 0,
226
+ hardFail: violations.length > 0,
227
+ violations,
228
+ warnings,
229
+ note: `${invocations} documented \`node scripts/gds/*.js\` invocation(s) across ${docs.length} tracked .md file(s): the script exists and every long flag it is written with appears in that script's source. Static — never executes a CLI (a documented command that blocks on a prompt would hang CI). ${DOC_CLI_ALLOW.size} allowlisted.`,
230
+ };
231
+ }
232
+
233
+ function main() {
234
+ const r = checkDocNamesRealCli();
235
+ for (const v of r.violations) console.error(`\u2717 ${v}`);
236
+ console.log(`${r.hardFail ? 'FAIL' : 'PASS'} ${r.note}`);
237
+ process.exit(r.hardFail ? 1 : 0);
238
+ }
239
+
240
+ if (require.main === module) main();
241
+
242
+ module.exports = {
243
+ checkDocNamesRealCli, scanDocs, trackedMarkdown, scriptMentionsFlag, stripLineComments,
244
+ DOC_CLI_ALLOW, DOC_CLI_RE, DOC_FLAG_RE, UNIVERSAL_FLAGS,
245
+ };
@@ -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
 
package/src/module-api.js CHANGED
@@ -1037,8 +1037,10 @@ 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)
1040
1042
  // ---------------------------------------------------------------------------
1041
- const CORE_VERSION = '1.19.305'; // CI auto-patch carrier (ADR 0161); see changelog above
1043
+ const CORE_VERSION = '1.19.306'; // CI auto-patch carrier (ADR 0161); see changelog above
1042
1044
 
1043
1045
  // A namespaced logger so a module's log lines are attributable + consistent.
1044
1046
  // 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
+ });