@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
|
@@ -51,6 +51,7 @@
|
|
|
51
51
|
*/
|
|
52
52
|
|
|
53
53
|
const fs = require("node:fs");
|
|
54
|
+
const { walkSections, parseRows, rowState, tableName, REQUIRED_COLUMN_COUNT } = require("./gsd-t-testplan-rows.cjs");
|
|
54
55
|
const path = require("node:path");
|
|
55
56
|
|
|
56
57
|
// ─── tasks.md parsing ────────────────────────────────────────────────────
|
|
@@ -179,6 +180,15 @@ function _acBulletText(lines) {
|
|
|
179
180
|
// bold forms both match, exactly like the M83 field scan.
|
|
180
181
|
const PSEUDOCODE_SECTION_FIELD_RE = /^\s*[-*]?\s*pseudocode-section\s*:/i;
|
|
181
182
|
|
|
183
|
+
// ─── M115 A7: plan-row binding (test-plan-first-contract.md §2 + §7) ──────
|
|
184
|
+
//
|
|
185
|
+
// `**Plan-Row**: <TestPlan-doc-title>#<TableName>/Seq-<n>` — an ADDITIONAL way
|
|
186
|
+
// an acceptance line may clear, alongside the existing Files+Test binding.
|
|
187
|
+
// Recognized by the contract §2 row identity: the plan document, a table
|
|
188
|
+
// name, and a Seq. Matched emphasis-agnostically on the BARED line, exactly
|
|
189
|
+
// like every other field scan in this file — never a substring search.
|
|
190
|
+
const PLAN_ROW_FIELD_RE = /^\s*[-*]?\s*plan-row\s*:/i;
|
|
191
|
+
|
|
182
192
|
/**
|
|
183
193
|
* Parse the `**PseudoCode-Section**: <Title>#<anchor>` citation from a task's
|
|
184
194
|
* lines. Returns { title, anchor } structured segments, or null if absent.
|
|
@@ -255,19 +265,94 @@ function docTitleFromFilename(filename) {
|
|
|
255
265
|
return m ? m[1] : null;
|
|
256
266
|
}
|
|
257
267
|
|
|
268
|
+
/**
|
|
269
|
+
* Parse a `**Plan-Row**: <doc>#<TableName>/Seq-<n>` citation (M115 A7,
|
|
270
|
+
* test-plan-first-contract.md §2 + §7 row identity: plan document + table
|
|
271
|
+
* name + Seq). Structured segments, never substring-matched — the value is
|
|
272
|
+
* split path-as-path on the FIRST `#`, then the anchor on the LAST `/`.
|
|
273
|
+
* Returns { doc, table, seq, raw } or null if the field is absent.
|
|
274
|
+
*/
|
|
275
|
+
function parsePlanRowCitation(lines) {
|
|
276
|
+
for (const ln of lines) {
|
|
277
|
+
const bare = _bare(ln);
|
|
278
|
+
if (!PLAN_ROW_FIELD_RE.test(bare)) continue;
|
|
279
|
+
const idx = bare.indexOf(":");
|
|
280
|
+
if (idx < 0) continue;
|
|
281
|
+
const val = bare.slice(idx + 1).trim();
|
|
282
|
+
const hash = val.indexOf("#");
|
|
283
|
+
if (hash < 0) return { doc: val, table: "", seq: "", raw: val, malformed: true };
|
|
284
|
+
const doc = val.slice(0, hash).trim();
|
|
285
|
+
const anchor = val.slice(hash + 1).trim();
|
|
286
|
+
const slash = anchor.lastIndexOf("/");
|
|
287
|
+
if (slash < 0) return { doc, table: anchor, seq: "", raw: val, malformed: true };
|
|
288
|
+
const table = anchor.slice(0, slash).trim();
|
|
289
|
+
const seqPart = anchor.slice(slash + 1).trim();
|
|
290
|
+
const seqMatch = seqPart.match(/^Seq-(.+)$/i);
|
|
291
|
+
if (!seqMatch) return { doc, table, seq: "", raw: val, malformed: true };
|
|
292
|
+
return { doc, table, seq: seqMatch[1].trim(), raw: val, malformed: false };
|
|
293
|
+
}
|
|
294
|
+
return null;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* Load a single TestPlan doc's real row identities: every `## Table: <name>`
|
|
299
|
+
* section's `Seq` column values, read structurally (positional table-cell
|
|
300
|
+
* parsing, never a substring search) — the same discipline as
|
|
301
|
+
* enumerateSections. Returns Set<"table::seq"> or null if the doc is absent.
|
|
302
|
+
*/
|
|
303
|
+
function loadTestPlanRowIdentities(testPlanDir, docTitle) {
|
|
304
|
+
// TestPlan docs live in `.gsd-t/test-plans/` — the directory the front door
|
|
305
|
+
// (commands/gsd-t-test-plan.md) writes to and the verify gate globs
|
|
306
|
+
// (contract §7, ratified at M115 integrate).
|
|
307
|
+
//
|
|
308
|
+
// Red Team M115 (2026-09-03): a doc title is a single path SEGMENT. Anything
|
|
309
|
+
// else (`../`, a slash, an absolute path) could name a file outside the
|
|
310
|
+
// test-plans directory, so the resolved path must stay inside it.
|
|
311
|
+
if (typeof docTitle !== "string" || !/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(docTitle)) return null;
|
|
312
|
+
const root = path.resolve(testPlanDir);
|
|
313
|
+
const candidate = path.resolve(root, `TestPlan-${docTitle}.md`);
|
|
314
|
+
if (!candidate.startsWith(root + path.sep)) return null;
|
|
315
|
+
let md;
|
|
316
|
+
try { md = fs.readFileSync(candidate, "utf8"); } catch { return null; }
|
|
317
|
+
// identity -> row state, read through the ONE shared plan reader (fence-aware
|
|
318
|
+
// for both fence styles, exact six-cell width). Red Team HIGH: a `GAP` row is
|
|
319
|
+
// an UNANSWERED requirement and must never clear an acceptance criterion;
|
|
320
|
+
// code-review run 4: a row with an EXTRA cell was read as sourced and cleared
|
|
321
|
+
// one anyway. A malformed row is its own state and never clears.
|
|
322
|
+
const identities = new Map();
|
|
323
|
+
for (const sec of walkSections(md)) {
|
|
324
|
+
const table = tableName(sec.heading);
|
|
325
|
+
if (!table) continue; // Open gaps, Sign-off, ... are not tables of identities
|
|
326
|
+
for (const row of parseRows(sec.lines, sec.startLine)) {
|
|
327
|
+
if (!row.cells[0]) continue;
|
|
328
|
+
const id = `${table}::${row.cells[0]}`;
|
|
329
|
+
// Two rows with one identity: nobody can say which one a citation means, so
|
|
330
|
+
// neither clears (code-review M115 run 6 — last-write-wins let a later
|
|
331
|
+
// sourced row overwrite an earlier GAP).
|
|
332
|
+
if (identities.has(id)) { identities.set(id, "duplicate"); continue; }
|
|
333
|
+
const state = row.width !== REQUIRED_COLUMN_COUNT ? "malformed" : rowState(row.cells[5]);
|
|
334
|
+
identities.set(id, state);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
return identities;
|
|
338
|
+
}
|
|
339
|
+
|
|
258
340
|
/**
|
|
259
341
|
* A task is "behavioral" (subject to the gate) if it declares acceptance
|
|
260
342
|
* criteria — i.e. it promises an observable behavior. Pure-scaffolding tasks
|
|
261
343
|
* with no ACs are out of scope (nothing to trace).
|
|
262
344
|
*/
|
|
263
|
-
function assessTask(task) {
|
|
345
|
+
function assessTask(task, opts = {}) {
|
|
264
346
|
const lines = task.lines;
|
|
265
347
|
// M87 D2: every task may carry a section citation, AC-bearing or not — capture
|
|
266
348
|
// it regardless so non-behavioral coverage tasks still count toward citations.
|
|
267
349
|
const sectionCitation = parseSectionCitation(lines);
|
|
350
|
+
// M115 A7: every task may ALSO carry a plan-row citation, AC-bearing or not,
|
|
351
|
+
// captured unconditionally for the same reason.
|
|
352
|
+
const planRowCitation = parsePlanRowCitation(lines);
|
|
268
353
|
const hasAc = hasMultiField(lines, AC_FIELD_RE);
|
|
269
354
|
if (!hasAc) {
|
|
270
|
-
return { title: task.title, behavioral: false, violations: [], sectionCitation };
|
|
355
|
+
return { title: task.title, behavioral: false, violations: [], sectionCitation, planRowCitation };
|
|
271
356
|
}
|
|
272
357
|
|
|
273
358
|
// Underscore-preserving values for path/runner scans (Red Team recheck HIGH).
|
|
@@ -297,17 +382,33 @@ function assessTask(task) {
|
|
|
297
382
|
|
|
298
383
|
const isHeadline = lines.some((ln) => HEADLINE_FIELD_RE.test(_bare(ln)));
|
|
299
384
|
|
|
385
|
+
// M115 A7 — ADDITIVE ONLY: an acceptance line may ALSO clear via a plan-row
|
|
386
|
+
// binding. This arm fires ONLY when a citation is actually present AND it
|
|
387
|
+
// resolves to a real row in a real loaded plan — never as a fallback the
|
|
388
|
+
// old Files+Test path drops into when it comes up short. `opts.resolvePlanRow`
|
|
389
|
+
// is supplied by the caller (runGate) already knowing which plan docs are in
|
|
390
|
+
// scope; when no resolver is supplied (e.g. a bare assessTask() call, or a
|
|
391
|
+
// milestone with no test plan), planRowClears stays false and every existing
|
|
392
|
+
// violation kind fires exactly as it did before this change.
|
|
393
|
+
let planRowClears = false;
|
|
394
|
+
if (planRowCitation && !planRowCitation.malformed && typeof opts.resolvePlanRow === "function") {
|
|
395
|
+
planRowClears = !!opts.resolvePlanRow(planRowCitation);
|
|
396
|
+
}
|
|
397
|
+
|
|
300
398
|
const violations = [];
|
|
301
|
-
if (!hasFiles) {
|
|
399
|
+
if (!hasFiles && !planRowClears) {
|
|
302
400
|
violations.push({ kind: "ac-without-path", detail: "task declares acceptance criteria but no **Files** implementing path — an unbacked promise." });
|
|
303
401
|
}
|
|
304
|
-
if (!hasTest) {
|
|
402
|
+
if (!hasTest && !planRowClears) {
|
|
305
403
|
violations.push({ kind: "ac-without-test", detail: "task declares acceptance criteria but names no test (Test field, test path, or runner) — the dead-code class: it can pass vacuously / never be exercised." });
|
|
306
404
|
}
|
|
307
405
|
if (isHeadline && !hasImplPath) {
|
|
308
406
|
violations.push({ kind: "headline-without-impl", detail: "HEADLINE task has no non-test implementing path — the milestone's reason to exist is not bound to real code (the M5 AC-6 dead-code failure)." });
|
|
309
407
|
}
|
|
310
|
-
|
|
408
|
+
// A resolved plan row IS the headline's test (the suite is generated from the
|
|
409
|
+
// plan — contract §7.1); the impl-path rule above is untouched, a test row is
|
|
410
|
+
// not an implementation (Red Team M115 run 4, MEDIUM).
|
|
411
|
+
if (isHeadline && !hasTest && !planRowClears) {
|
|
311
412
|
violations.push({ kind: "headline-without-test", detail: "HEADLINE task has no test proving the milestone's core capability is delivered (the missing >100MB-fixture failure)." });
|
|
312
413
|
}
|
|
313
414
|
|
|
@@ -317,6 +418,8 @@ function assessTask(task) {
|
|
|
317
418
|
isHeadline,
|
|
318
419
|
hasFiles, hasTest, hasImplPath,
|
|
319
420
|
sectionCitation,
|
|
421
|
+
planRowCitation,
|
|
422
|
+
planRowClears,
|
|
320
423
|
violations,
|
|
321
424
|
};
|
|
322
425
|
}
|
|
@@ -518,6 +621,50 @@ function runGate({ projectDir = process.cwd(), milestone = null, tasksFile = nul
|
|
|
518
621
|
return { ok: false, exitCode: 64, milestone, reason: "no-tasks-files", tasks: [], violations: [] };
|
|
519
622
|
}
|
|
520
623
|
|
|
624
|
+
// M87 D2 dir resolution, hoisted ABOVE the task loop unchanged (M115 A7):
|
|
625
|
+
// this computation depends only on (projectDir, pseudocodeDir), never on
|
|
626
|
+
// taskResults, so moving it earlier changes nothing about what it resolves
|
|
627
|
+
// to — it only makes the resolved dir available to the plan-row resolver
|
|
628
|
+
// below. Containment: an explicit --pseudocode-dir must resolve INSIDE the
|
|
629
|
+
// project tree (the D2 test legitimately points it at test/fixtures/, also
|
|
630
|
+
// inside the project; an out-of-tree `../../evildocs` is refused). The
|
|
631
|
+
// default `.gsd-t/pseudocode/` is always in-tree. A refused dir → empty doc
|
|
632
|
+
// set (logged skip, never an out-of-tree read), same fail-closed shape as a
|
|
633
|
+
// missing dir.
|
|
634
|
+
let resolvedPcDir = pseudocodeDir || path.join(projectDir, ".gsd-t", "pseudocode");
|
|
635
|
+
if (pseudocodeDir) {
|
|
636
|
+
const resolvedProject = path.resolve(projectDir);
|
|
637
|
+
const candidate = path.resolve(resolvedProject, pseudocodeDir);
|
|
638
|
+
if (candidate !== resolvedProject && !candidate.startsWith(resolvedProject + path.sep)) {
|
|
639
|
+
resolvedPcDir = null; // out-of-tree → no docs loaded (fail-closed)
|
|
640
|
+
} else {
|
|
641
|
+
resolvedPcDir = candidate;
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
// M115 A7: resolve a plan-row citation against `.gsd-t/test-plans/` — the
|
|
646
|
+
// in-tree directory the front door writes plans to and the verify gate
|
|
647
|
+
// globs (contract §7). A milestone with no test plan (dir or doc simply
|
|
648
|
+
// absent) makes every citation resolve to false — planRowClears stays
|
|
649
|
+
// false and the task takes EXACTLY its pre-A7 path. Never a fallback: this
|
|
650
|
+
// only ever WIDENS what clears, it is not consulted unless a citation was
|
|
651
|
+
// actually written.
|
|
652
|
+
const testPlanDir = path.join(path.resolve(projectDir), ".gsd-t", "test-plans");
|
|
653
|
+
const planRowIdentityCache = new Map(); // docTitle -> Map<"table::seq", state>|null
|
|
654
|
+
function resolvePlanRow(citation) {
|
|
655
|
+
if (!citation.doc || !citation.table || !citation.seq) return false;
|
|
656
|
+
if (!planRowIdentityCache.has(citation.doc)) {
|
|
657
|
+
planRowIdentityCache.set(citation.doc, loadTestPlanRowIdentities(testPlanDir, citation.doc));
|
|
658
|
+
}
|
|
659
|
+
const identities = planRowIdentityCache.get(citation.doc);
|
|
660
|
+
if (!identities) return false;
|
|
661
|
+
const state = identities.get(`${citation.table}::${citation.seq}`);
|
|
662
|
+
// An open GAP row is the plan saying "nobody has answered this yet". Letting
|
|
663
|
+
// it clear an acceptance criterion would recreate the dead-deliverable shape
|
|
664
|
+
// this gate exists to catch (Red Team M115 HIGH).
|
|
665
|
+
return state === "sourced" || state === "decided";
|
|
666
|
+
}
|
|
667
|
+
|
|
521
668
|
const taskResults = [];
|
|
522
669
|
const violations = [];
|
|
523
670
|
let behavioralCount = 0;
|
|
@@ -525,7 +672,7 @@ function runGate({ projectDir = process.cwd(), milestone = null, tasksFile = nul
|
|
|
525
672
|
let md;
|
|
526
673
|
try { md = fs.readFileSync(f.tasksPath, "utf8"); } catch { continue; }
|
|
527
674
|
for (const t of parseTasks(md)) {
|
|
528
|
-
const r = assessTask(t);
|
|
675
|
+
const r = assessTask(t, { resolvePlanRow });
|
|
529
676
|
r.domain = f.domain;
|
|
530
677
|
taskResults.push(r);
|
|
531
678
|
if (r.behavioral) behavioralCount++;
|
|
@@ -536,21 +683,6 @@ function runGate({ projectDir = process.cwd(), milestone = null, tasksFile = nul
|
|
|
536
683
|
}
|
|
537
684
|
|
|
538
685
|
// M87 D2: section-citation coverage over the in-scope PseudoCode docs.
|
|
539
|
-
// Containment: an explicit --pseudocode-dir must resolve INSIDE the project
|
|
540
|
-
// tree (the D2 test legitimately points it at test/fixtures/, also inside the
|
|
541
|
-
// project; an out-of-tree `../../evildocs` is refused). The default
|
|
542
|
-
// `.gsd-t/pseudocode/` is always in-tree. A refused dir → empty doc set
|
|
543
|
-
// (logged skip, never an out-of-tree read), same fail-closed shape as a missing dir.
|
|
544
|
-
let resolvedPcDir = pseudocodeDir || path.join(projectDir, ".gsd-t", "pseudocode");
|
|
545
|
-
if (pseudocodeDir) {
|
|
546
|
-
const resolvedProject = path.resolve(projectDir);
|
|
547
|
-
const candidate = path.resolve(resolvedProject, pseudocodeDir);
|
|
548
|
-
if (candidate !== resolvedProject && !candidate.startsWith(resolvedProject + path.sep)) {
|
|
549
|
-
resolvedPcDir = null; // out-of-tree → no docs loaded (fail-closed)
|
|
550
|
-
} else {
|
|
551
|
-
resolvedPcDir = candidate;
|
|
552
|
-
}
|
|
553
|
-
}
|
|
554
686
|
const pseudocodeDocs = resolvedPcDir ? loadPseudocodeDocs(resolvedPcDir) : new Map();
|
|
555
687
|
const coverage = assessSectionCoverage(taskResults, pseudocodeDocs);
|
|
556
688
|
for (const v of coverage.violations) violations.push(v);
|
|
@@ -631,5 +763,6 @@ module.exports = {
|
|
|
631
763
|
runGate, parseTasks, assessTask, listTasksFiles,
|
|
632
764
|
parseSectionCitation, slugifyHeading, enumerateSections, docTitleFromFilename,
|
|
633
765
|
loadPseudocodeDocs, assessSectionCoverage,
|
|
766
|
+
parsePlanRowCitation, loadTestPlanRowIdentities,
|
|
634
767
|
_internal: { fieldValue, TEST_PATH_RE },
|
|
635
768
|
};
|
package/bin/gsd-t.js
CHANGED
|
@@ -1812,6 +1812,13 @@ const GLOBAL_BIN_TOOLS = [
|
|
|
1812
1812
|
// M109 — project-history reader + rule miner, used by /gsd-t-setup to write a
|
|
1813
1813
|
// project CLAUDE.md from what actually happened rather than from a template.
|
|
1814
1814
|
"gsd-t-project-history.cjs", "gsd-t-rule-mine.cjs",
|
|
1815
|
+
// M115 (contract test-plan-first-contract.md §5) — the A2 shape gate and the A5
|
|
1816
|
+
// non-convergence halt for the test-plan-first command. Both are dispatched by
|
|
1817
|
+
// `bin/gsd-t.js` ("testplan-lint" / "testplan-halt" cases) AND by the verify
|
|
1818
|
+
// workflow's gate wiring, so an omission here is the same propagation-gap class
|
|
1819
|
+
// caught four times before ([[project_global_bin_propagation_gap]]). Also in
|
|
1820
|
+
// PROJECT_BIN_TOOLS below — both lists, both tools.
|
|
1821
|
+
"gsd-t-testplan-lint.cjs", "gsd-t-testplan-halt.cjs", "gsd-t-testplan-rows.cjs",
|
|
1815
1822
|
];
|
|
1816
1823
|
|
|
1817
1824
|
// Directories under bin/ that must ship whole. A runner whose parts stay behind
|
|
@@ -3574,6 +3581,11 @@ const PROJECT_BIN_TOOLS = [
|
|
|
3574
3581
|
// Plus the verify-gate lint (gsd-t-verify-gate.cjs invokes it via __dirname,
|
|
3575
3582
|
// so it must sit alongside the gate in the project's bin/).
|
|
3576
3583
|
"gsd-t-doc-marker.cjs", "gsd-t-env-registry.cjs", "gsd-t-env-registry-check.cjs",
|
|
3584
|
+
// M115 (contract test-plan-first-contract.md §5) — the A2 shape gate and the A5
|
|
3585
|
+
// non-convergence halt. The verify workflow's project-local runCli resolves these,
|
|
3586
|
+
// and /gsd-t-test-plan calls them directly — same propagation-gap class as the
|
|
3587
|
+
// graph tools above. Also in GLOBAL_BIN_TOOLS — both lists, both tools.
|
|
3588
|
+
"gsd-t-testplan-lint.cjs", "gsd-t-testplan-halt.cjs", "gsd-t-testplan-rows.cjs",
|
|
3577
3589
|
];
|
|
3578
3590
|
|
|
3579
3591
|
// Files that older versions of this installer copied into project bin/ but
|
|
@@ -5999,6 +6011,36 @@ if (require.main === module) {
|
|
|
5999
6011
|
});
|
|
6000
6012
|
process.exit(res.status == null ? 1 : res.status);
|
|
6001
6013
|
}
|
|
6014
|
+
case "testplan-lint": {
|
|
6015
|
+
// M115 (contract §5) — `gsd-t testplan-lint (--doc <f> | --dir <d>)` thin dispatcher
|
|
6016
|
+
// to the A2 test-plan shape gate (owner: deterministic-gates). Sibling of
|
|
6017
|
+
// `pseudocode-style` — same proven shape (0 clean / 4 violations / 64 bad input).
|
|
6018
|
+
const { spawnSync } = require("child_process");
|
|
6019
|
+
const js = path.join(__dirname, "gsd-t-testplan-lint.cjs");
|
|
6020
|
+
if (!require("node:fs").existsSync(js)) {
|
|
6021
|
+
error(`gsd-t-testplan-lint.cjs not found at ${js} — reinstall GSD-T (contract test-plan-first-contract.md §5)`);
|
|
6022
|
+
process.exit(1);
|
|
6023
|
+
}
|
|
6024
|
+
const res = spawnSync(process.execPath, [js, ...args.slice(1)], {
|
|
6025
|
+
stdio: "inherit",
|
|
6026
|
+
});
|
|
6027
|
+
process.exit(res.status == null ? 1 : res.status);
|
|
6028
|
+
}
|
|
6029
|
+
case "testplan-halt": {
|
|
6030
|
+
// M115 (contract §5) — `gsd-t testplan-halt check --doc <f> --round <n>` thin
|
|
6031
|
+
// dispatcher to the A5 non-convergence halt (owner: halt-convergence). Reuses
|
|
6032
|
+
// bin/gsd-t-loop-ledger.cjs read-only for the repeated-symptom cap.
|
|
6033
|
+
const { spawnSync } = require("child_process");
|
|
6034
|
+
const js = path.join(__dirname, "gsd-t-testplan-halt.cjs");
|
|
6035
|
+
if (!require("node:fs").existsSync(js)) {
|
|
6036
|
+
error(`gsd-t-testplan-halt.cjs not found at ${js} — reinstall GSD-T (contract test-plan-first-contract.md §5)`);
|
|
6037
|
+
process.exit(1);
|
|
6038
|
+
}
|
|
6039
|
+
const res = spawnSync(process.execPath, [js, ...args.slice(1)], {
|
|
6040
|
+
stdio: "inherit",
|
|
6041
|
+
});
|
|
6042
|
+
process.exit(res.status == null ? 1 : res.status);
|
|
6043
|
+
}
|
|
6002
6044
|
case "archive-domains": {
|
|
6003
6045
|
// Backlog #40 — `gsd-t archive-domains --domains a,b --archive <dir>` deterministic
|
|
6004
6046
|
// archive+sweep of a completed milestone's domain dirs (complete-milestone Step 7).
|
package/commands/cpua.md
CHANGED
|
@@ -115,11 +115,29 @@ Verify the output shows `+ @tekyzinc/gsd-t@{NEW_VERSION}`. If publish fails:
|
|
|
115
115
|
## Step 6: Update global install + propagate to projects
|
|
116
116
|
|
|
117
117
|
```bash
|
|
118
|
-
npm
|
|
118
|
+
G="$(npm root -g)/@tekyzinc/gsd-t"
|
|
119
|
+
# 1. Install by TARBALL URL, not by version spec. Right after `npm publish`, npm keeps
|
|
120
|
+
# the pre-publish package listing cached and `@tekyzinc/gsd-t@{NEW}` returns ETARGET
|
|
121
|
+
# for minutes while `npm view` already shows the version (v5.17.14, 2026-09-03).
|
|
122
|
+
# The tarball URL bypasses the cached listing and is deterministic.
|
|
123
|
+
npm install -g "https://registry.npmjs.org/@tekyzinc/gsd-t/-/gsd-t-{NEW_VERSION}.tgz"
|
|
124
|
+
# 2. VERIFY ON DISK — never trust npm's exit code for this step. On v5.17.13 `npm install -g`
|
|
125
|
+
# reported success and left the OLD version in place; `update-all` then ran from the
|
|
126
|
+
# stale global, told 32 projects they were "already current", and overwrote a
|
|
127
|
+
# hand-patched project file with the old build.
|
|
128
|
+
ON_DISK=$(node -p "require('$G/package.json').version")
|
|
129
|
+
if [ "$ON_DISK" != "{NEW_VERSION}" ]; then
|
|
130
|
+
echo "global install is $ON_DISK, expected {NEW_VERSION} — removing and reinstalling"
|
|
131
|
+
npm uninstall -g @tekyzinc/gsd-t; rm -rf "$G"; npm cache clean --force
|
|
132
|
+
npm install -g "https://registry.npmjs.org/@tekyzinc/gsd-t/-/gsd-t-{NEW_VERSION}.tgz"
|
|
133
|
+
ON_DISK=$(node -p "require('$G/package.json').version")
|
|
134
|
+
[ "$ON_DISK" = "{NEW_VERSION}" ] || { echo "HALT: global install still $ON_DISK"; exit 1; }
|
|
135
|
+
fi
|
|
136
|
+
# 3. Only now propagate.
|
|
119
137
|
gsd-t update-all 2>&1 | tail -30
|
|
120
138
|
```
|
|
121
139
|
|
|
122
|
-
|
|
140
|
+
Never `npm update -g` (npm rejects it for legacy version strings like `3.19.00`). After `update-all`, prove propagation by `ls`/`grep` of one NEW or CHANGED file in a real registered project — the "copied N tool(s)" line is a report, not proof. Note: `update-all` also overwrites `~/.claude/commands/*.md` from the package, so THIS file's source of truth is `commands/cpua.md` in the GSD-T repo; an edit made only in `~/.claude/commands/` is lost on the next propagation.
|
|
123
141
|
|
|
124
142
|
## Step 7: Report
|
|
125
143
|
|
package/commands/gsd-t-help.md
CHANGED
|
@@ -31,6 +31,7 @@ MILESTONE WORKFLOW [auto] = in wave
|
|
|
31
31
|
milestone Define a new milestone
|
|
32
32
|
partition [auto] Decompose milestone into domains + contracts
|
|
33
33
|
plan [auto] Create atomic task lists per domain
|
|
34
|
+
test-plan Enumerate the case space from requirements, before any code/tests exist
|
|
34
35
|
impact [auto] Analyze downstream effects before execution
|
|
35
36
|
execute [auto] Run tasks (solo or team mode)
|
|
36
37
|
test-sync [auto] Sync tests with code changes
|
|
@@ -235,6 +236,14 @@ Use these when user asks for help on a specific command:
|
|
|
235
236
|
- **Note (M26)**: Pre-mortem step now also reads rules.jsonl for historical failure patterns via getPreMortemRules
|
|
236
237
|
- **Note (M38)**: Conversational use cases (formerly `/gsd-t-prompt`, `/gsd-t-brainstorm`, `/gsd-t-discuss`) are now handled by the Smart Router's conversational mode — just describe what you want via `/gsd` or plain text.
|
|
237
238
|
|
|
239
|
+
### test-plan
|
|
240
|
+
- **Summary**: Enumerate every test case a requirements document implies, BEFORE any code or tests exist — an unfillable row is surfaced as a missing or wrong requirement, not smoothed over.
|
|
241
|
+
- **Auto-invoked**: No (standalone, on-demand; runs in-session like `architect`)
|
|
242
|
+
- **Args**: `/gsd-t-test-plan [requirements path]` (before-mode, default) or `/gsd-t-test-plan --after` (re-enumerate against built code, classify failures by cited evidence)
|
|
243
|
+
- **Creates**: `.gsd-t/test-plans/TestPlan-[FeatureArea].md`
|
|
244
|
+
- **Use when**: A requirements document exists (or a milestone is about to be planned) and you want its case space interrogated for gaps before any test is written; or after a milestone is built, to classify test failures as code-bug vs. wrong-requirement from cited evidence only.
|
|
245
|
+
- **Note (M115)**: Every open row is batched into ONE question round — never a drip. Three rounds without closure, or the same failure signature twice running, HALTs via `gsd-t testplan-halt` rather than filling an open row with anything plausible. `gsd-t testplan-lint` gates the plan's shape before it is presented, and again as a FAIL-blocking `verify` gate.
|
|
246
|
+
|
|
238
247
|
### impact
|
|
239
248
|
- **Summary**: Analyze downstream effects of planned changes
|
|
240
249
|
- **Auto-invoked**: Yes (in wave, between plan and execute)
|
|
@@ -4,6 +4,7 @@ Scaffolds the two framework-default logging streams — **trace** (transient deb
|
|
|
4
4
|
|
|
5
5
|
## What this does
|
|
6
6
|
|
|
7
|
+
- **Declared streams are skipped.** If `.gsd-t/logging-manifest.json` names a stream's `module` (e.g. `{ "audit": { "module": "server/src/audit.ts", "store": { "kind": "postgres", "table": "tb_audit_log" }, "retention": "indefinite" } }`), that stream already exists and is NOT scaffolded — reported under `declared`. This is how a project with a real audit table at a path the checker does not guess adds only the missing trace stream, with no second audit module written beside the first. An unreadable manifest is an error, not "no manifest".
|
|
7
8
|
- Copies the trace module template (`templates/logging/trace-module.template.ts`) to `src/logging/trace.ts` — **only if that file does not already exist**.
|
|
8
9
|
- Copies the audit module template (`templates/logging/audit-module.template.ts`) to `src/logging/audit.ts` — **only if that file does not already exist**.
|
|
9
10
|
- Distills the per-project trace category / audit action schema from the project's own plan (when `--plan` is given) into `.gsd-t/logging-schema.json` — never confabulated; an unstated category/action is a gap, not a guess.
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# GSD-T: Test-Plan-First — Enumerate the Case Space Before Tests Exist
|
|
2
|
+
|
|
3
|
+
You run the M115 test-plan-first interrogation, in-session, as the lead agent — the same
|
|
4
|
+
pattern as `/gsd-t-architect`. There is no separate Workflow phase script for this command:
|
|
5
|
+
every deliverable it orchestrates is a deterministic gate (`bin/gsd-t-testplan-lint.cjs`,
|
|
6
|
+
`bin/gsd-t-testplan-halt.cjs`) or a subagent protocol file you Read and follow directly, per
|
|
7
|
+
`.gsd-t/contracts/test-plan-first-contract.md`.
|
|
8
|
+
|
|
9
|
+
**Why this exists.** Every check GSD-T runs today reads something that already exists (code,
|
|
10
|
+
a plan) and can only judge what was built. This runs BEFORE any code exists: it enumerates
|
|
11
|
+
every case the requirements already imply, so a missing or wrong requirement is caught as a
|
|
12
|
+
gap in a row, not as a bug found later.
|
|
13
|
+
|
|
14
|
+
## Argument Parsing
|
|
15
|
+
|
|
16
|
+
Parse `$ARGUMENTS`:
|
|
17
|
+
- **No flag (before-mode, default)** — enumerate from what is already held (requirements,
|
|
18
|
+
architecture, contracts, standing rules, any code that exists), before tests are written.
|
|
19
|
+
- **`--after`** — the same enumeration run against already-built code: run the plan's rows
|
|
20
|
+
as tests, classify each failure by cited evidence.
|
|
21
|
+
- **First positional** (either mode) — the requirements document or feature-area slice to
|
|
22
|
+
enumerate. Defaults to `docs/requirements.md`.
|
|
23
|
+
|
|
24
|
+
## Before-mode (default)
|
|
25
|
+
|
|
26
|
+
0. **Where the plan lives.** Write the plan to `.gsd-t/test-plans/TestPlan-[FeatureArea].md`
|
|
27
|
+
— the sibling location of `.gsd-t/pseudocode/PseudoCode-[Title].md`, one stage earlier in
|
|
28
|
+
the pipeline. This is what the verify workflow's gate globs.
|
|
29
|
+
1. **Read the enumeration protocol.** `Read templates/prompts/test-plan-enumerator-subagent.md`
|
|
30
|
+
in full — the E1-E8 rules that define the case space. Do not restate it here; follow it.
|
|
31
|
+
2. **Read the mold.** `Read templates/TestPlan-spec.md` — the section set and six-column
|
|
32
|
+
row schema a plan must have (frozen shape: `.gsd-t/contracts/test-plan-first-contract.md`
|
|
33
|
+
§2-§3).
|
|
34
|
+
3. **Enumerate.** Apply E1-E8 to the target requirements document. Fill each row from named
|
|
35
|
+
evidence (`sourced`), or mark it `DECIDED-WITHOUT-YOU` (self-answered — also grouped
|
|
36
|
+
under `## Decided without you`), or leave it `GAP`/`GAP:CONTRADICTION` (open). Never fill
|
|
37
|
+
an unanswerable row with something plausible — that is the one failure this command
|
|
38
|
+
exists to prevent.
|
|
39
|
+
4. **Batch every open row into ONE question round (A4).** Collect every `GAP`/
|
|
40
|
+
`GAP:CONTRADICTION` row from every table, ask them together in a single round, fold the
|
|
41
|
+
answers in together, then re-enumerate to confirm closure. **Never ask one question at a
|
|
42
|
+
time as each answer arrives — a drip defeats the batching the reviewer's attention
|
|
43
|
+
depends on.**
|
|
44
|
+
5. **Round cap: three rounds, never a fourth.** After folding each round's answers,
|
|
45
|
+
re-enumerate and run `gsd-t testplan-halt check --doc <plan path> --round <n>` (falls
|
|
46
|
+
back to `node bin/gsd-t-testplan-halt.cjs check ...` locally). If it reports
|
|
47
|
+
`halted: true` — either round three still has open rows, or the same failure signature
|
|
48
|
+
has recurred across two consecutive rounds — STOP: hand back `blocked-needs-human`
|
|
49
|
+
naming every row the tool lists as never settled, rather than continuing with anything
|
|
50
|
+
left open. A repeated symptom means the belief behind an earlier answer is wrong, not
|
|
51
|
+
that the row needs a third try.
|
|
52
|
+
6. **Fold closed answers into `docs/requirements.md`.**
|
|
53
|
+
7. **Gate for shape.** Run `gsd-t testplan-lint --doc <plan path> --json` (falls back to
|
|
54
|
+
`node bin/gsd-t-testplan-lint.cjs --doc <plan path> --json` locally). A non-zero exit
|
|
55
|
+
means the plan is malformed — fix it before presenting; never present a plan that fails
|
|
56
|
+
its own gate.
|
|
57
|
+
8. **Present for sign-off.** Show the plan (or a summary with the file path) and stop for
|
|
58
|
+
the user's review. **Tests are generated from the rows only after sign-off** — this
|
|
59
|
+
command does not write tests itself.
|
|
60
|
+
|
|
61
|
+
## `--after` mode
|
|
62
|
+
|
|
63
|
+
1. Read the same enumerator protocol and re-enumerate against the requirements as they
|
|
64
|
+
stand today, against the built code.
|
|
65
|
+
2. Run the resulting rows as tests against the code.
|
|
66
|
+
3. For each failure, `Read templates/prompts/test-plan-evidence-classifier.md` and follow it
|
|
67
|
+
to classify the failure as code-bug or wrong-requirement, from cited evidence only —
|
|
68
|
+
never a default arm.
|
|
69
|
+
4. Fix code-bugs directly (small, local). A wider refactor a classification surfaces is
|
|
70
|
+
spilled to `.gsd-t/techdebt.md` rather than done in this pass.
|
|
71
|
+
5. Re-run `gsd-t testplan-lint --doc <plan path> --json` on the updated plan before
|
|
72
|
+
presenting the result.
|
|
73
|
+
|
|
74
|
+
## Document Ripple
|
|
75
|
+
|
|
76
|
+
Fold closed answers into `docs/requirements.md`. Log a Decision Log entry in
|
|
77
|
+
`.gsd-t/progress.md` for the enumeration round(s) run and their outcome. A wider refactor
|
|
78
|
+
found in `--after` mode goes to `.gsd-t/techdebt.md`, not into this pass's diff.
|
|
79
|
+
|
|
80
|
+
## Next Up
|
|
81
|
+
|
|
82
|
+
`/gsd-t-plan` — turn the signed-off plan's rows into tasks.
|
|
83
|
+
|
|
84
|
+
**Also available:**
|
|
85
|
+
- `/gsd-t-execute` — when run in `--after` mode against a milestone already mid-build.
|
package/commands/gsd.md
CHANGED
|
@@ -88,6 +88,7 @@ When the same request could fit multiple commands at different scales:
|
|
|
88
88
|
- **Requires its own milestone with domains** → `milestone` or `project`
|
|
89
89
|
- **Needs investigation before fixing** → `debug` (not `quick`)
|
|
90
90
|
- **Spec/requirements to verify against code** → `gap-analysis` (not `scan`)
|
|
91
|
+
- **Enumerate every test case a requirements doc implies, BEFORE any code or tests exist** → `test-plan` (not `plan` — `plan` writes task lists from an already-partitioned milestone; `test-plan` interrogates the requirements themselves for gaps)
|
|
91
92
|
|
|
92
93
|
### Design-to-code routing:
|
|
93
94
|
|
|
@@ -179,7 +180,7 @@ Where `{last-command}` is:
|
|
|
179
180
|
|
|
180
181
|
**CRITICAL: `{command}` and `{last-command}` MUST be a real GSD-T command slug — never a free-form description.**
|
|
181
182
|
|
|
182
|
-
Valid command slugs: `quick`, `debug`, `feature`, `execute`, `milestone`, `project`, `scan`, `gap-analysis`, `plan`, `partition`, `impact`, `integrate`, `verify`, `test-sync`, `complete-milestone`, `wave`, `status`, `populate`, `setup`, `init`, `health`, `log`, `pause`, `resume`, `prd`, `backlog-add`, `backlog-list`, `backlog-promote`, `promote-debt`, `triage-and-merge`, `version-update`, `version-update-all`, `design-decompose`, `design-build`, `design-audit`, `design-review`
|
|
183
|
+
Valid command slugs: `quick`, `debug`, `feature`, `execute`, `milestone`, `project`, `scan`, `gap-analysis`, `plan`, `partition`, `impact`, `integrate`, `verify`, `test-sync`, `test-plan`, `complete-milestone`, `wave`, `status`, `populate`, `setup`, `init`, `health`, `log`, `pause`, `resume`, `prd`, `backlog-add`, `backlog-list`, `backlog-promote`, `promote-debt`, `triage-and-merge`, `version-update`, `version-update-all`, `design-decompose`, `design-build`, `design-audit`, `design-review`
|
|
183
184
|
|
|
184
185
|
**WRONG ❌** — do not do this:
|
|
185
186
|
```
|
package/docs/requirements.md
CHANGED
|
@@ -1001,6 +1001,24 @@ Contract: `.gsd-t/contracts/unproven-assumption-doctrine-contract.md` v1.0.0 STA
|
|
|
1001
1001
|
| REQ-M90-06 (SC-SELF-OBEDIENCE) | M90's own artifacts show the doctrine applied to itself: discuss produced a sourced approach, pseudocode signed off before code, premises re-verified on disk, and any ≥3-cycle same-signature non-convergence triggered a recorded premise re-examination (not a variant patch). Every §6 [RULE] traces to an enforcement point (orphan rule FAILS the guard-map test). | m90-d-contract-doctrine-integrate | D4-T1/T7 | complete (M90) |
|
|
1002
1002
|
| REQ-M90-07 (SC-RESEARCH-GATE) | Plan is mechanically blocked until discuss emits a sourced approach with ≥1 external citation per detection mechanism. | m90-d-contract-doctrine-integrate (doctrine contract) | D4-T1 | satisfied (discuss `.gsd-t/discuss/M90-approach-sourced.md`) |
|
|
1003
1003
|
|
|
1004
|
+
## M115 Test-Plan-First Requirements Interrogation (complete - v5.18.10)
|
|
1005
|
+
|
|
1006
|
+
Before any code exists, enumerate every test case a milestone's requirements imply into a reviewable sequence-table document; every row nobody can fill in is a requirements gap, surfaced by trying to write the row rather than by reading code. Origin: the TimeTracking v1.27 rate-ledger review (2026-09-01/02), whose test plan found three unwritten requirements.
|
|
1007
|
+
|
|
1008
|
+
Contract: `.gsd-t/contracts/test-plan-first-contract.md` v1.1.0 STABLE · Pseudocode: `.gsd-t/pseudocode/PseudoCode-TestPlanFirst.md` · Command: `/gsd-t-test-plan`
|
|
1009
|
+
|
|
1010
|
+
| ID | Requirement | Domain | Tests | Status |
|
|
1011
|
+
|----|-------------|--------|-------|--------|
|
|
1012
|
+
| REQ-M115-A1 (BLIND-REPLAY) | Cold enumeration from the pre-review requirements alone (generic protocol, no answer-key material, memory-free context) surfaces the three gaps the human review found; scored against pre-registered hit conditions held out from the enumerator. Condition 1 re-written to the gap's shape (rate changes silently alter issued invoices) with a recorded ⚠ Divergence, David-approved 2026-09-03. | enumerator-core | `test/m115-a1-blind-replay.test.js` (6) over `.gsd-t/scan/m115-cold-enumeration-blind-scoped.md` | **complete 2026-09-03** |
|
|
1013
|
+
| REQ-M115-A2 (SHAPE-LINT) | `gsd-t testplan-lint (--doc \| --dir)` exits 0 clean / 4 violations / 64 bad input over the six-column mold; unknown markers, blank Seq, blank source, wrong width, duplicate identities, fenced fakes are violations; mandatory negative tests | deterministic-gates | `test/m115-a2-testplan-lint.test.js` (14), `test/m115-verify-fixes.test.js` | **complete 2026-09-03** |
|
|
1014
|
+
| REQ-M115-A3 (SELF-ANSWER-VISIBLE) | Every DECIDED-WITHOUT-YOU row is grouped under one `## Decided without you` heading exactly matching the rows, fence-aware, so a reviewer can overrule any at a glance | plan-visibility | `test/m115-a3-self-answer-visibility.test.js` (6) | **complete 2026-09-03** |
|
|
1015
|
+
| REQ-M115-A4 (ONE-ROUND) | Open rows are asked in ONE batched question round per loop, never one at a time | plan-visibility / front-door-wiring | `commands/gsd-t-test-plan.md` protocol; `test/m115-a8-front-door-test-plan.test.js` | **complete 2026-09-03** |
|
|
1016
|
+
| REQ-M115-A5 (NON-CONVERGENCE-HALT) | Three question rounds with rows still open, or the same open-row set twice running, HALTS with `blocked-needs-human` naming every open row; rounds are counted per plan and per round (idempotent re-check), through `gsd-t-loop-ledger.cjs` read-only | halt-convergence | `test/m115-a5-non-convergence-halt.test.js` (13), `test/m115-verify-fixes.test.js` | **complete 2026-09-03** |
|
|
1017
|
+
| REQ-M115-A6 (EVIDENCE-OR-HALT) | In `--after` mode every failing case is classified code-bug / wrong-requirement / cannot-tell from cited evidence — three arms, no default branch; cannot-tell joins the question round | plan-visibility | `test/m115-a6-evidence-or-halt.test.js` (21) | **complete 2026-09-03** |
|
|
1018
|
+
| REQ-M115-A7 (PLAN-ROW-BINDING) | An acceptance criterion may bind to `**Plan-Row**: <Plan>#<Table>/Seq-<n>` in `.gsd-t/test-plans/`; only a sourced or decided row of exactly six cells with a unique identity clears; GAP, malformed, duplicate or escaped-path rows never do; M83/M87 behaviour byte-identical | deterministic-gates | `test/m115-a7-traceability-plan-row.test.js` (17), `test/m83-*.test.js`, `test/m87-*.test.js` unchanged | **complete 2026-09-03** |
|
|
1019
|
+
| REQ-M115-A8 (FRONT-DOOR) | `/gsd-t-test-plan` is reachable: command file, `/gsd` router case, both bin-tool registries, CLI dispatch, verify-gate wiring with a NAMED skip distinguishable from a clean run; the front-door test was RED before wiring and GREEN after | front-door-wiring | `test/m115-a8-front-door-test-plan.test.js` (15) | **complete 2026-09-03** |
|
|
1020
|
+
| REQ-M115-R1 (ONE-READER) | Every consumer of a test plan (lint, halt, traceability gate) reads sections and rows through `bin/gsd-t-testplan-rows.cjs` — both fence styles, exact six-cell width, one classifier, one heading pattern — so the three cannot drift apart again | verify (7 runs) | `test/m115-verify-fixes.test.js` (29), `test/verify-gate-tools-propagated.test.js` (sibling requires ship) | **complete 2026-09-03** |
|
|
1021
|
+
|
|
1004
1022
|
## Updated Functional Requirements (scan findings - v4.0.27)
|
|
1005
1023
|
|
|
1006
1024
|
The deep scan identified functional deficiencies not captured in previous requirements. These are recorded here for tracking:
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tekyzinc/gsd-t",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.18.10",
|
|
4
4
|
"description": "GSD-T: Contract-Driven Development for Claude Code — 54 slash commands with headless-by-default workflow spawning, unattended supervisor relay with event stream, graph-powered code analysis, real-time agent dashboard, task telemetry, doc-ripple enforcement, backlog management, impact analysis, test sync, milestone archival, and PRD generation",
|
|
5
5
|
"author": "Tekyz, Inc.",
|
|
6
6
|
"license": "MIT",
|
|
@@ -256,6 +256,7 @@ Every GSD-T project gets TWO logging streams scaffolded by default at `gsd-t-ini
|
|
|
256
256
|
- **Storage is stack-adaptive and human-approval-gated** — `bin/gsd-t-logging-scaffolder.cjs` detects the stack, presents real alternatives, and STOPS for approval; it never silently picks a backend (the one sanctioned pause against the Level-3 full-auto default).
|
|
257
257
|
- **Trace and audit NEVER collapse into one stream.** A trace envelope carrying audit markers (`before`/`after`/`actor`/`action`) or vice-versa is a contract violation and a `gsd-t-verify` FAIL — see each contract's §no-collapse boundary.
|
|
258
258
|
- **Brownfield migration**: `gsd-t migrate-logging <projectDir>` scaffolds both streams into an EXISTING project additively — it never modifies or deletes a pre-existing file. See `commands/gsd-t-migrate-logging.md`.
|
|
259
|
+
- **Streams that already exist somewhere the checker does not guess** (an audit table behind `server/src/audit.ts`, say) are DECLARED in `.gsd-t/logging-manifest.json` — `{ "audit": { "module": "server/src/audit.ts", "store": { "kind": "postgres", "table": "tb_audit_log" }, "retention": "indefinite" } }`. The declaration is checked (a path that does not exist FAILs), an external store's rows are not inspected offline (reported in `notes`, the module surface is the enforced evidence), and `migrate-logging` skips a declared stream so it never scaffolds a twin.
|
|
259
260
|
|
|
260
261
|
## Orthogonal Validation Triad (Mandatory)
|
|
261
262
|
|
|
@@ -607,6 +608,7 @@ Add `**Also available:**` with `- /gsd-t-{alt} — {desc}` lines if alternatives
|
|
|
607
608
|
| `partition` | `plan` | `discuss` (if complex) |
|
|
608
609
|
| `discuss` | `plan` | |
|
|
609
610
|
| `plan` | `execute` | `impact` (if risky) |
|
|
611
|
+
| `test-plan` | `plan` | `execute` (if `--after` mode against a milestone already mid-build) |
|
|
610
612
|
| `impact` | `execute` | |
|
|
611
613
|
| `execute` | `test-sync` | |
|
|
612
614
|
| `test-sync` | `verify` | `integrate` (if multi-domain) |
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# TestPlan-{Title}
|
|
2
|
+
|
|
3
|
+
**Project:** {Project Name} · **Date:** {Date}
|
|
4
|
+
|
|
5
|
+
One sentence: what feature area this plan enumerates, and which requirements document it
|
|
6
|
+
was enumerated from.
|
|
7
|
+
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
<!--
|
|
11
|
+
─────────────────────────────────────────────────────────────────────────────
|
|
12
|
+
HOW TO WRITE THIS (delete this comment block in the real instance)
|
|
13
|
+
─────────────────────────────────────────────────────────────────────────────
|
|
14
|
+
|
|
15
|
+
This is the blank mold every test plan (a document that enumerates every
|
|
16
|
+
case a feature area implies, so a reviewer approves DIRECTION before
|
|
17
|
+
TESTS exist) is filled from. It is the sibling of `PseudoCode-spec.md` —
|
|
18
|
+
same job, one section earlier in the pipeline: PseudoCode maps intended
|
|
19
|
+
BEHAVIOR before code; this maps intended CASES before tests.
|
|
20
|
+
|
|
21
|
+
Full row schema, marker literals, and self-answered-visibility rule:
|
|
22
|
+
`.gsd-t/contracts/test-plan-first-contract.md` §2–§3 (frozen — do not
|
|
23
|
+
restate or diverge from it here; this file shows the shape in place).
|
|
24
|
+
|
|
25
|
+
Section order below is FIXED. A plan missing any of the five, or with
|
|
26
|
+
them out of order, does not satisfy the contract's row schema.
|
|
27
|
+
|
|
28
|
+
Name the real file `TestPlan-[FeatureArea].md` — never a milestone id —
|
|
29
|
+
the way a PseudoCode doc is named for its subject, not its milestone.
|
|
30
|
+
─────────────────────────────────────────────────────────────────────────────
|
|
31
|
+
-->
|
|
32
|
+
|
|
33
|
+
## Decided without you
|
|
34
|
+
|
|
35
|
+
Every row anywhere in this document whose `Source` column carries the marker
|
|
36
|
+
`DECIDED-WITHOUT-YOU` is copied here a second time, so a reviewer can overrule any of them
|
|
37
|
+
by reading this one group and nowhere else. Present always — even with nothing under it.
|
|
38
|
+
|
|
39
|
+
- `{table name}` Seq `{n}` — {the decision made} — evidence: {what was used to decide it}
|
|
40
|
+
|
|
41
|
+
When there are none:
|
|
42
|
+
|
|
43
|
+
> None — every row is sourced.
|
|
44
|
+
|
|
45
|
+
---
|
|
46
|
+
|
|
47
|
+
## Table: {Feature or Capability Name}
|
|
48
|
+
|
|
49
|
+
| Seq | Setup / date | Action | Expected result | Effect on saved data | Source |
|
|
50
|
+
|---|---|---|---|---|---|
|
|
51
|
+
| 1 | {system state and date before the action} | {the one thing done} | {what must happen, stated so a test can fail it} | {what changes in data already stored — `none` is a real, written answer} | `docs/requirements.md#{anchor}` |
|
|
52
|
+
| 2 | {system state and date before the action} | {the one thing done} | {what must happen} | {effect, or `none`} | DECIDED-WITHOUT-YOU — {evidence used} |
|
|
53
|
+
| 3 | {system state and date before the action} | {the one thing done} | {what must happen} | {effect, or `none`} | GAP — {why it could not be filled} |
|
|
54
|
+
|
|
55
|
+
Add one `## Table:` section per coherent sub-area within this feature. Each keeps its own
|
|
56
|
+
`Seq` numbering starting at 1.
|
|
57
|
+
|
|
58
|
+
---
|
|
59
|
+
|
|
60
|
+
## Open gaps
|
|
61
|
+
|
|
62
|
+
Every `GAP` (and `GAP:CONTRADICTION`) row across every table, collected here in one list.
|
|
63
|
+
This is what the single question round is built from — a reviewer answers this list, not
|
|
64
|
+
the tables.
|
|
65
|
+
|
|
66
|
+
- `{table name}` Seq `{n}` — {why it could not be filled}
|
|
67
|
+
|
|
68
|
+
When there are none:
|
|
69
|
+
|
|
70
|
+
> None — every row is answered.
|
|
71
|
+
|
|
72
|
+
---
|
|
73
|
+
|
|
74
|
+
## Sign-off
|
|
75
|
+
|
|
76
|
+
| Signed by | Date |
|
|
77
|
+
|---|---|
|
|
78
|
+
| {name} | {Date} |
|
|
@@ -28,8 +28,14 @@ const FF = (() => {
|
|
|
28
28
|
try {
|
|
29
29
|
execFileSync(full, ['-version'], { stdio: 'ignore' });
|
|
30
30
|
return full;
|
|
31
|
-
} catch {
|
|
32
|
-
|
|
31
|
+
} catch (err) {
|
|
32
|
+
// Installed but broken (a Homebrew upgrade left a shared library missing).
|
|
33
|
+
// Rendering with a different binary would change the output silently —
|
|
34
|
+
// ffmpeg-full carries filters the plain build lacks. Halt with the fix.
|
|
35
|
+
throw new Error(
|
|
36
|
+
`ffmpeg-full is installed at ${full} but does not run (${String(err).slice(0, 120)}). ` +
|
|
37
|
+
'Fix: brew reinstall ffmpeg-full',
|
|
38
|
+
);
|
|
33
39
|
}
|
|
34
40
|
}
|
|
35
41
|
return 'ffmpeg';
|