@bongos/core 1.19.303 → 1.19.305
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 +23 -13
- package/.claude/skills/ideate/SKILL.md +70 -0
- package/docs/file-map.md +1 -1
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/scripts/gds/capture.js +147 -5
- package/src/module-api.js +5 -1
- package/tests/idea_capture_grades.mjs +159 -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.305",
|
|
6
|
+
"core_contract": "1.19.305",
|
|
7
|
+
"source_commit": "6dc5a84be60b26f11d572eeccf10b8092c7352ae",
|
|
8
8
|
"source_ref": "HEAD",
|
|
9
|
-
"built_at": "2026-08-
|
|
9
|
+
"built_at": "2026-08-23T19:20:49.275Z",
|
|
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": 1414,
|
|
15
15
|
"rules": 3,
|
|
16
16
|
"gate_literals": 3,
|
|
17
17
|
"gate": "passed"
|
|
18
18
|
},
|
|
19
|
-
"file_count":
|
|
20
|
-
"tree_sha256": "
|
|
19
|
+
"file_count": 1758,
|
|
20
|
+
"tree_sha256": "3a7cd2627a967fe3dbe7edf3dc916b4d728170b826669b64ae457d5e92ac10d1",
|
|
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",
|
|
@@ -1557,7 +1557,7 @@
|
|
|
1557
1557
|
{
|
|
1558
1558
|
"path": "docs/file-map.md",
|
|
1559
1559
|
"mode": "0000644",
|
|
1560
|
-
"sha256": "
|
|
1560
|
+
"sha256": "ffd052f893ea0ef47a65e66a6d260fd43c1ccc653fa277f11f13805099e73d45"
|
|
1561
1561
|
},
|
|
1562
1562
|
{
|
|
1563
1563
|
"path": "docs/handoff-template.md",
|
|
@@ -4577,12 +4577,12 @@
|
|
|
4577
4577
|
{
|
|
4578
4578
|
"path": "package-lock.json",
|
|
4579
4579
|
"mode": "0000644",
|
|
4580
|
-
"sha256": "
|
|
4580
|
+
"sha256": "4678a5bfd751ffe8ac5c3283eb25af2e011ccb14d32032bf373c7c77bcb61a3d"
|
|
4581
4581
|
},
|
|
4582
4582
|
{
|
|
4583
4583
|
"path": "package.json",
|
|
4584
4584
|
"mode": "0000644",
|
|
4585
|
-
"sha256": "
|
|
4585
|
+
"sha256": "5ff86f97c1df0bf8d224512adc339aac2859db0a65e40e7783032b0128f7cbbd"
|
|
4586
4586
|
},
|
|
4587
4587
|
{
|
|
4588
4588
|
"path": "public-docs/index.html",
|
|
@@ -5037,7 +5037,7 @@
|
|
|
5037
5037
|
{
|
|
5038
5038
|
"path": "scripts/gds/capture.js",
|
|
5039
5039
|
"mode": "0000644",
|
|
5040
|
-
"sha256": "
|
|
5040
|
+
"sha256": "950a65cf4766f795622c6780dcfa9a1b3f9aefe38f406ec6759b129028958aa9"
|
|
5041
5041
|
},
|
|
5042
5042
|
{
|
|
5043
5043
|
"path": "scripts/gds/cf-tunnel.js",
|
|
@@ -6237,7 +6237,7 @@
|
|
|
6237
6237
|
{
|
|
6238
6238
|
"path": "src/module-api.js",
|
|
6239
6239
|
"mode": "0000644",
|
|
6240
|
-
"sha256": "
|
|
6240
|
+
"sha256": "a59a54dc09613fc6d95a2efa1262b6d8099fabfb1ef90289462b81accd9c7fea"
|
|
6241
6241
|
},
|
|
6242
6242
|
{
|
|
6243
6243
|
"path": "src/module-loader/catalog.js",
|
|
@@ -7519,6 +7519,11 @@
|
|
|
7519
7519
|
"mode": "0000644",
|
|
7520
7520
|
"sha256": "ea992d1ab2ddedc27aaa5348a397356bef91a4c1ad9b295eb1a0ed1c4fc707e9"
|
|
7521
7521
|
},
|
|
7522
|
+
{
|
|
7523
|
+
"path": "tests/idea_capture_grades.mjs",
|
|
7524
|
+
"mode": "0000644",
|
|
7525
|
+
"sha256": "fb129f6ee327d946ba2e7ee46bab3b9795621f7f405637e1e8ed2b29807a84cf"
|
|
7526
|
+
},
|
|
7522
7527
|
{
|
|
7523
7528
|
"path": "tests/idea_completeness.mjs",
|
|
7524
7529
|
"mode": "0000644",
|
|
@@ -7559,6 +7564,11 @@
|
|
|
7559
7564
|
"mode": "0000644",
|
|
7560
7565
|
"sha256": "1e6d0784ecb37e613561cc8ee45fa23e178ecf112d5a8f218b9795dca0e7d0fc"
|
|
7561
7566
|
},
|
|
7567
|
+
{
|
|
7568
|
+
"path": "tests/idea_ideate_full.mjs",
|
|
7569
|
+
"mode": "0000644",
|
|
7570
|
+
"sha256": "5000ed6a7b1a24eced0f070e556af354c8fa9c7937f43c7b932a3ca29f8a5f35"
|
|
7571
|
+
},
|
|
7562
7572
|
{
|
|
7563
7573
|
"path": "tests/idea_quick_guard.mjs",
|
|
7564
7574
|
"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.
|
package/docs/file-map.md
CHANGED
|
@@ -174,7 +174,7 @@
|
|
|
174
174
|
│ ├── box.js ← (#598/ADR 0031) per-builder dev box lifecycle CLI: provision·park·wake·deprovision·sweep-idle·sweep-dormant·status (dry-run default, --apply to act; rank-gated)
|
|
175
175
|
│ ├── smoke-box.sh ← (#598) hermetic box-lifecycle smoke (no DB/DO/network): runs tests/boxes.mjs + tests/cf_tunnel.mjs (#771 named tunnel) + route-rank + module-load
|
|
176
176
|
│ ├── recall.js ← `/recall` CLI: two-call progressive-disclosure search (summary → `--expand <id>`) over Bongos retrieval layer (ADR 0060); `node scripts/gds/recall.js <query>`
|
|
177
|
-
│ ├── capture.js ← file an idea_inbox row from the CLI (the ideas.capture kernel port); backs the "capture a tangent" flow across skills
|
|
177
|
+
│ ├── capture.js ← file an idea_inbox row from the CLI (the ideas.capture kernel port); backs the "capture a tangent" flow across skills. Task 1002927 / R151 taught it the GRADES: `--full --fields-file f.json` files a Full Idea, `--spark` is capture-then-mark (two requests — the route refuses a pre-marked capture), and after a plain quick capture it OFFERS to develop further. That offer is TTY-gated (`--no-prompt` too): capture.js is run by agents with no terminal and reads its body from stdin, so an unguarded prompt would hang every non-interactive caller
|
|
178
178
|
│ ├── spark.js ← (tasks 1002920 / R144 + 1002921 / R145 + 1002922 / R146) the whole spark handoff from the terminal: mark one of your OWN quick ideas as a Big Idea Spark (`--undo` withdraws it), `--list` is the SPARK QUEUE, `--template` prints the five Full Idea fields, `--develop <id> --fields-file <json>` is the PICKUP (any builder; author + developer both recorded). Author-only server-side for the mark (it is the consent); its own command because sparking acts on an EXISTING idea, which is why capture.js refuses grade='spark'
|
|
179
179
|
│ ├── worktree.js ← (task 1002809) junction-safe worktree add/remove/list — outside-repo worktrees get a node_modules junction via node (no shell quoting), removed BEFORE `git worktree remove`
|
|
180
180
|
│ ├── learning-capture.js ← file a `learnings` row (DB-backed short-form rule capture, replacing hand-edited recipe files for a single gotcha)
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.19.
|
|
3
|
+
"version": "1.19.305",
|
|
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.305",
|
|
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.305",
|
|
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",
|
package/scripts/gds/capture.js
CHANGED
|
@@ -7,7 +7,30 @@
|
|
|
7
7
|
// [--kind feature] \
|
|
8
8
|
// [--body-file path] \
|
|
9
9
|
// [--version V1|PMS-V2|GDS-V3] \
|
|
10
|
-
// [--priority N]
|
|
10
|
+
// [--priority N] \
|
|
11
|
+
// [--spark] \
|
|
12
|
+
// [--full --fields-file path.json] \
|
|
13
|
+
// [--no-prompt]
|
|
14
|
+
//
|
|
15
|
+
// THE GRADE AXIS (task 1002927 / BV1.R151, goal 1000062). Three grades exist and
|
|
16
|
+
// this CLI reaches all three, but NOT symmetrically, because they are not
|
|
17
|
+
// symmetrical acts:
|
|
18
|
+
// quick — the default. Title + body. Unchanged.
|
|
19
|
+
// full — `--full --fields-file f.json`, the five R141 fields. One request.
|
|
20
|
+
// spark — `--spark`, which is TWO requests: capture, then mark. The route
|
|
21
|
+
// deliberately REFUSES grade='spark' at capture (R144: an idea does not
|
|
22
|
+
// arrive pre-marked; sparking is the author's later act on an existing
|
|
23
|
+
// idea), and that invariant is not being worked around here — the
|
|
24
|
+
// author is simply performing both acts in one command, and the mark
|
|
25
|
+
// still goes through the author-only route that checks captured_by.
|
|
26
|
+
//
|
|
27
|
+
// THE POST-CAPTURE OFFER, and the one thing that must not break: after a plain
|
|
28
|
+
// quick capture, an interactive terminal is asked whether to develop the thought
|
|
29
|
+
// into a Full Idea. It is TTY-GATED. capture.js is run by agents and scripts with
|
|
30
|
+
// no terminal (and reads its BODY from stdin when --body-file is absent), so an
|
|
31
|
+
// unguarded prompt would hang every non-interactive caller. No TTY, or
|
|
32
|
+
// --no-prompt, means the offer never appears and the behaviour is exactly what it
|
|
33
|
+
// was before this task.
|
|
11
34
|
//
|
|
12
35
|
// Defaults:
|
|
13
36
|
// --kind → server auto-classifies from title+body (keyword heuristics
|
|
@@ -27,7 +50,8 @@
|
|
|
27
50
|
// Inbox now: 8 open
|
|
28
51
|
|
|
29
52
|
const fs = require('node:fs/promises');
|
|
30
|
-
const
|
|
53
|
+
const readline = require('node:readline');
|
|
54
|
+
const { cliClient, requireSession, arg, hasFlag } = require('./cli-lib');
|
|
31
55
|
|
|
32
56
|
|
|
33
57
|
async function resolveDefaultVersion() {
|
|
@@ -59,12 +83,49 @@ function formatCaptureFailure(status, data) {
|
|
|
59
83
|
return `capture failed (${status}): ${rendered}`;
|
|
60
84
|
}
|
|
61
85
|
|
|
86
|
+
// DEFAULT IS NO, and only an explicit yes counts. Extracted so the done-when
|
|
87
|
+
// ("declinable in one keystroke") is a testable predicate rather than a regex
|
|
88
|
+
// buried in a readline callback: a bare Enter, a stray space, an EOF (null), or
|
|
89
|
+
// anything that is not y/yes all decline.
|
|
90
|
+
function acceptsOffer(answer) {
|
|
91
|
+
return /^y(es)?$/i.test(String(answer == null ? '' : answer).trim());
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// The offer. DEFAULT IS NO: a bare Enter declines and leaves a plain quick idea,
|
|
95
|
+
// which is the done-when. It prints the command rather than launching an
|
|
96
|
+
// interactive five-field interview — the fields are prose, the terminal is a bad
|
|
97
|
+
// place to write five paragraphs, and the two surfaces built for it (the hall
|
|
98
|
+
// form and /ideate) are better at it than a readline loop would ever be. So the
|
|
99
|
+
// offer's job is to make the NEXT step obvious, not to become that step.
|
|
100
|
+
async function offerToDevelop(ideaId) {
|
|
101
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
102
|
+
try {
|
|
103
|
+
const answer = await new Promise((resolve) => {
|
|
104
|
+
rl.question('\nDevelop this into a Full Idea? It gets a completeness score and can earn on the ideator lane. [y/N] ', resolve);
|
|
105
|
+
});
|
|
106
|
+
if (!acceptsOffer(answer)) {
|
|
107
|
+
// Declined. Nothing further happens — the quick idea stands exactly as filed.
|
|
108
|
+
console.log('Left as a quick idea.');
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
console.log('\nTwo ways to write it:');
|
|
112
|
+
console.log(` in the hall: /builders/ideas (a live completeness meter as you type)`);
|
|
113
|
+
console.log(` in the terminal: node scripts/gds/spark.js --template (then --fields-file)`);
|
|
114
|
+
console.log(`\nOr offer it up for someone else to develop:`);
|
|
115
|
+
console.log(` node scripts/gds/spark.js ${ideaId}`);
|
|
116
|
+
} finally {
|
|
117
|
+
rl.close();
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
62
121
|
async function main() {
|
|
63
|
-
await requireSession();
|
|
64
122
|
const args = process.argv.slice(2);
|
|
65
123
|
const title = args[0];
|
|
66
124
|
if (!title || title.startsWith('--')) {
|
|
67
125
|
console.error('usage: node scripts/gds/capture.js "title" [--kind feature] [--body-file path] [--version V1|PMS-V2] [--priority N]');
|
|
126
|
+
console.error(' ... [--spark] offer it up for someone else to develop');
|
|
127
|
+
console.error(' ... [--full --fields-file f.json] file a Full Idea (five fields; see spark.js --template)');
|
|
128
|
+
console.error(' ... [--no-prompt] never ask anything (default when not a terminal)');
|
|
68
129
|
process.exit(2);
|
|
69
130
|
}
|
|
70
131
|
|
|
@@ -72,6 +133,37 @@ async function main() {
|
|
|
72
133
|
const bodyFile = arg('--body-file', args);
|
|
73
134
|
const versionFromArg = arg('--version', args);
|
|
74
135
|
const priorityStr = arg('--priority', args);
|
|
136
|
+
const wantSpark = hasFlag('--spark', args);
|
|
137
|
+
const wantFull = hasFlag('--full', args);
|
|
138
|
+
const fieldsFile = arg('--fields-file', args);
|
|
139
|
+
const noPrompt = hasFlag('--no-prompt', args);
|
|
140
|
+
|
|
141
|
+
// Refused rather than resolved: --spark and --full are contradictory requests
|
|
142
|
+
// (a spark is a thought offered up BECAUSE it is not developed), and silently
|
|
143
|
+
// picking one would file something the caller did not ask for.
|
|
144
|
+
if (wantSpark && wantFull) {
|
|
145
|
+
console.error('--spark and --full are contradictory: a spark is a thought you are NOT developing yet. Pick one.');
|
|
146
|
+
process.exit(2);
|
|
147
|
+
}
|
|
148
|
+
if (wantFull && !fieldsFile) {
|
|
149
|
+
console.error('--full needs --fields-file PATH (a JSON object of the five fields).');
|
|
150
|
+
console.error('Run: node scripts/gds/spark.js --template to see them.');
|
|
151
|
+
process.exit(2);
|
|
152
|
+
}
|
|
153
|
+
if (fieldsFile && !wantFull) {
|
|
154
|
+
console.error('--fields-file only means something with --full.');
|
|
155
|
+
process.exit(2);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
let fieldValues = null;
|
|
159
|
+
if (wantFull) {
|
|
160
|
+
try {
|
|
161
|
+
fieldValues = JSON.parse(await fs.readFile(fieldsFile, 'utf8'));
|
|
162
|
+
} catch (err) {
|
|
163
|
+
console.error(`failed to read --fields-file as JSON: ${err.message}`);
|
|
164
|
+
process.exit(2);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
75
167
|
|
|
76
168
|
let bodyMd = '';
|
|
77
169
|
if (bodyFile) {
|
|
@@ -93,6 +185,13 @@ async function main() {
|
|
|
93
185
|
priority = p;
|
|
94
186
|
}
|
|
95
187
|
|
|
188
|
+
// The session is demanded HERE, not at the top of main(): every exit above this
|
|
189
|
+
// line is an argument error, and answering "you are not signed in" to a mistyped
|
|
190
|
+
// flag sends the caller to /builder-reauth for a session that is fine — while a
|
|
191
|
+
// builder who is NOT signed in cannot read the usage block to find out how to
|
|
192
|
+
// sign in. resolveDefaultVersion() is the first call that actually needs a token.
|
|
193
|
+
await requireSession();
|
|
194
|
+
|
|
96
195
|
const version = versionFromArg || (await resolveDefaultVersion());
|
|
97
196
|
|
|
98
197
|
const payload = {
|
|
@@ -102,6 +201,12 @@ async function main() {
|
|
|
102
201
|
suggested_priority: priority,
|
|
103
202
|
};
|
|
104
203
|
if (kindFromArg) payload.kind = kindFromArg;
|
|
204
|
+
if (wantFull) {
|
|
205
|
+
// The server validates these against the R141 declaration and scores them
|
|
206
|
+
// with R142 — this CLI sends them and states nothing about their shape.
|
|
207
|
+
payload.grade = 'full';
|
|
208
|
+
payload.field_values = fieldValues;
|
|
209
|
+
}
|
|
105
210
|
|
|
106
211
|
const api = await cliClient();
|
|
107
212
|
const r = await api.inbox.postInbox({ body: payload });
|
|
@@ -115,11 +220,48 @@ async function main() {
|
|
|
115
220
|
const versionLabel = idea.suggested_version || 'none';
|
|
116
221
|
const priorityLabel = idea.suggested_priority == null ? 'null' : `P${idea.suggested_priority}`;
|
|
117
222
|
|
|
223
|
+
const gradeLabel = idea.grade && idea.grade !== 'quick' ? `, grade=${idea.grade}` : '';
|
|
118
224
|
console.log(
|
|
119
|
-
`Captured idea #${idea.id}: kind=${kindLabel}, version=${versionLabel}, priority=${priorityLabel}`
|
|
225
|
+
`Captured idea #${idea.id}: kind=${kindLabel}, version=${versionLabel}, priority=${priorityLabel}${gradeLabel}`
|
|
120
226
|
);
|
|
227
|
+
if (wantFull) {
|
|
228
|
+
// The server is the authority on whether the grade write landed: capture is
|
|
229
|
+
// FAIL-OPEN on grading (R143), so an idea can land as quick with a warning.
|
|
230
|
+
// Reporting what was ASKED for instead of what happened is how a builder
|
|
231
|
+
// ends up believing they filed a Full Idea when they did not.
|
|
232
|
+
if (idea.grade === 'full') {
|
|
233
|
+
const s = idea.completeness_score;
|
|
234
|
+
console.log(`Filed as a Full Idea — completeness ${Number.isInteger(s) ? `${s}/100` : 'unscored'}.`);
|
|
235
|
+
} else {
|
|
236
|
+
console.log('NOTE: stored as a QUICK idea — the grade could not be written. The text is safe; re-file the fields when you can.');
|
|
237
|
+
}
|
|
238
|
+
}
|
|
121
239
|
console.log(`Inbox now: ${open_count} open`);
|
|
122
240
|
|
|
241
|
+
// --spark: capture, then MARK. Two requests by design, not a workaround — the
|
|
242
|
+
// capture route refuses grade='spark' (R144) because an idea does not arrive
|
|
243
|
+
// pre-marked, and the mark still goes through the author-only route.
|
|
244
|
+
if (wantSpark) {
|
|
245
|
+
const sp = await api.inbox.postInboxIdSpark({ id: idea.id, body: {} });
|
|
246
|
+
if (sp.ok) {
|
|
247
|
+
console.log('Marked as a Big Idea Spark — it is in the queue at /builders/ideas for someone to develop.');
|
|
248
|
+
} else {
|
|
249
|
+
// The idea is filed and that is the part that mattered. Say plainly that
|
|
250
|
+
// only the mark failed, and how to retry just the mark.
|
|
251
|
+
console.error(`Filed, but the spark mark failed (${sp.status}). The idea is safe; retry with:`);
|
|
252
|
+
console.error(` node scripts/gds/spark.js ${idea.id}`);
|
|
253
|
+
process.exitCode = 1;
|
|
254
|
+
}
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// THE POST-CAPTURE OFFER (the brief: "Claude asks if you would like to submit
|
|
259
|
+
// as a full idea after a quick idea is made"). Only for a plain quick capture,
|
|
260
|
+
// only at a real terminal, and declinable with a single Enter.
|
|
261
|
+
if (!wantFull && !noPrompt && process.stdin.isTTY && process.stdout.isTTY) {
|
|
262
|
+
await offerToDevelop(idea.id);
|
|
263
|
+
}
|
|
264
|
+
|
|
123
265
|
// Migration detection lives only on the server now (POST /api/gds/tasks sets
|
|
124
266
|
// tasks.needs_migration from the title/description on create — migration 081,
|
|
125
267
|
// task 876). Ideas carry no touches[] and aren't tasks, so the old CLI-side
|
|
@@ -127,7 +269,7 @@ async function main() {
|
|
|
127
269
|
// referred to; promotion to a task re-runs the detection automatically.
|
|
128
270
|
}
|
|
129
271
|
|
|
130
|
-
module.exports = { resolveDefaultVersion, formatCaptureFailure };
|
|
272
|
+
module.exports = { resolveDefaultVersion, formatCaptureFailure, acceptsOffer };
|
|
131
273
|
|
|
132
274
|
if (require.main === module) {
|
|
133
275
|
main().catch((err) => {
|
package/src/module-api.js
CHANGED
|
@@ -1033,8 +1033,12 @@ const { validateOrRespond, LIMITS, parseId, asyncHandler, corsPublicGet, parsePa
|
|
|
1033
1033
|
// landed since 1.19.301 with no explicit bump. run 32604538465. (task 1002620)
|
|
1034
1034
|
// 1.19.303 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
1035
1035
|
// landed since 1.19.302 with no explicit bump. run 32605477232. (task 1002620)
|
|
1036
|
+
// 1.19.304 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
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)
|
|
1036
1040
|
// ---------------------------------------------------------------------------
|
|
1037
|
-
const CORE_VERSION = '1.19.
|
|
1041
|
+
const CORE_VERSION = '1.19.305'; // CI auto-patch carrier (ADR 0161); see changelog above
|
|
1038
1042
|
|
|
1039
1043
|
// A namespaced logger so a module's log lines are attributable + consistent.
|
|
1040
1044
|
// Usage: const log = api.logger('dev-box'); log.info('mounted');
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
// tests/idea_capture_grades.mjs
|
|
2
|
+
//
|
|
3
|
+
// task 1002927 (BV1.R151, goal 1000062) — capture.js learns the grades.
|
|
4
|
+
//
|
|
5
|
+
// The done-when: capture.js can file all three grades; the post-capture offer
|
|
6
|
+
// appears after a quick capture and is DECLINABLE IN ONE KEYSTROKE; declining
|
|
7
|
+
// leaves a plain quick idea.
|
|
8
|
+
//
|
|
9
|
+
// The riskiest part of this task is not the grades, it is the PROMPT. capture.js
|
|
10
|
+
// is run by agents and scripts with no terminal, and it already reads its body
|
|
11
|
+
// from stdin when --body-file is absent — so an unguarded question would hang
|
|
12
|
+
// every non-interactive caller, silently, forever. The TTY gate is therefore
|
|
13
|
+
// asserted twice: as a source invariant, and by actually running the process with
|
|
14
|
+
// piped stdin and watching it not hang.
|
|
15
|
+
//
|
|
16
|
+
// Run: node tests/idea_capture_grades.mjs
|
|
17
|
+
|
|
18
|
+
import { strict as assert } from 'node:assert';
|
|
19
|
+
import { createRequire } from 'node:module';
|
|
20
|
+
import { spawnSync } from 'node:child_process';
|
|
21
|
+
import fs from 'node:fs';
|
|
22
|
+
import path from 'node:path';
|
|
23
|
+
import { fileURLToPath } from 'node:url';
|
|
24
|
+
|
|
25
|
+
const require = createRequire(import.meta.url);
|
|
26
|
+
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
27
|
+
const CAPTURE = path.join(ROOT, 'scripts/gds/capture.js');
|
|
28
|
+
const SRC = fs.readFileSync(CAPTURE, 'utf8');
|
|
29
|
+
const { acceptsOffer } = require('../scripts/gds/capture.js');
|
|
30
|
+
|
|
31
|
+
// Run capture.js as a real process with piped stdio. Every case below is refused
|
|
32
|
+
// by argument validation BEFORE any network call, so this needs no session.
|
|
33
|
+
function run(args, { input = '' } = {}) {
|
|
34
|
+
return spawnSync(process.execPath, [CAPTURE, ...args], {
|
|
35
|
+
input, encoding: 'utf8', timeout: 20000,
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
let passed = 0;
|
|
40
|
+
let failed = 0;
|
|
41
|
+
function test(name, fn) {
|
|
42
|
+
try { fn(); passed++; console.log(` ok ${name}`); }
|
|
43
|
+
catch (err) { failed++; console.error(` FAIL ${name}\n ${err.message}`); }
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// --- declinable in ONE keystroke ----------------------------------------
|
|
47
|
+
|
|
48
|
+
test('a bare Enter declines — that is the one keystroke', () => {
|
|
49
|
+
assert.equal(acceptsOffer(''), false);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test('only an explicit yes accepts', () => {
|
|
53
|
+
for (const yes of ['y', 'Y', 'yes', 'YES', ' y ', 'Yes']) {
|
|
54
|
+
assert.equal(acceptsOffer(yes), true, JSON.stringify(yes));
|
|
55
|
+
}
|
|
56
|
+
for (const no of ['n', 'no', 'N', 'maybe', 'yep', 'ya', '1', 'ok']) {
|
|
57
|
+
assert.equal(acceptsOffer(no), false, JSON.stringify(no));
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test('an EOF or a closed pipe declines rather than throwing', () => {
|
|
62
|
+
// readline resolves with undefined on EOF; treating that as a yes would drag a
|
|
63
|
+
// non-interactive caller into a flow it never asked for.
|
|
64
|
+
assert.equal(acceptsOffer(null), false);
|
|
65
|
+
assert.equal(acceptsOffer(undefined), false);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
// --- the TTY gate --------------------------------------------------------
|
|
69
|
+
|
|
70
|
+
test('the offer is gated on a REAL terminal, both directions', () => {
|
|
71
|
+
assert.match(SRC, /process\.stdin\.isTTY/, 'stdin must be a terminal');
|
|
72
|
+
assert.match(SRC, /process\.stdout\.isTTY/, 'and stdout, or the question goes into a pipe unseen');
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test('the offer is skipped for a Full Idea capture and by --no-prompt', () => {
|
|
76
|
+
// Asking "develop this into a Full Idea?" right after someone filed one would
|
|
77
|
+
// be absurd; --no-prompt is the explicit opt-out for a caller that has a TTY
|
|
78
|
+
// but does not want to be asked.
|
|
79
|
+
assert.match(SRC, /if \(!wantFull && !noPrompt && process\.stdin\.isTTY/);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test('running with PIPED stdin does not hang or prompt', () => {
|
|
83
|
+
// The real regression this guards: a prompt with no TTY blocks forever. These
|
|
84
|
+
// args are refused at validation, which is enough to prove the process exits.
|
|
85
|
+
const r = run(['a title', '--spark', '--full'], { input: '' });
|
|
86
|
+
assert.equal(r.status, 2, `exited cleanly (signal=${r.signal})`);
|
|
87
|
+
assert.equal(r.signal, null, 'not killed by the timeout');
|
|
88
|
+
assert.doesNotMatch(String(r.stdout), /Develop this into a Full Idea\?/);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
// --- all three grades reachable -----------------------------------------
|
|
92
|
+
|
|
93
|
+
test('--full requires the five fields, and says where to find them', () => {
|
|
94
|
+
const r = run(['a title', '--full']);
|
|
95
|
+
assert.equal(r.status, 2);
|
|
96
|
+
assert.match(r.stderr, /--full needs --fields-file/);
|
|
97
|
+
assert.match(r.stderr, /spark\.js --template/, 'it points at the thing that prints the fields');
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
test('--fields-file without --full is refused rather than silently ignored', () => {
|
|
101
|
+
const r = run(['a title', '--fields-file', '/tmp/nope.json']);
|
|
102
|
+
assert.equal(r.status, 2);
|
|
103
|
+
assert.match(r.stderr, /only means something with --full/);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test('--spark and --full are refused together, with the reason', () => {
|
|
107
|
+
const r = run(['a title', '--spark', '--full']);
|
|
108
|
+
assert.equal(r.status, 2);
|
|
109
|
+
assert.match(r.stderr, /contradictory/);
|
|
110
|
+
// The reason matters: it is not an arbitrary exclusion, a spark is BY DEFINITION
|
|
111
|
+
// a thought you are not developing yet.
|
|
112
|
+
assert.match(r.stderr, /NOT developing yet/);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
test('an unreadable --fields-file fails before any capture', () => {
|
|
116
|
+
const r = run(['a title', '--full', '--fields-file', '/tmp/definitely-not-here.json']);
|
|
117
|
+
assert.equal(r.status, 2);
|
|
118
|
+
assert.match(r.stderr, /failed to read --fields-file as JSON/);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
test('--spark is capture-then-MARK, not a pre-marked capture', () => {
|
|
122
|
+
// The route refuses grade='spark' at capture (R144: an idea does not arrive
|
|
123
|
+
// pre-marked), and this CLI does not work around that — it performs the second
|
|
124
|
+
// act through the author-only mark route.
|
|
125
|
+
assert.match(SRC, /postInboxIdSpark/, 'it calls the mark route');
|
|
126
|
+
// Asserted over the PAYLOAD ASSIGNMENT, not over the file text: the file
|
|
127
|
+
// explains the rule in prose, and forbidding the string anywhere would fail on
|
|
128
|
+
// the comment that documents why the rule exists. (Fourth time this session an
|
|
129
|
+
// "absent from the source" assertion needed narrowing to what it meant —
|
|
130
|
+
// learning 1000196.)
|
|
131
|
+
const assignments = [...SRC.matchAll(/payload\.grade\s*=\s*'([a-z]+)'/g)].map((m) => m[1]);
|
|
132
|
+
assert.deepEqual(assignments, ['full'], 'the only grade this CLI ever sends at capture is full');
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
test('a full capture reports what the SERVER stored, not what was asked for', () => {
|
|
136
|
+
// Capture is fail-open on grading (R143): the idea can land as quick with a
|
|
137
|
+
// warning. Reporting the request is how someone believes they filed a Full Idea
|
|
138
|
+
// when they did not.
|
|
139
|
+
assert.match(SRC, /idea\.grade === 'full'/);
|
|
140
|
+
assert.match(SRC, /stored as a QUICK idea/);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
test('a failed spark mark still reports the idea as safe, and how to retry', () => {
|
|
144
|
+
// The capture already committed; losing the mark must not read as losing the
|
|
145
|
+
// thought.
|
|
146
|
+
assert.match(SRC, /the idea is safe|The idea is safe/);
|
|
147
|
+
assert.match(SRC, /spark\.js \$\{idea\.id\}/, 'it prints the exact retry command');
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
test('usage names all three grades', () => {
|
|
151
|
+
const r = run([]);
|
|
152
|
+
assert.equal(r.status, 2);
|
|
153
|
+
for (const flag of ['--spark', '--full', '--no-prompt']) {
|
|
154
|
+
assert.ok(r.stderr.includes(flag), `usage mentions ${flag}`);
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
console.log(`\nidea_capture_grades: ${passed} passed, ${failed} failed`);
|
|
159
|
+
if (failed > 0) process.exit(1);
|
|
@@ -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);
|