@bongos/core 1.19.304 → 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.304",
6
- "core_contract": "1.19.304",
7
- "source_commit": "8c22da0cc4ab95a1684933ef98112abb201bc850",
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:13:32.623Z",
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": 1413,
14
+ "functional_verbatim": 1416,
15
15
  "rules": 3,
16
16
  "gate_literals": 3,
17
17
  "gate": "passed"
18
18
  },
19
- "file_count": 1757,
20
- "tree_sha256": "644acfeea1c7e902934e366c56379f0191e6de8e08e6338567155722b916b2fd",
19
+ "file_count": 1760,
20
+ "tree_sha256": "a2c3f65e69e891c0076d0b10268d3549cfddf8d76d1f320274e86fd833d56013",
21
21
  "files": [
22
22
  {
23
23
  "path": ".claude/skills/blocker-review/SKILL.md",
@@ -177,7 +177,7 @@
177
177
  {
178
178
  "path": ".claude/skills/ideate/SKILL.md",
179
179
  "mode": "0000644",
180
- "sha256": "8c89cdb688f39e9ca9009cfb0345f8f75fb12ff91de31914d410c5b7f78d18c2"
180
+ "sha256": "7e2c13e70e3ac208c4754f32685c2453c501f35b3742310ee49a5855bc0c399b"
181
181
  },
182
182
  {
183
183
  "path": ".claude/skills/merge-mode/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": "3f027a59178132ea747301cbcc78f7404c0f0ec820c6651affcf8854b1cf70f3"
4580
+ "sha256": "ab53e6d02f437a24895dd3d04b3e8ef39ddf4257b8c4b2e784a84dd10820adc4"
4581
4581
  },
4582
4582
  {
4583
4583
  "path": "package.json",
4584
4584
  "mode": "0000644",
4585
- "sha256": "79d94ba55244fa52d3f8f35717803666064daecb4d91d5547060baaa2143692d"
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": "b8db1ca313e0156a5891a779be650f8c56069015529e064a18b8e4970e7e312f"
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",
@@ -7564,6 +7574,11 @@
7564
7574
  "mode": "0000644",
7565
7575
  "sha256": "1e6d0784ecb37e613561cc8ee45fa23e178ecf112d5a8f218b9795dca0e7d0fc"
7566
7576
  },
7577
+ {
7578
+ "path": "tests/idea_ideate_full.mjs",
7579
+ "mode": "0000644",
7580
+ "sha256": "5000ed6a7b1a24eced0f070e556af354c8fa9c7937f43c7b932a3ca29f8a5f35"
7581
+ },
7567
7582
  {
7568
7583
  "path": "tests/idea_quick_guard.mjs",
7569
7584
  "mode": "0000644",
@@ -72,6 +72,76 @@ The body should carry the development, not just a sentence. Use these line-tag c
72
72
  - The developed block when you have it: `surfaces:`, `risks:`, `open questions:`.
73
73
  - In autonomous mode only: `origin: autonomous`.
74
74
 
75
+ ## Authoring a FULL IDEA in conversation (task 1002931 / BV1.R155)
76
+
77
+ A **Full Idea** is the ideator craft's real output: five answered questions that
78
+ are a baseline scope of work someone else can deliver against *without
79
+ re-interviewing the person who had the thought*. Drawing those five answers out
80
+ of a conversation is exactly this skill's dynamic — and it is the path for
81
+ someone who thinks by talking rather than by filling in a form.
82
+
83
+ **Get the questions from the template, never from memory.** They are declared
84
+ once, as data, and a project can define its own:
85
+
86
+ ```
87
+ node scripts/gds/spark.js --template
88
+ ```
89
+
90
+ That prints each field, its label, and the substance floor the scorer expects.
91
+ Ask them **as questions, in that order** — one at a time, in the ideator's own
92
+ thread of thought, not as a form read aloud.
93
+
94
+ ### The rule that governs this whole section: you write down what they said
95
+
96
+ You are already "the instrument, not the author". Here that stops being a stance
97
+ and becomes a hard constraint, because **the completeness score is deterministic
98
+ and it feeds credits** (R142 scores substance; ADR 0172 pays on it). If you
99
+ ghost-write a field, the number is fraudulent and the builder is paid for your
100
+ words. So:
101
+
102
+ - **Never invent field content.** Not a sentence, not a clause.
103
+ - **Never pad a thin answer** to clear the floor. A short answer scores short —
104
+ that is the scorer working, not a problem to fix.
105
+ - **Never polish an answer into words they did not use.** Tightening *their*
106
+ phrasing when they ask is help; rewriting it is authorship.
107
+ - **An unanswered question stays empty.** If they do not want to answer "why does
108
+ it matter", file it without that field and let the score say so. A Full Idea
109
+ with a gap is honest; a Full Idea you completed for them is not.
110
+ - What you *should* do is what this skill is for: **press on a thin answer.**
111
+ "That is one line — what is behind it?" is the sounding board doing its job.
112
+ Their next sentence is theirs. Yours would not be.
113
+
114
+ Read the answers back before filing and let them correct you. They bless it; you
115
+ file it.
116
+
117
+ ### Filing it
118
+
119
+ Write their answers to a JSON file keyed by the template's field keys, then:
120
+
121
+ ```
122
+ node scripts/gds/capture.js "<their title>" --full --fields-file <path>
123
+ ```
124
+
125
+ That is the **same route** the hall form uses (`POST /inbox` with `grade='full'`),
126
+ so the record is identical — same validation against the R141 template, same R142
127
+ score, same ideator-credit lane. There is deliberately no separate write path for
128
+ conversational authoring: a second one would drift, and "indistinguishable in the
129
+ database" is this task's done-when.
130
+
131
+ The command reports what the server **stored**. If it says the idea landed as
132
+ *quick*, say so plainly — capture is fail-open on grading, and telling someone
133
+ they filed a Full Idea when they did not is the one outcome worse than the
134
+ failure itself.
135
+
136
+ ### Developing someone else's spark
137
+
138
+ The same conversation works on a **Big Idea Spark** — a quick idea whose author
139
+ marked it as needing development. `node scripts/gds/spark.js --list` shows what is
140
+ waiting; develop one with `spark.js --develop <id> --fields-file <path>`. The
141
+ author keeps the credit for having had the thought and the payout splits with
142
+ them (ADR 0185), so the never-invent rule applies with more force, not less:
143
+ those five answers are now going out under two people's names.
144
+
75
145
  ## Trust boundary — filing is open, triage is not
76
146
 
77
147
  Filing into the inbox is open to **any** authenticated builder (Xenos included) — that path stays open here. **Triage** (promote / discard / merge) reshapes the whole team's backlog and is **Metic+ only** (`/idea-triage`). So in this skill you *file*; you never verdict. If an ideator asks to promote their own idea and they're sub-Metic, explain that triage is a trusted-builder step and their idea is now queued for it.
@@ -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.304",
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.304",
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.304",
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
@@ -1035,8 +1035,12 @@ const { validateOrRespond, LIMITS, parseId, asyncHandler, corsPublicGet, parsePa
1035
1035
  // landed since 1.19.302 with no explicit bump. run 32605477232. (task 1002620)
1036
1036
  // 1.19.304 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
1037
1037
  // landed since 1.19.303 with no explicit bump. run 32660527575. (task 1002620)
1038
+ // 1.19.305 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
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)
1038
1042
  // ---------------------------------------------------------------------------
1039
- const CORE_VERSION = '1.19.304'; // 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
1040
1044
 
1041
1045
  // A namespaced logger so a module's log lines are attributable + consistent.
1042
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
+ });
@@ -0,0 +1,170 @@
1
+ // tests/idea_ideate_full.mjs
2
+ //
3
+ // task 1002931 (BV1.R155, goal 1000062) — /ideate authors a Full Idea
4
+ // conversationally.
5
+ //
6
+ // A skill is a prompt, so the thing to pin is the PROMPT'S CONTRACT. Two
7
+ // done-when clauses, and both are checkable:
8
+ //
9
+ // 1. "indistinguishable in the database from one authored in the hall" — so the
10
+ // skill must file through the SAME route the hall uses, and must not describe
11
+ // a bespoke write path of its own. A second path is how two surfaces start
12
+ // producing subtly different rows.
13
+ // 2. "never invents field content on the builder's behalf" — the sharp one. The
14
+ // completeness score is deterministic and feeds credits (ADR 0172), so a
15
+ // ghost-written field is a fraudulent number and the builder is paid for the
16
+ // model's words. The rule must be present WITH that reason: an instruction
17
+ // whose justification is missing is the first one dropped under pressure.
18
+ //
19
+ // Plus the rule this goal applies to every surface: the five questions live in the
20
+ // R141 declaration, so no consumer restates them.
21
+ //
22
+ // Run: node tests/idea_ideate_full.mjs
23
+
24
+ import { strict as assert } from 'node:assert';
25
+ import { createRequire } from 'node:module';
26
+ import { spawnSync } from 'node:child_process';
27
+ import fs from 'node:fs';
28
+ import path from 'node:path';
29
+ import { fileURLToPath } from 'node:url';
30
+
31
+ const require = createRequire(import.meta.url);
32
+ const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
33
+ const SKILL = fs.readFileSync(path.join(ROOT, '.claude/skills/ideate/SKILL.md'), 'utf8');
34
+ // Prose assertions run against a WHITESPACE-NORMALISED copy. Markdown is hard-
35
+ // wrapped, so any phrase long enough to be worth asserting will eventually
36
+ // straddle a newline — and a regex that does not allow for that fails on a
37
+ // reflow that changed nothing. Structural assertions (code fences, list markers)
38
+ // still use SKILL itself.
39
+ const NORM = SKILL.replace(/\s+/g, ' ');
40
+ const { BASE_TEMPLATE } = require('../modules/ideas/templates.js');
41
+
42
+ let passed = 0;
43
+ let failed = 0;
44
+ function test(name, fn) {
45
+ try { fn(); passed++; console.log(` ok ${name}`); }
46
+ catch (err) { failed++; console.error(` FAIL ${name}\n ${err.message}`); }
47
+ }
48
+
49
+ // --- (1) the same record as the hall ------------------------------------
50
+
51
+ // THE COMMAND THE SKILL NAMES MUST ACTUALLY EXIST — and this assertion is here
52
+ // because a grader caught its absence. A test that pins prompt text and stops
53
+ // there will happily pass while the skill documents a command that does nothing:
54
+ // R155's branch was cut before R151 added these flags, so for a while the skill
55
+ // said `capture.js --full` to a capture.js that had never heard of it. Prompt
56
+ // tests must reach the executable, or they only prove the prose is self-consistent.
57
+ test('capture.js really supports the flags the skill tells you to use', () => {
58
+ const capture = path.join(ROOT, 'scripts/gds/capture.js');
59
+ const usage = spawnSync(process.execPath, [capture], { encoding: 'utf8', timeout: 20000 });
60
+ assert.equal(usage.status, 2, 'the no-args usage path');
61
+ assert.match(usage.stderr, /--full/, 'usage advertises --full');
62
+ assert.match(usage.stderr, /--fields-file/, 'usage advertises --fields-file');
63
+
64
+ // Stronger than the usage text: --full must be UNDERSTOOD, not merely listed.
65
+ // An unrecognised flag would be silently ignored and file a quick idea; the
66
+ // R151 guard proves the flag is wired to real behaviour.
67
+ const guard = spawnSync(process.execPath, [capture, 'a title', '--full'], { encoding: 'utf8', timeout: 20000 });
68
+ assert.equal(guard.status, 2);
69
+ assert.match(guard.stderr, /--full needs --fields-file/, '--full is parsed and validated');
70
+
71
+ // And it can actually send a grade — the thing that makes the record match the
72
+ // hall's. Asserted over the payload assignment, not the file text.
73
+ const src = fs.readFileSync(capture, 'utf8');
74
+ const grades = [...src.matchAll(/payload\.grade\s*=\s*'([a-z]+)'/g)].map((m) => m[1]);
75
+ assert.deepEqual(grades, ['full'], 'capture.js sends grade=full');
76
+ });
77
+
78
+ test('it files through capture.js --full — the route the hall form uses', () => {
79
+ assert.match(SKILL, /capture\.js "<their title>" --full --fields-file/);
80
+ // Named explicitly so the next reader knows WHY it is that command and not a
81
+ // bespoke one.
82
+ assert.match(NORM, /same.{0,10}route.{0,40}hall form uses/i);
83
+ });
84
+
85
+ test('it describes NO second write path', () => {
86
+ // The failure this guards: a skill that tells an agent to curl the API by hand
87
+ // ends up sending a different payload than the form does, and the two records
88
+ // drift while both claim to be Full Ideas.
89
+ assert.ok(!/curl/i.test(SKILL), 'no hand-rolled HTTP');
90
+ assert.ok(!/api\.js POST \/api\/gds\/inbox/.test(SKILL), 'no raw POST for filing an idea');
91
+ const grade = [...SKILL.matchAll(/grade\s*=\s*'([a-z]+)'/g)].map((m) => m[1]);
92
+ assert.ok(grade.every((g) => g === 'full'), `only the full grade is named: ${JSON.stringify(grade)}`);
93
+ });
94
+
95
+ test('it says the report comes from the SERVER, not from the request', () => {
96
+ // Capture is fail-open on grading (R143): an idea can land as quick.
97
+ assert.match(NORM, /reports what the server \*\*stored\*\*/);
98
+ assert.match(NORM, /landed as.{0,20}quick/);
99
+ });
100
+
101
+ // --- (2) it never invents field content ---------------------------------
102
+
103
+ test('the never-invent rule is stated, in more than one form', () => {
104
+ assert.match(NORM, /Never invent field content/);
105
+ assert.match(NORM, /Never pad a thin answer/);
106
+ assert.match(NORM, /Never polish an answer into words they did not use/);
107
+ });
108
+
109
+ test('and it carries the REASON — the score feeds credits', () => {
110
+ // An instruction without its justification is the first one dropped when the
111
+ // model is trying to be helpful. This is the sentence that makes ghost-writing
112
+ // legible as fraud rather than as service.
113
+ assert.match(NORM, /deterministic/);
114
+ assert.match(NORM, /feeds credits/);
115
+ assert.match(NORM, /fraudulent/);
116
+ assert.match(NORM, /paid for your words/i);
117
+ });
118
+
119
+ test('an unanswered question is allowed to stay empty', () => {
120
+ // The tempting failure is completing the set "to be helpful", which is exactly
121
+ // the ghost-writing the rule forbids.
122
+ assert.match(NORM, /stays empty/);
123
+ assert.match(NORM, /honest/);
124
+ });
125
+
126
+ test('it names the thing to do INSTEAD — press on a thin answer', () => {
127
+ // A rule that only forbids leaves the model with nothing to do; this is the
128
+ // sounding-board behaviour the whole skill exists for.
129
+ assert.match(NORM, /press on a thin answer/i);
130
+ assert.match(NORM, /Their next sentence is theirs/);
131
+ });
132
+
133
+ test('the builder blesses it; the skill files it', () => {
134
+ assert.match(NORM, /They bless it; you file it\./);
135
+ });
136
+
137
+ // --- the five questions are not restated -------------------------------
138
+
139
+ test('the skill names NO field key and NO field label', () => {
140
+ // Same rule the hall composer obeys (R152): the questions are declared once, in
141
+ // the R141 template, and a copy here would silently ignore a per-project one.
142
+ for (const f of BASE_TEMPLATE.fields) {
143
+ assert.ok(!SKILL.includes(f.key), `restates the field key "${f.key}"`);
144
+ assert.ok(!SKILL.includes(f.label), `restates the label "${f.label}"`);
145
+ }
146
+ });
147
+
148
+ test('it points at the command that PRINTS the questions', () => {
149
+ assert.match(SKILL, /spark\.js --template/);
150
+ assert.match(NORM, /never from memory/i);
151
+ });
152
+
153
+ // --- the sibling path ---------------------------------------------------
154
+
155
+ test('it can also develop someone else\'s spark, with the rule reinforced', () => {
156
+ assert.match(SKILL, /spark\.js --list/);
157
+ assert.match(SKILL, /spark\.js --develop/);
158
+ // The never-invent rule matters MORE there: the answers go out under two names.
159
+ assert.match(NORM, /two people's names/);
160
+ });
161
+
162
+ test('the section does not contradict the skill\'s founding rule', () => {
163
+ // "you are the instrument, not the author" is the whole premise; the Full Idea
164
+ // section has to be an application of it, not an exception to it.
165
+ assert.match(NORM, /you are the instrument, not the author/);
166
+ assert.match(NORM, /stops being a stance and becomes a hard constraint/);
167
+ });
168
+
169
+ console.log(`\nidea_ideate_full: ${passed} passed, ${failed} failed`);
170
+ if (failed > 0) process.exit(1);