@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 +29 -14
- package/.claude/skills/ideate/SKILL.md +70 -0
- package/.claude/skills/otb-tile-generate/SKILL.md +1 -1
- package/.claude/skills/paint/SKILL.md +1 -1
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/scripts/gds/doc-cli-guard.js +245 -0
- package/scripts/gds/fitness.js +1 -1
- package/src/module-api.js +5 -1
- package/tests/doc_cli_guard.mjs +195 -0
- package/tests/idea_ideate_full.mjs +170 -0
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.
|
|
6
|
-
"core_contract": "1.19.
|
|
7
|
-
"source_commit": "
|
|
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:
|
|
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":
|
|
14
|
+
"functional_verbatim": 1416,
|
|
15
15
|
"rules": 3,
|
|
16
16
|
"gate_literals": 3,
|
|
17
17
|
"gate": "passed"
|
|
18
18
|
},
|
|
19
|
-
"file_count":
|
|
20
|
-
"tree_sha256": "
|
|
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": "
|
|
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": "
|
|
215
|
+
"sha256": "53bfcf6c531c0aeefc1696b470ba6a746f4490501ab092766f42c501f06d453d"
|
|
216
216
|
},
|
|
217
217
|
{
|
|
218
218
|
"path": ".claude/skills/paint/SKILL.md",
|
|
219
219
|
"mode": "0000644",
|
|
220
|
-
"sha256": "
|
|
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": "
|
|
4580
|
+
"sha256": "ab53e6d02f437a24895dd3d04b3e8ef39ddf4257b8c4b2e784a84dd10820adc4"
|
|
4581
4581
|
},
|
|
4582
4582
|
{
|
|
4583
4583
|
"path": "package.json",
|
|
4584
4584
|
"mode": "0000644",
|
|
4585
|
-
"sha256": "
|
|
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": "
|
|
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": "
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
+
};
|
package/scripts/gds/fitness.js
CHANGED
|
@@ -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.
|
|
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);
|