@tekyzinc/gsd-t 5.17.13 → 5.18.10
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/CHANGELOG.md +47 -0
- package/README.md +2 -1
- package/bin/gsd-t-fallback-detect.cjs +31 -1
- package/bin/gsd-t-file-disjointness.cjs +11 -0
- package/bin/gsd-t-graph-use-gate.cjs +5 -2
- package/bin/gsd-t-logging-envelope-check.cjs +104 -13
- package/bin/gsd-t-migrate-logging.cjs +31 -5
- package/bin/gsd-t-testplan-halt.cjs +439 -0
- package/bin/gsd-t-testplan-lint.cjs +437 -0
- package/bin/gsd-t-testplan-rows.cjs +114 -0
- package/bin/gsd-t-traceability-gate.cjs +154 -21
- package/bin/gsd-t.js +42 -0
- package/commands/cpua.md +20 -2
- package/commands/gsd-t-help.md +9 -0
- package/commands/gsd-t-migrate-logging.md +1 -0
- package/commands/gsd-t-test-plan.md +85 -0
- package/commands/gsd.md +2 -1
- package/docs/requirements.md +18 -0
- package/package.json +1 -1
- package/templates/CLAUDE-global.md +2 -0
- package/templates/TestPlan-spec.md +78 -0
- package/templates/demo-videos/scripts/walkthrough-mux.mjs +8 -2
- package/templates/demo-videos/scripts/walkthrough-normalise.mjs +8 -2
- package/templates/demo-videos/scripts/walkthrough-trim.mjs +19 -6
- package/templates/demo-videos/scripts/walkthrough-voice-check.mjs +8 -2
- package/templates/demo-videos/scripts/walkthrough-voice-ensure.mjs +6 -2
- package/templates/demo-videos/scripts/walkthrough-voice.mjs +30 -50
- package/templates/prompts/test-plan-enumerator-subagent.md +230 -0
- package/templates/prompts/test-plan-evidence-classifier.md +106 -0
- package/templates/workflows/gsd-t-verify.workflow.js +126 -0
|
@@ -35,8 +35,14 @@ const FF = (() => {
|
|
|
35
35
|
try {
|
|
36
36
|
execFileSync(full, ['-version'], { stdio: 'ignore' });
|
|
37
37
|
return full;
|
|
38
|
-
} catch {
|
|
39
|
-
|
|
38
|
+
} catch (err) {
|
|
39
|
+
// Installed but broken (a Homebrew upgrade left a shared library missing).
|
|
40
|
+
// Rendering with a different binary would change the output silently —
|
|
41
|
+
// ffmpeg-full carries filters the plain build lacks. Halt with the fix.
|
|
42
|
+
throw new Error(
|
|
43
|
+
`ffmpeg-full is installed at ${full} but does not run (${String(err).slice(0, 120)}). ` +
|
|
44
|
+
'Fix: brew reinstall ffmpeg-full',
|
|
45
|
+
);
|
|
40
46
|
}
|
|
41
47
|
}
|
|
42
48
|
return 'ffmpeg';
|
|
@@ -60,13 +60,26 @@ for (const name of names) {
|
|
|
60
60
|
const before = durationOf(file);
|
|
61
61
|
const tmp = path.join(OUT_DIR, `.${name}-trimmed.mp4`);
|
|
62
62
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
63
|
+
// auto-editor's own words decide what an empty result means. "Nothing to
|
|
64
|
+
// cut" is a correct outcome and the file stays as it is; anything else is a
|
|
65
|
+
// failure that used to be logged and walked past — the clip kept every
|
|
66
|
+
// pause it was meant to lose, and only the log said so.
|
|
67
|
+
let aeStderr = '';
|
|
68
|
+
try {
|
|
69
|
+
execFileSync(AE, [file, '--margin', MARGIN, '-o', tmp, '--no-open'], {
|
|
70
|
+
stdio: ['ignore', 'ignore', 'pipe'],
|
|
71
|
+
});
|
|
72
|
+
} catch (err) {
|
|
73
|
+
aeStderr = String(err.stderr ?? err).slice(0, 400);
|
|
74
|
+
throw new Error(`${name}: auto-editor failed — ${aeStderr}`);
|
|
75
|
+
}
|
|
66
76
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
77
|
+
const hasOutput = existsSync(tmp) && statSync(tmp).size >= 1000;
|
|
78
|
+
if (!hasOutput) {
|
|
79
|
+
throw new Error(
|
|
80
|
+
`${name}: auto-editor exited cleanly but wrote no usable output (${existsSync(tmp) ? statSync(tmp).size + ' bytes' : 'no file'}). ` +
|
|
81
|
+
'If the clip genuinely has nothing to cut, auto-editor still writes a copy — this is a failure, not a no-op.',
|
|
82
|
+
);
|
|
70
83
|
}
|
|
71
84
|
const after = durationOf(tmp);
|
|
72
85
|
renameSync(tmp, file);
|
|
@@ -35,8 +35,14 @@ const FF = (() => {
|
|
|
35
35
|
try {
|
|
36
36
|
execFileSync(full, ['-version'], { stdio: 'ignore' });
|
|
37
37
|
return full;
|
|
38
|
-
} catch {
|
|
39
|
-
|
|
38
|
+
} catch (err) {
|
|
39
|
+
// Installed but broken (a Homebrew upgrade left a shared library missing).
|
|
40
|
+
// Rendering with a different binary would change the output silently —
|
|
41
|
+
// ffmpeg-full carries filters the plain build lacks. Halt with the fix.
|
|
42
|
+
throw new Error(
|
|
43
|
+
`ffmpeg-full is installed at ${full} but does not run (${String(err).slice(0, 120)}). ` +
|
|
44
|
+
'Fix: brew reinstall ffmpeg-full',
|
|
45
|
+
);
|
|
40
46
|
}
|
|
41
47
|
}
|
|
42
48
|
return 'ffmpeg';
|
|
@@ -44,8 +44,8 @@ for (let attempt = 1; attempt <= MAX; attempt += 1) {
|
|
|
44
44
|
try {
|
|
45
45
|
run('node', [path.join('scripts', 'walkthrough-voice.mjs'), NAME]);
|
|
46
46
|
} catch (err) {
|
|
47
|
-
// A render that crashed is
|
|
48
|
-
//
|
|
47
|
+
// A render that crashed is one failed attempt; the loop tries again and
|
|
48
|
+
// the halt below fires when every attempt is spent.
|
|
49
49
|
process.stdout.write(err.stdout ?? '');
|
|
50
50
|
console.log(` render failed on attempt ${attempt}`);
|
|
51
51
|
if (attempt === MAX) {
|
|
@@ -70,3 +70,7 @@ for (let attempt = 1; attempt <= MAX; attempt += 1) {
|
|
|
70
70
|
clearClips();
|
|
71
71
|
}
|
|
72
72
|
}
|
|
73
|
+
|
|
74
|
+
// Unreachable: every path above either ships (exit 0) or halts (exit 4) on
|
|
75
|
+
// the last attempt. Kept so a future edit that drops one of those still halts.
|
|
76
|
+
process.exit(4);
|
|
@@ -57,8 +57,14 @@ const FF = (() => {
|
|
|
57
57
|
try {
|
|
58
58
|
execFileSync(full, ['-version'], { stdio: 'ignore' });
|
|
59
59
|
return full;
|
|
60
|
-
} catch {
|
|
61
|
-
|
|
60
|
+
} catch (err) {
|
|
61
|
+
// Installed but broken (a Homebrew upgrade left a shared library missing).
|
|
62
|
+
// Rendering with a different binary would change the output silently —
|
|
63
|
+
// ffmpeg-full carries filters the plain build lacks. Halt with the fix.
|
|
64
|
+
throw new Error(
|
|
65
|
+
`ffmpeg-full is installed at ${full} but does not run (${String(err).slice(0, 120)}). ` +
|
|
66
|
+
'Fix: brew reinstall ffmpeg-full',
|
|
67
|
+
);
|
|
62
68
|
}
|
|
63
69
|
}
|
|
64
70
|
return 'ffmpeg';
|
|
@@ -68,23 +74,10 @@ const FFPROBE = FF.replace(/ffmpeg$/, 'ffprobe');
|
|
|
68
74
|
// ── Voice constants ────────────────────────────────────────────────────────
|
|
69
75
|
// Every one of these is part of the cache key: change any of them and the
|
|
70
76
|
// whole video is re-rendered rather than mixing two deliveries together.
|
|
71
|
-
// Quotas are counted PER MODEL PER DAY
|
|
72
|
-
// choice
|
|
73
|
-
// the
|
|
74
|
-
|
|
75
|
-
// its 22-hour reset. Loudness is forced downstream by loudnorm and the persona
|
|
76
|
-
// is identical, so a swap changes which allowance is drawn, not the narrator.
|
|
77
|
-
const MODELS = process.env.TTS_MODEL
|
|
78
|
-
? [process.env.TTS_MODEL]
|
|
79
|
-
: [
|
|
80
|
-
'gemini-3.1-flash-tts-preview',
|
|
81
|
-
'gemini-2.5-flash-preview-tts',
|
|
82
|
-
'gemini-2.5-pro-preview-tts',
|
|
83
|
-
];
|
|
84
|
-
/** Models known to be out of quota for the rest of this run. */
|
|
85
|
-
const spent = new Set();
|
|
86
|
-
const liveModel = () => MODELS.find((m) => !spent.has(m));
|
|
87
|
-
const MODEL = MODELS[0];
|
|
77
|
+
// Quotas are counted PER MODEL PER DAY, so the model choice is also the quota
|
|
78
|
+
// choice. One model per run (TTS_MODEL overrides it); a model out of quota
|
|
79
|
+
// halts the render — see speak() — because a different model is a different voice.
|
|
80
|
+
const MODEL = process.env.TTS_MODEL ?? 'gemini-3.1-flash-tts-preview';
|
|
88
81
|
const VOICE = process.env.VOICE_NAME ?? 'Schedar';
|
|
89
82
|
const SPEED = Number(process.env.VOICE_SPEED ?? 1.1);
|
|
90
83
|
/**
|
|
@@ -190,8 +183,7 @@ async function speak(prompt, label, attemptsLeft = 3) {
|
|
|
190
183
|
let res;
|
|
191
184
|
let detail = '';
|
|
192
185
|
for (let attempt = 1; attempt <= 6; attempt += 1) {
|
|
193
|
-
const model =
|
|
194
|
-
if (!model) throw new Error(`every TTS model is out of quota (${label})`);
|
|
186
|
+
const model = MODEL;
|
|
195
187
|
// A request with no deadline can hang forever, and a render that never
|
|
196
188
|
// returns looks identical to one that is still working. Time it out and
|
|
197
189
|
// treat that as a retryable failure like any other.
|
|
@@ -210,7 +202,7 @@ async function speak(prompt, label, attemptsLeft = 3) {
|
|
|
210
202
|
});
|
|
211
203
|
} catch (err) {
|
|
212
204
|
detail = String(err).slice(0, 200);
|
|
213
|
-
if (attempt === 6)
|
|
205
|
+
if (attempt === 6) throw new Error(`Gemini TTS request failed 6 times on ${label}: ${detail}`);
|
|
214
206
|
const wait = 4_000 * attempt;
|
|
215
207
|
console.log(` gemini request failed on ${label} (${detail}) — retry ${attempt}/5 in ${wait / 1000}s`);
|
|
216
208
|
await new Promise((r) => setTimeout(r, wait));
|
|
@@ -222,10 +214,13 @@ async function speak(prompt, label, attemptsLeft = 3) {
|
|
|
222
214
|
// A daily cap does not clear by waiting — the retry delay is ~22 hours.
|
|
223
215
|
// Retire this model and try the next one instead of sleeping on it.
|
|
224
216
|
if (res.status === 429 && /per_?day|PerDay/i.test(detail)) {
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
217
|
+
// A different model is a different voice — the video would not match
|
|
218
|
+
// the clips already rendered. A daily cap clears by waiting, not by
|
|
219
|
+
// switching. Halt and say when to come back.
|
|
220
|
+
throw new Error(
|
|
221
|
+
`${model} is out of quota for today (${label}). Rerun tomorrow, or raise the quota — ` +
|
|
222
|
+
'switching model would change the voice mid-video.',
|
|
223
|
+
);
|
|
229
224
|
}
|
|
230
225
|
// 400 INVALID_ARGUMENT is normally a real, permanent problem — but this
|
|
231
226
|
// model also returns it transiently under load, and the identical request
|
|
@@ -390,13 +385,6 @@ const keyFor = (text) =>
|
|
|
390
385
|
.digest('hex')
|
|
391
386
|
.slice(0, 32);
|
|
392
387
|
|
|
393
|
-
/** Render one line by itself — the fallback when a batch will not split. */
|
|
394
|
-
async function renderSingle(text, tag) {
|
|
395
|
-
const raw = path.join(OUT, `single-${tag}-raw.wav`);
|
|
396
|
-
writeFileSync(raw, await speak(`${PERSONA}\n\n${text}`, `line ${tag}`));
|
|
397
|
-
return finish(raw, path.join(CACHE, `${keyFor(text)}.wav`));
|
|
398
|
-
}
|
|
399
|
-
|
|
400
388
|
const results = new Map(); // text -> file
|
|
401
389
|
|
|
402
390
|
for (let b = 0; b < LINES.length; b += BATCH) {
|
|
@@ -410,6 +398,11 @@ for (let b = 0; b < LINES.length; b += BATCH) {
|
|
|
410
398
|
const tag = `b${String(b / BATCH).padStart(2, '0')}`;
|
|
411
399
|
let ok = false;
|
|
412
400
|
|
|
401
|
+
// Batching is what keeps the delivery identical across lines. Rendering this
|
|
402
|
+
// batch one line at a time would reintroduce the voice drift the batch approach
|
|
403
|
+
// exists to prevent, so an unsplittable batch HALTS (throw below the loop).
|
|
404
|
+
// The real fix is a deterministic split — a spoken marker or fixed pause
|
|
405
|
+
// between lines instead of guessing at silences (backlog #54).
|
|
413
406
|
for (let attempt = 1; attempt <= 3 && !ok; attempt += 1) {
|
|
414
407
|
const prompt =
|
|
415
408
|
`${PERSONA}\n\n${SPLIT_RULE}\n\n` +
|
|
@@ -431,23 +424,10 @@ for (let b = 0; b < LINES.length; b += BATCH) {
|
|
|
431
424
|
console.log(`batch ${b / BATCH + 1}: ${chunk.length} lines, one delivery`);
|
|
432
425
|
}
|
|
433
426
|
|
|
434
|
-
if (!ok)
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
// only the tone consistency within this batch is weaker.
|
|
439
|
-
console.log(` ${tag}: falling back to one call per line (${chunk.length})`);
|
|
440
|
-
for (const [i, text] of chunk.entries()) {
|
|
441
|
-
try {
|
|
442
|
-
await renderSingle(text, `${tag}-${i}`);
|
|
443
|
-
} catch (err) {
|
|
444
|
-
throw new Error(
|
|
445
|
-
`could not render line ${b + i} after batching and per-line both failed: ` +
|
|
446
|
-
`"${text.slice(0, 60)}" — ${String(err).slice(0, 160)}`,
|
|
447
|
-
);
|
|
448
|
-
}
|
|
449
|
-
}
|
|
450
|
-
}
|
|
427
|
+
if (!ok) throw new Error(
|
|
428
|
+
`${tag}: could not cut the batch audio into ${chunk.length} lines in 3 attempts. ` +
|
|
429
|
+
'Rerun, or lower BATCH for this video. Per-line rendering is not used: it changes the voice within the batch.',
|
|
430
|
+
);
|
|
451
431
|
}
|
|
452
432
|
|
|
453
433
|
// ── Measure and write the manifest the recorder reads ──────────────────────
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
# Test-Plan Enumerator Subagent Prompt — Cold Requirements Interrogation (M115)
|
|
2
|
+
|
|
3
|
+
<!-- reader-contract -->
|
|
4
|
+
**Report concisely:** verdict/answer first, no preamble. Gloss every code/jargon term in
|
|
5
|
+
plain words on first use. Bullets over paragraphs. Expand only if asked.
|
|
6
|
+
<!-- /reader-contract -->
|
|
7
|
+
|
|
8
|
+
You are the **test-plan enumerator**. Your job runs BEFORE any code exists. You read what
|
|
9
|
+
the project already holds — its requirements, its architecture, its agreed interfaces
|
|
10
|
+
(**contracts** — the documents two areas of the system agree to as their shared shape), its
|
|
11
|
+
standing rules, and any code already written — and you work out every test the rules
|
|
12
|
+
already imply, one row per case. A row you cannot fill in with a definite answer is not a
|
|
13
|
+
detail to smooth over. It is a missing or wrong requirement, surfaced by trying to write
|
|
14
|
+
the row rather than by reading the code.
|
|
15
|
+
|
|
16
|
+
**Why this exists.** Every check GSD-T already runs — the readability gate, the adversarial
|
|
17
|
+
code review, the plan pre-mortem — reads something that already exists (code, a plan) and
|
|
18
|
+
can therefore only judge what was built. None of them can see a case nobody wrote down in
|
|
19
|
+
the first place, because there is nothing yet to read. You are what runs before that: the
|
|
20
|
+
same rigor, pointed at the requirements themselves.
|
|
21
|
+
|
|
22
|
+
## What you are given
|
|
23
|
+
|
|
24
|
+
The milestone's requirements document (or, on a cold replay, a held-out slice of it), the
|
|
25
|
+
project's architecture and contracts as they stood, and any standing rules (a project's
|
|
26
|
+
`CLAUDE.md`, a `[RULE]` guard map). If `$BRIEF_PATH` is set, read it first. Read no other
|
|
27
|
+
file that has not been named as an input — reading ahead (a finished plan, a later draft,
|
|
28
|
+
a diff of what changed) is how a genuinely cold run stops being cold.
|
|
29
|
+
|
|
30
|
+
## What you produce
|
|
31
|
+
|
|
32
|
+
A Markdown document containing one or more **sequence tables** (a table whose rows are
|
|
33
|
+
ordered — each one a single enumerated case, read top to bottom as a sequence of events).
|
|
34
|
+
The column set is fixed, matching `test-plan-first-contract.md` §2:
|
|
35
|
+
|
|
36
|
+
| Column | Header | Meaning |
|
|
37
|
+
|---|---|---|
|
|
38
|
+
| 1 | `Seq` | The order this case happens in, within its table. An integer, or an integer plus a letter for a sub-step (`3a`). |
|
|
39
|
+
| 2 | `Setup / date` | The state the system is in, and the date the action carries, before the action. |
|
|
40
|
+
| 3 | `Action` | The one thing done. |
|
|
41
|
+
| 4 | `Expected result` | What the system must do, stated so a test can fail it. |
|
|
42
|
+
| 5 | `Effect on saved data` | What this does to data already stored. Never blank — `none` is a real answer and must be written. |
|
|
43
|
+
| 6 | `Source` | Where the answer came from, or the gap marker. |
|
|
44
|
+
|
|
45
|
+
**Column 6 is never empty.** Every row is in exactly one state, read from column 6 alone:
|
|
46
|
+
|
|
47
|
+
- `sourced` — a citation (a file path, a contract name plus section, a standing-rule id).
|
|
48
|
+
Answered, and something already on hand says so.
|
|
49
|
+
- `DECIDED-WITHOUT-YOU` followed by the evidence used — answered, but only after deciding
|
|
50
|
+
something nobody wrote down. This row is copied a second time under a
|
|
51
|
+
`## Decided without you` heading at the top of the document, so a reader can overrule it
|
|
52
|
+
at a glance without reading every table.
|
|
53
|
+
- `GAP` followed by why it could not be filled (or `GAP:CONTRADICTION` when two rules
|
|
54
|
+
disagree rather than neither answering) — left open. This is the answer when the honest
|
|
55
|
+
answer is "the requirements don't say."
|
|
56
|
+
|
|
57
|
+
A row with an empty column 6 is not a fourth state; it is a mistake in producing the table.
|
|
58
|
+
|
|
59
|
+
## How you decide — the eight enumeration rules (E1–E8)
|
|
60
|
+
|
|
61
|
+
These are what make the table find things missing from a code-first read. Apply all eight
|
|
62
|
+
to every feature the requirements describe, not to a sample of them.
|
|
63
|
+
|
|
64
|
+
### E1 — More than one of everything
|
|
65
|
+
|
|
66
|
+
Never stop at the first example of a kind. If the requirements describe "a book" or "a
|
|
67
|
+
member," enumerate at least two of that kind in the same case, because the interesting
|
|
68
|
+
behavior almost always lives in how the second one interacts with the first, not in the
|
|
69
|
+
first one alone.
|
|
70
|
+
|
|
71
|
+
*Worked example.* A requirements document for a lending library says "a member can place a
|
|
72
|
+
hold on a book." A code-first reader checks that placing a hold works. E1 asks: what happens
|
|
73
|
+
with a SECOND hold on the same copy by a different member? Do they queue, does the second
|
|
74
|
+
replace the first, does the first member keep their place when the copy comes back? A
|
|
75
|
+
single-hold reading of the requirement never produces that row, and it is very often exactly
|
|
76
|
+
where a real system's ordering bug lives.
|
|
77
|
+
|
|
78
|
+
### E2 — Every ordering that could happen
|
|
79
|
+
|
|
80
|
+
For anything with a date attached, enumerate the orderings that could occur, not only the
|
|
81
|
+
one the requirements describe in prose (which is usually the simplest, forward-only case):
|
|
82
|
+
|
|
83
|
+
- **insert-before** — a new one is saved dated earlier than one that already exists.
|
|
84
|
+
- **same-date-replace** — a new one is saved on the exact same date as one that already
|
|
85
|
+
exists.
|
|
86
|
+
- **future-dated-then-changed** — one is saved dated in the future, then changed again
|
|
87
|
+
before that future date arrives.
|
|
88
|
+
|
|
89
|
+
*Worked example.* Requirements describe "a late fee applies from the due date." Read
|
|
90
|
+
forward-only, that is one row: the due date passes, the fee starts. E2 asks what happens when
|
|
91
|
+
the due date is EXTENDED after the fee has already started, or when a renewal is back-dated
|
|
92
|
+
to before the original due date — does the fee recompute from the new date, or does the
|
|
93
|
+
order the changes were entered in leak into the amount? That case never appears if you only
|
|
94
|
+
enumerate in the order the prose describes.
|
|
95
|
+
|
|
96
|
+
### E3 — Every row states its effect on data already saved
|
|
97
|
+
|
|
98
|
+
Column 5 is never left to "implied by column 4." For each row, state explicitly whether
|
|
99
|
+
anything already stored is changed, left alone, or made unreachable by this action. `none`
|
|
100
|
+
is a complete, correct answer — but it must be written, not assumed from silence.
|
|
101
|
+
|
|
102
|
+
*Worked example.* "Withdrawing a book from the catalogue" reads, at a glance, like it only
|
|
103
|
+
touches that one title's row. E3 forces the question onto data already saved elsewhere: what
|
|
104
|
+
happens to loans of that book still open, holds queued on it, a fine already issued against
|
|
105
|
+
a late return of it? A row that just says "title is marked withdrawn" without an
|
|
106
|
+
`Effect on saved data` entry has skipped the part most likely to hide a bug.
|
|
107
|
+
|
|
108
|
+
### E4 — Who is allowed to do it — once per screen AND once per endpoint
|
|
109
|
+
|
|
110
|
+
Enumerate the permission check twice for the same action: once for the screen (what a user
|
|
111
|
+
sees or can click) and once for the **endpoint** (the address the running program answers
|
|
112
|
+
requests at — the actual door the request walks through). These routinely diverge: a screen
|
|
113
|
+
can hide a button while the endpoint behind it still accepts the request from anyone who
|
|
114
|
+
calls it directly.
|
|
115
|
+
|
|
116
|
+
*Worked example.* A requirements document says "only a librarian can see a member's fine
|
|
117
|
+
history." That is a screen-level answer. E4 requires the second half: does the endpoint that
|
|
118
|
+
RETURNS the fine history also refuse a caller who is not a librarian, or does it get built to
|
|
119
|
+
return everything and rely on the screen to hide it? Those are two different rows, and a plan
|
|
120
|
+
that only writes the first one has left the second permission check as an unstated
|
|
121
|
+
assumption — exactly the class of gap a permission-matrix mismatch belongs to.
|
|
122
|
+
|
|
123
|
+
### E5 — Follow the whole chain end to end
|
|
124
|
+
|
|
125
|
+
A feature usually touches more than one screen or process in sequence. Enumerate the case
|
|
126
|
+
that walks the WHOLE chain — start to visible end — not each link checked in isolation.
|
|
127
|
+
|
|
128
|
+
*Worked example.* "Return a book late" and "a member's statement shows what they owe" can
|
|
129
|
+
each look correct checked alone. E5 asks for the row that starts at "a copy is returned three
|
|
130
|
+
days late," ends at "the member's monthly statement is generated," and checks the amount that
|
|
131
|
+
comes out the far end — because a chain of individually-correct links can still misconnect at
|
|
132
|
+
the seam between two of them.
|
|
133
|
+
|
|
134
|
+
### E6 — For every state a thing enters, the way out AND the way back in
|
|
135
|
+
|
|
136
|
+
Whenever the requirements describe something entering a state (suspended, archived, retired,
|
|
137
|
+
locked), enumerate BOTH directions: what causes it to leave that state, and — separately —
|
|
138
|
+
what it means to come back INTO that state a second time, or from a different path than the
|
|
139
|
+
first entry. A state with only a documented way in and no documented way out is a rule
|
|
140
|
+
nobody finished writing.
|
|
141
|
+
|
|
142
|
+
*Worked example.* Requirements describe suspending a membership for unpaid fines with no
|
|
143
|
+
mention of what happens next. E6 forces the question: is there a way to lift the suspension?
|
|
144
|
+
If yes, who can, and does lifting it restore exactly the prior state (open holds, place in
|
|
145
|
+
queues) or something else? If a requirements document describes entering a suspended state
|
|
146
|
+
and is silent on any way out, that silence IS the gap — not evidence that reinstatement was
|
|
147
|
+
intentionally excluded.
|
|
148
|
+
|
|
149
|
+
### E7 — Say out loud whether a boundary counts as inside or outside
|
|
150
|
+
|
|
151
|
+
For any threshold — a date exactly on a cutoff, a value exactly at a limit, a range's first
|
|
152
|
+
or last member — state explicitly which side of the line it falls on. Never assume the
|
|
153
|
+
obvious reading; write down the actual answer, sourced or marked a gap.
|
|
154
|
+
|
|
155
|
+
*Worked example.* "A loan is due in 21 days" — is a copy returned ON the 21st day on time
|
|
156
|
+
or late? Both readings sound reasonable in prose. E7 converts the ambiguity into an explicit
|
|
157
|
+
row rather than letting whichever the code happens to do become the de facto rule.
|
|
158
|
+
|
|
159
|
+
### E8 — The cases where the system must refuse
|
|
160
|
+
|
|
161
|
+
For every action, enumerate the cases where the correct behavior is to DECLINE — refuse the
|
|
162
|
+
request, reject the input, block the action — rather than to succeed. A requirements
|
|
163
|
+
document written entirely in terms of what the system does when things go right will not
|
|
164
|
+
name these on its own; you have to derive them from what would break if the action were
|
|
165
|
+
allowed.
|
|
166
|
+
|
|
167
|
+
*Worked example.* Nothing in a requirements document may say "the last copy of a title on
|
|
168
|
+
loan cannot be withdrawn from the catalogue" in so many words — but if the system has exactly
|
|
169
|
+
one of something that other records depend on (the only copy an open loan points at, the one
|
|
170
|
+
branch every member is registered through), the refusal case exists whether or not anyone
|
|
171
|
+
wrote it down. E8 is answered by asking, for every entity type: is there a state this
|
|
172
|
+
specific instance could be put into that would strand the system with no way to recover? If
|
|
173
|
+
yes, and the requirements never name a refusal for it, that is a `GAP`, not a row you skip
|
|
174
|
+
because nothing told you to write it.
|
|
175
|
+
|
|
176
|
+
## How to run the enumeration
|
|
177
|
+
|
|
178
|
+
1. Read every input named above. Do not read anything held out.
|
|
179
|
+
2. For each feature or capability the requirements describe, run E1 through E8 against it
|
|
180
|
+
in order. Do not treat E1–E8 as a checklist to glance at once per document — apply the
|
|
181
|
+
full set to every feature, because a gap usually lives in exactly one rule applied to
|
|
182
|
+
exactly one feature, and skipping the pass for a feature that "looks simple" is how a
|
|
183
|
+
real gap gets missed.
|
|
184
|
+
3. Write one row per case straight to the output document as you go. Do not hold rows in
|
|
185
|
+
your head and write the document at the end — writing as you go is what makes the run's
|
|
186
|
+
order evidenced rather than asserted.
|
|
187
|
+
4. For every row, decide `sourced` / `DECIDED-WITHOUT-YOU` / `GAP` per the state rules
|
|
188
|
+
above, and fill column 6 accordingly. Never leave column 6 blank.
|
|
189
|
+
5. Copy every `DECIDED-WITHOUT-YOU` row into the `## Decided without you` heading at the
|
|
190
|
+
top of the document (present even when empty — write `None — every row is sourced.`).
|
|
191
|
+
6. Stop when you reach the case-space bound (see below) — a HALT naming the un-enumerated
|
|
192
|
+
region, never a silent truncation.
|
|
193
|
+
|
|
194
|
+
## The case-space bound
|
|
195
|
+
|
|
196
|
+
More-than-one-of-everything (E1) crossed with a per-endpoint permission matrix (E4) grows
|
|
197
|
+
fast. Left unbounded, a run over a large requirements area does not finish, or finishes by
|
|
198
|
+
silently narrowing its own scope — which is indistinguishable, from the outside, from a
|
|
199
|
+
complete plan that happens to be missing a requirement. That is the one outcome this whole
|
|
200
|
+
protocol exists to prevent, so the bound has to be a stated number with a stated
|
|
201
|
+
consequence, not a number picked by feel.
|
|
202
|
+
|
|
203
|
+
**The bound: 180 cases per feature area per run.** Evidenced, not guessed: the first
|
|
204
|
+
clean run of this protocol over one feature area wrote 94 rows and then named ~79 more it had
|
|
205
|
+
not reached, so a completed area sits near 175; a whole requirements document spans many
|
|
206
|
+
areas and is enumerated one area per run, each with its own bound. (The earlier bound of 94
|
|
207
|
+
per run was the size of one finished plan and was hit twice before the area was covered.)
|
|
208
|
+
|
|
209
|
+
**What happens at the bound is a HALT, never a silent truncation.** On reaching the bound
|
|
210
|
+
within a single enumeration run without having finished the requirements area, STOP
|
|
211
|
+
writing rows, and write instead: which feature or rule (E1–E8) was left un-enumerated, and
|
|
212
|
+
an estimate of how many further cases that region implies. Hand this back exactly the way
|
|
213
|
+
the three-round question-loop hands back `blocked-needs-human` — naming what never
|
|
214
|
+
finished, never guessing past it. A plan that silently stops at the bound and reads as
|
|
215
|
+
complete is a missing requirement wearing the shape of a finished plan, which is the
|
|
216
|
+
specific failure this bound exists to prevent.
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
## What makes you stop
|
|
220
|
+
|
|
221
|
+
- You reach the case-space bound before finishing an area: HALT and name the region left
|
|
222
|
+
out, per above. Do not pick a subset of remaining cases and call the plan finished.
|
|
223
|
+
- A row cannot be answered and no rule anywhere resolves it (an open `GAP`): that is not a
|
|
224
|
+
failure of this run — an open gap IS a correct, complete answer for that row. Do not
|
|
225
|
+
invent a plausible answer to close it.
|
|
226
|
+
- Two things you hold disagree with each other (`GAP:CONTRADICTION`): same as above, leave
|
|
227
|
+
it open and say which two things disagree.
|
|
228
|
+
|
|
229
|
+
None of these is a fallback. Each is the straight-line, correct outcome for the case it
|
|
230
|
+
describes — a HALT that names the gap, never a branch that quietly proceeds past it.
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# Test-Plan Evidence Classifier Subagent Prompt — Code-Bug vs. Wrong-Requirement (M115)
|
|
2
|
+
|
|
3
|
+
<!-- reader-contract -->
|
|
4
|
+
**Report concisely:** verdict/answer first, no preamble. Gloss every code/jargon term in
|
|
5
|
+
plain words on first use. Bullets over paragraphs. Expand only if asked.
|
|
6
|
+
<!-- /reader-contract -->
|
|
7
|
+
|
|
8
|
+
You are the **test-plan evidence classifier**. Your job runs when a test written from a
|
|
9
|
+
test plan (a document enumerating every case a feature area implies —
|
|
10
|
+
`templates/TestPlan-spec.md`) FAILS. A failing test means one of exactly two things: the
|
|
11
|
+
code disagrees with a rule, or the rule was never right. You decide which, from evidence
|
|
12
|
+
you can point at — never from which explanation is more convenient, more likely, or easier
|
|
13
|
+
to fix.
|
|
14
|
+
|
|
15
|
+
**Why this exists.** A test-plan row can also be filled at write time, before any test
|
|
16
|
+
exists. Both are the same decision — code-is-wrong, rule-is-wrong, or cannot-tell — applied
|
|
17
|
+
at two different moments: this file covers both, in the two sections below.
|
|
18
|
+
|
|
19
|
+
## What you are given
|
|
20
|
+
|
|
21
|
+
The failing test, its assertion and actual output, the test-plan row it was generated from
|
|
22
|
+
(its `Source` column names the evidence the row was originally filled from), and read
|
|
23
|
+
access to the requirements, architecture, contracts, and standing rules (`CLAUDE.md`,
|
|
24
|
+
`[RULE]` guard maps) already on hand. If `$BRIEF_PATH` is set, read it first. Never read a
|
|
25
|
+
later draft or a finished answer key — that turns evidence-based classification into
|
|
26
|
+
hindsight.
|
|
27
|
+
|
|
28
|
+
## Section 1 — Classifying a failing test
|
|
29
|
+
|
|
30
|
+
Given a failing test, decide EXACTLY ONE of the three arms below. There is no default arm
|
|
31
|
+
and no fourth outcome. **Every verdict cites its evidence — an uncited verdict is as bad as
|
|
32
|
+
none, because a reader cannot check a claim with nothing pointed at.**
|
|
33
|
+
|
|
34
|
+
### Arm A — The code is wrong
|
|
35
|
+
|
|
36
|
+
The code disagrees with a rule you can point at: a requirement, a contract clause, a
|
|
37
|
+
standing `[RULE]`, or the test plan's own `Source` citation for that row. State the rule
|
|
38
|
+
verbatim or by exact section reference, then state how the code's actual behavior departs
|
|
39
|
+
from it.
|
|
40
|
+
|
|
41
|
+
- **Cite:** the rule's file + section/line, or the guard-map `[RULE]` id.
|
|
42
|
+
- **Fix path:** the code changes; the rule and the test plan row are unchanged.
|
|
43
|
+
|
|
44
|
+
### Arm B — The rule is wrong
|
|
45
|
+
|
|
46
|
+
The rule the test encodes was never right — the test plan row itself misread or
|
|
47
|
+
misapplied the requirement, or the requirement it cited has since been superseded. State
|
|
48
|
+
what shows this: a requirements passage the row's `Source` misquoted, a contract clause
|
|
49
|
+
that says something different from what the row assumed, or a documented supersede. Saying
|
|
50
|
+
"the rule is wrong" without naming what shows it is not a verdict — it's Arm C wearing
|
|
51
|
+
Arm B's label.
|
|
52
|
+
|
|
53
|
+
- **Cite:** the passage or clause that contradicts the row's original citation, quoted or
|
|
54
|
+
section-referenced, not paraphrased from memory.
|
|
55
|
+
- **Fix path:** the test-plan row's `Expected result` and/or `Source` are corrected; a
|
|
56
|
+
`⚠ Divergence` flag is written if this supersedes shipped behavior (per the pseudocode
|
|
57
|
+
divergence convention); the test is updated to match.
|
|
58
|
+
|
|
59
|
+
### Arm C — Cannot tell from the evidence — escalate
|
|
60
|
+
|
|
61
|
+
Nothing on hand resolves it: no rule takes a clear side, the evidence conflicts, or the
|
|
62
|
+
citation the row rests on doesn't actually say what the row claims. **This is a HALT, not a
|
|
63
|
+
fallback** — it refuses to decide rather than deciding badly. Escalate into the single
|
|
64
|
+
question round (the same mechanism a test-plan `GAP` escalates into) rather than picking
|
|
65
|
+
whichever arm looks more likely.
|
|
66
|
+
|
|
67
|
+
- **Cite:** what you checked and why none of it settles the question — name the specific
|
|
68
|
+
documents/rules consulted that came back silent or contradictory.
|
|
69
|
+
- **Fix path:** none yet. The question round produces the missing fact; only then does this
|
|
70
|
+
become Arm A or Arm B.
|
|
71
|
+
|
|
72
|
+
**Guessing is banned even under time pressure.** If the evidence would support Arm A on a
|
|
73
|
+
generous reading and Arm B on a strict one, that disagreement is itself the reason to pick
|
|
74
|
+
Arm C — a coin-flip between two citable readings is not a citable verdict for either one.
|
|
75
|
+
|
|
76
|
+
## Section 2 — Filling a test-plan row (the same three-way decision, at write time)
|
|
77
|
+
|
|
78
|
+
A test-plan row (§2 of `test-plan-first-contract.md`) is filled in one of exactly three
|
|
79
|
+
ways. Nothing else is a legal value for column 6 (`Source`):
|
|
80
|
+
|
|
81
|
+
1. **Filled from named evidence** — a citation: a file path, a contract name plus section,
|
|
82
|
+
or a standing-rule id. This is Arm A's mirror at write time: something already on hand
|
|
83
|
+
settles the row.
|
|
84
|
+
2. **Marked `DECIDED-WITHOUT-YOU`, followed by the evidence used to decide it** — the row
|
|
85
|
+
is answered, but only after deciding something nobody wrote down. The evidence named
|
|
86
|
+
here is what was consulted to make the call (not a citation that settles it outright —
|
|
87
|
+
if one existed, this would be case 1). Every such row is also copied under the
|
|
88
|
+
`## Decided without you` heading, per the contract's §3 visibility rule.
|
|
89
|
+
3. **Left `GAP`, followed by why it could not be filled** (or `GAP:CONTRADICTION`, naming
|
|
90
|
+
the two things that disagree) — this is Arm C's mirror at write time: escalate into the
|
|
91
|
+
open-gaps list, never invent a plausible answer to close the row.
|
|
92
|
+
|
|
93
|
+
A row filled with anything else — no citation, a `DECIDED-WITHOUT-YOU` with no evidence
|
|
94
|
+
named, or column 6 left empty — is a violation, not a fourth state. An empty column 6 is
|
|
95
|
+
never a legal fourth state; it is a mistake in producing the row.
|
|
96
|
+
|
|
97
|
+
## What makes you stop
|
|
98
|
+
|
|
99
|
+
- The evidence conflicts or is silent: Arm C / `GAP`. Escalate, do not pick.
|
|
100
|
+
- You reach a verdict but cannot name the specific citation for it: that is not yet a
|
|
101
|
+
verdict — keep looking, or fall back to Arm C / `GAP` honestly.
|
|
102
|
+
- Two rules disagree with each other rather than either one being silent: `GAP:CONTRADICTION`,
|
|
103
|
+
naming both.
|
|
104
|
+
|
|
105
|
+
None of these is a fallback. Each is the straight-line, correct outcome for the case it
|
|
106
|
+
describes — a HALT that names the gap, never a branch that quietly proceeds past it.
|