@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
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,53 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to GSD-T are documented here. Updated with each release.
|
|
4
4
|
|
|
5
|
+
## [5.18.10] - 2026-09-03
|
|
6
|
+
|
|
7
|
+
### Added — M115 Test-Plan-First Requirements Interrogation (`/gsd-t-test-plan`)
|
|
8
|
+
|
|
9
|
+
Before any code exists, enumerate every test case a milestone's requirements imply into a
|
|
10
|
+
reviewable sequence-table document. Every row nobody can fill in is a requirements gap.
|
|
11
|
+
|
|
12
|
+
- `commands/gsd-t-test-plan.md` + `/gsd` router case: the front door (in-session, judgement-driven).
|
|
13
|
+
- `templates/prompts/test-plan-enumerator-subagent.md`: the eight enumeration rules (E1-E8), generic
|
|
14
|
+
worked examples; `templates/TestPlan-spec.md`: the mold; `templates/prompts/test-plan-evidence-classifier.md`:
|
|
15
|
+
the `--after` classifier (three arms, no default).
|
|
16
|
+
- `bin/gsd-t-testplan-rows.cjs`: the ONE plan reader (both fence styles, exact six-cell rows, one classifier).
|
|
17
|
+
- `bin/gsd-t-testplan-lint.cjs` (0/4/64), `bin/gsd-t-testplan-halt.cjs` (three-round cap + repeated-symptom
|
|
18
|
+
cap over the loop ledger, per plan and per round), `bin/gsd-t-traceability-gate.cjs` (additive `**Plan-Row**`
|
|
19
|
+
binding; GAP / malformed / duplicate / escaped rows never clear).
|
|
20
|
+
- Verify-gate wiring with a NAMED skip when no plan exists; discovery failure halts.
|
|
21
|
+
- Contract `.gsd-t/contracts/test-plan-first-contract.md` 1.1.0 STABLE; `integration-points.md`.
|
|
22
|
+
- Held-out fixture `test/fixtures/m115-blind-replay/` (the TimeTracking rate-ledger answer key) and the
|
|
23
|
+
clean-room replay artifacts under `.gsd-t/scan/`.
|
|
24
|
+
- 120 milestone tests, of which 29 pin verify findings across seven verify runs.
|
|
25
|
+
|
|
26
|
+
## [5.17.14] - 2026-09-03
|
|
27
|
+
|
|
28
|
+
### Fixed — two more gate defects surfaced by TimeTracking (TD-395 round 2), plus a silent library and a crashing finalizer
|
|
29
|
+
|
|
30
|
+
- `bin/gsd-t-fallback-detect.cjs`: `--scan` now covers only what git treats as the project's
|
|
31
|
+
source (tracked + untracked-not-ignored). Git-ignored files — where GSD-T's propagated
|
|
32
|
+
`bin/*.cjs` tools live — are skipped and counted (`skippedIgnored`); outside a git repo
|
|
33
|
+
nothing is filtered and the envelope says so (`gitSourceFilter:false`). The gate had
|
|
34
|
+
flagged GSD-T's own freshly-synced checker as the project's fallbacks.
|
|
35
|
+
- `bin/gsd-t-logging-envelope-check.cjs`: `.gsd-t/logging-manifest.json` lets a project
|
|
36
|
+
DECLARE a stream the candidate paths cannot see (`{ "audit": { "module":
|
|
37
|
+
"server/src/audit.ts", "store": { "kind": "postgres", "table": "tb_audit_log" },
|
|
38
|
+
"retention": "indefinite" } }`). The declaration is checked — a missing path or malformed
|
|
39
|
+
file FAILs (`logging-manifest-invalid`); an external store's rows are not inspected offline
|
|
40
|
+
and that is reported in `notes`, with the module surface as the enforced evidence.
|
|
41
|
+
- `bin/gsd-t-migrate-logging.cjs`: a declared stream is skipped (reported under `declared`),
|
|
42
|
+
so declaring an existing audit never scaffolds a second audit module beside it.
|
|
43
|
+
- `bin/gsd-t-file-disjointness.cjs`: run directly it produced no output and exit 0, which a
|
|
44
|
+
partition finalizer read as a clean check. It now exits 64 and names `gsd-t parallel --dry-run`.
|
|
45
|
+
- Contract `logging-verify-gate-contract.md` §discovery (0), `CLAUDE-global.md`,
|
|
46
|
+
`commands/gsd-t-migrate-logging.md` rippled. 12 tests.
|
|
47
|
+
- `/cpua` now verifies the global install advanced ON DISK and halts if not — the 5.17.13
|
|
48
|
+
release saw `npm install -g` report success while leaving the old version in place.
|
|
49
|
+
|
|
50
|
+
Also: M115 PLANNED (25 atomic tasks, headline bound, two pre-mortem findings closed). Not built.
|
|
51
|
+
|
|
5
52
|
## [5.17.13] - 2026-09-03
|
|
6
53
|
|
|
7
54
|
### Fixed — the verify gate failed projects for GSD-T's own gaps (TD-395), and a lost finalizer crashed the phase workflow
|
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# GSD-T: Contract-Driven Development for Claude Code
|
|
2
2
|
|
|
3
|
-
**v5.
|
|
3
|
+
**v5.18.10** - A methodology for reliable, parallelizable development using Claude Code with optional Agent Teams support.
|
|
4
4
|
|
|
5
5
|
**Eliminates context rot** — task-level fresh dispatch (one subagent per task, ~10-20% context each) means compaction never triggers.
|
|
6
6
|
**Compaction-proof debug loops** — `gsd-t headless --debug-loop` runs test-fix-retest cycles as separate `claude -p` sessions. A JSONL debug ledger persists all hypothesis/fix/learning history across fresh sessions. Anti-repetition preamble injection prevents retrying failed hypotheses. Escalation tiers (sonnet → opus → human) and a hard iteration ceiling enforced externally.
|
|
@@ -197,6 +197,7 @@ This will replace changed command files, back up your CLAUDE.md if customized, a
|
|
|
197
197
|
| `/gsd-t-milestone` | Define new milestone | Manual |
|
|
198
198
|
| `/gsd-t-partition` | Decompose into domains + contracts | In wave |
|
|
199
199
|
| `/gsd-t-plan` | Create atomic task lists per domain (tasks auto-split to fit one context window) | In wave |
|
|
200
|
+
| `/gsd-t-test-plan` | Enumerate every test case a requirements doc implies, before any code or tests exist (`--after` re-enumerates against built code, classifying failures by cited evidence) | Manual |
|
|
200
201
|
| `/gsd-t-impact` | Analyze downstream effects | In wave |
|
|
201
202
|
| `/gsd-t-architect` | Interviews you first (shows its read of current behavior for confirmation, researches when unsure — max 3 cycles), then runs the Architect's Oversight Six-Stage Pass — simplest solution + reuse + traps, as plain-English pseudocode (plan-only; `--build` to auto-build, `--no-interview`/`--no-research` to skip a stage) | Manual |
|
|
202
203
|
| `/gsd-t-execute` | Run tasks — task-level fresh dispatch, worktree isolation, adaptive replanning | In wave |
|
|
@@ -461,6 +461,21 @@ function scanPlan(text, file) {
|
|
|
461
461
|
|
|
462
462
|
// ─── Directory walk ─────────────────────────────────────────────────────────
|
|
463
463
|
|
|
464
|
+
// The set of repo-relative paths git treats as the project's source: tracked
|
|
465
|
+
// plus untracked-not-ignored. Returns null when this is not a git repo or git
|
|
466
|
+
// is unavailable — the caller then scans everything and REPORTS the absence.
|
|
467
|
+
function gitSourceSet(projectDir) {
|
|
468
|
+
try {
|
|
469
|
+
const { execFileSync } = require("child_process");
|
|
470
|
+
const out = execFileSync("git", ["ls-files", "-z", "--cached", "--others", "--exclude-standard"], {
|
|
471
|
+
cwd: projectDir, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], maxBuffer: 64 * 1024 * 1024,
|
|
472
|
+
});
|
|
473
|
+
return new Set(out.split("\0").filter(Boolean));
|
|
474
|
+
} catch (_) {
|
|
475
|
+
return null;
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
|
|
464
479
|
function walk(dir, out, root) {
|
|
465
480
|
let entries;
|
|
466
481
|
try {
|
|
@@ -512,6 +527,8 @@ function main() {
|
|
|
512
527
|
|
|
513
528
|
let findings = [];
|
|
514
529
|
let scanned = 0;
|
|
530
|
+
let gitSourceFilter = false;
|
|
531
|
+
let skippedIgnored = 0;
|
|
515
532
|
|
|
516
533
|
try {
|
|
517
534
|
if (args.plan) {
|
|
@@ -521,7 +538,18 @@ function main() {
|
|
|
521
538
|
} else if (args.scan) {
|
|
522
539
|
const files = [];
|
|
523
540
|
walk(projectDir, files, projectDir);
|
|
524
|
-
|
|
541
|
+
// Only the project's OWN source is the project's to answer for. In a git
|
|
542
|
+
// repo that is: tracked files plus untracked files git does not ignore.
|
|
543
|
+
// Git-ignored files are not the project's code — GSD-T's propagated
|
|
544
|
+
// bin/*.cjs tools live there (TimeTracking, 2026-09-03: the gate flagged
|
|
545
|
+
// the freshly-synced logging checker, GSD-T's own code, as the project's
|
|
546
|
+
// fallbacks). Outside a git repo nothing is filtered, and the envelope
|
|
547
|
+
// says so (`gitSourceFilter:false`) rather than pretending.
|
|
548
|
+
const gitSource = gitSourceSet(projectDir);
|
|
549
|
+
gitSourceFilter = gitSource !== null;
|
|
550
|
+
const inScope = gitSource ? files.filter((f) => gitSource.has(path.relative(projectDir, f).replace(/\\/g, "/"))) : files;
|
|
551
|
+
skippedIgnored = files.length - inScope.length;
|
|
552
|
+
for (const f of inScope) {
|
|
525
553
|
try {
|
|
526
554
|
findings.push(...scanText(fs.readFileSync(f, "utf8"), path.relative(projectDir, f)));
|
|
527
555
|
scanned++;
|
|
@@ -599,6 +627,8 @@ function main() {
|
|
|
599
627
|
ok: unapproved.length === 0,
|
|
600
628
|
exitCode: unapproved.length === 0 ? EXIT_CLEAN : EXIT_FOUND,
|
|
601
629
|
filesScanned: scanned,
|
|
630
|
+
gitSourceFilter,
|
|
631
|
+
skippedIgnored,
|
|
602
632
|
found: findings.length,
|
|
603
633
|
preExisting,
|
|
604
634
|
approved,
|
|
@@ -612,3 +612,14 @@ module.exports = {
|
|
|
612
612
|
_resolveTouches: resolveTouches,
|
|
613
613
|
_gitHistoryTouches: gitHistoryTouches,
|
|
614
614
|
};
|
|
615
|
+
|
|
616
|
+
// This file is a LIBRARY. Invoked directly it used to exit 0 with no output at all,
|
|
617
|
+
// and a partition finalizer (M115, 2026-09-03) read that silence as "no collisions".
|
|
618
|
+
// Silence is not a pass. The CLI surface is `gsd-t parallel --dry-run` (parallel-cli.cjs).
|
|
619
|
+
if (require.main === module) {
|
|
620
|
+
process.stderr.write(
|
|
621
|
+
"gsd-t-file-disjointness.cjs is a library, not a command — it checks nothing when run directly.\n" +
|
|
622
|
+
"Use: gsd-t parallel --dry-run (bin/parallel-cli.cjs) to validate file-disjointness.\n"
|
|
623
|
+
);
|
|
624
|
+
process.exit(64);
|
|
625
|
+
}
|
|
@@ -167,8 +167,11 @@ function evaluate(consumers) {
|
|
|
167
167
|
if (c.wiringModes.length === 0) continue; // never declared; nothing claimed, nothing to prove
|
|
168
168
|
|
|
169
169
|
const claimedWired = c.wiringModes.some((m) => m.toLowerCase() === 'wired');
|
|
170
|
-
|
|
171
|
-
|
|
170
|
+
// A claim with no usable timestamp cannot be placed in time, so nothing
|
|
171
|
+
// can be attributed to it — the one `|| ''` here made EVERY query evidence,
|
|
172
|
+
// queries before the claim included (code-review M115 run 7).
|
|
173
|
+
const since = c.firstWiringTs;
|
|
174
|
+
const attributed = since ? unlabelledQueryTs.filter((ts) => ts && ts >= since).length : 0;
|
|
172
175
|
const evidenceCount = c.queryCount + attributed;
|
|
173
176
|
checked.push({ consumer: id, wiringModes: c.wiringModes, queryCount: c.queryCount, attributedQueryCount: attributed });
|
|
174
177
|
|
|
@@ -416,17 +416,103 @@ function _readModuleSurface(absModulePath) {
|
|
|
416
416
|
};
|
|
417
417
|
}
|
|
418
418
|
|
|
419
|
+
// ── §declaration — .gsd-t/logging-manifest.json ──────────────────────────────
|
|
420
|
+
//
|
|
421
|
+
// A project whose streams live somewhere the candidate lists do not guess
|
|
422
|
+
// (TimeTracking, 2026-09-03: audit = server/src/audit.ts → Postgres tb_audit_log)
|
|
423
|
+
// DECLARES them here. A declaration beats guessing, and it is checked, never
|
|
424
|
+
// trusted: a declared module path that does not exist is a FAIL, not a skip.
|
|
425
|
+
//
|
|
426
|
+
// { "trace": { "module": "server/src/trace.ts", "store": "…" },
|
|
427
|
+
// "audit": { "module": "server/src/audit.ts",
|
|
428
|
+
// "store": { "kind": "postgres", "table": "tb_audit_log" },
|
|
429
|
+
// "retention": "indefinite" } }
|
|
430
|
+
//
|
|
431
|
+
// `store` is either a repo-relative path (JSON array or SQLite, inspected like a
|
|
432
|
+
// discovered one) or an object naming an EXTERNAL store (kind + table). An
|
|
433
|
+
// external store cannot be opened offline, so its rows are not inspected — that
|
|
434
|
+
// is reported in `notes`, never hidden — and the enforceable evidence becomes the
|
|
435
|
+
// module surface (append-only declared, no update/delete path, retention).
|
|
436
|
+
// `retention: "indefinite"` declares a never-purged audit log; it satisfies the
|
|
437
|
+
// retention rule by policy and is noted.
|
|
438
|
+
function _readManifest(projectDir) {
|
|
439
|
+
const p = path.join(projectDir, '.gsd-t', 'logging-manifest.json');
|
|
440
|
+
if (!fs.existsSync(p)) return { present: false, manifest: null, error: null };
|
|
441
|
+
try {
|
|
442
|
+
const m = JSON.parse(fs.readFileSync(p, 'utf8'));
|
|
443
|
+
if (!m || typeof m !== 'object' || Array.isArray(m)) return { present: true, manifest: null, error: '.gsd-t/logging-manifest.json is not a JSON object' };
|
|
444
|
+
return { present: true, manifest: m, error: null };
|
|
445
|
+
} catch (err) {
|
|
446
|
+
return { ok: false, present: true, manifest: null, error: '.gsd-t/logging-manifest.json is not valid JSON: ' + (err && err.message ? err.message : String(err)) };
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
function _declaredStream(projectDir, manifest, stream, failures, notes) {
|
|
451
|
+
const out = { modulePath: null, storePath: null, externalStore: null, retentionIndefinite: false };
|
|
452
|
+
const d = manifest && manifest[stream];
|
|
453
|
+
if (!d) return out;
|
|
454
|
+
if (typeof d !== 'object' || Array.isArray(d)) {
|
|
455
|
+
failures.push({ rule: 'logging-manifest-invalid', stream, detail: 'manifest.' + stream + ' must be an object' });
|
|
456
|
+
return out;
|
|
457
|
+
}
|
|
458
|
+
// A declared path must stay inside the project (same containment the plan-row
|
|
459
|
+
// loader enforces — review M115 run 7).
|
|
460
|
+
const inside = (rel) => {
|
|
461
|
+
if (typeof rel !== 'string' || !rel) return null;
|
|
462
|
+
const root = path.resolve(projectDir);
|
|
463
|
+
const abs = path.resolve(root, rel);
|
|
464
|
+
return abs.startsWith(root + path.sep) ? abs : null;
|
|
465
|
+
};
|
|
466
|
+
if (d.module !== undefined) {
|
|
467
|
+
const abs = inside(d.module);
|
|
468
|
+
if (!abs || !fs.existsSync(abs)) {
|
|
469
|
+
failures.push({ rule: 'logging-manifest-invalid', stream, detail: 'declared ' + stream + ' module not found: ' + String(d.module) });
|
|
470
|
+
} else {
|
|
471
|
+
out.modulePath = abs;
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
if (d.store !== undefined) {
|
|
475
|
+
if (typeof d.store === 'string' && d.store) {
|
|
476
|
+
const abs = inside(d.store);
|
|
477
|
+
if (!abs || !fs.existsSync(abs)) failures.push({ rule: 'logging-manifest-invalid', stream, detail: 'declared ' + stream + ' store not found (or outside the project): ' + d.store });
|
|
478
|
+
else out.storePath = abs;
|
|
479
|
+
} else if (d.store && typeof d.store === 'object' && typeof d.store.kind === 'string' && d.store.kind) {
|
|
480
|
+
out.externalStore = d.store;
|
|
481
|
+
notes.push(stream + ' store declared as external (' + d.store.kind + (d.store.table ? ':' + d.store.table : '') + ') — live rows are not inspected offline; the module surface is the enforced evidence');
|
|
482
|
+
} else {
|
|
483
|
+
failures.push({ rule: 'logging-manifest-invalid', stream, detail: 'declared ' + stream + ' store must be a repo-relative path or {kind, table}' });
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
if (d.retention !== undefined) {
|
|
487
|
+
if (d.retention === 'indefinite') {
|
|
488
|
+
out.retentionIndefinite = true;
|
|
489
|
+
notes.push(stream + ' retention declared indefinite (never purged) — retention rule satisfied by policy');
|
|
490
|
+
} else {
|
|
491
|
+
failures.push({ rule: 'logging-manifest-invalid', stream, detail: 'retention may only be declared "indefinite"; anything else is read from the module surface' });
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
return out;
|
|
495
|
+
}
|
|
496
|
+
|
|
419
497
|
function checkLoggingEnvelopes(opts) {
|
|
420
498
|
opts = opts || {};
|
|
421
499
|
const projectDir = opts.projectDir || '.';
|
|
422
500
|
const failures = [];
|
|
501
|
+
const notes = [];
|
|
502
|
+
|
|
503
|
+
// (0) Declaration file — checked before any path guessing.
|
|
504
|
+
const mf = _readManifest(projectDir);
|
|
505
|
+
if (mf.error) failures.push({ rule: 'logging-manifest-invalid', stream: 'both', detail: mf.error });
|
|
506
|
+
const declTrace = _declaredStream(projectDir, mf.manifest, 'trace', failures, notes);
|
|
507
|
+
const declAudit = _declaredStream(projectDir, mf.manifest, 'audit', failures, notes);
|
|
423
508
|
|
|
424
509
|
// (i) Trace discovery — default for every project EXCEPT explicit opt-out
|
|
425
510
|
// (M100 correction: stateless CLI/library class has no runtime data-flow to trace,
|
|
426
511
|
// so a symmetric .gsd-t/trace-optout.json opt-out exists, mirroring audit's).
|
|
427
|
-
const traceModulePath = _firstExisting(projectDir, TRACE_MODULE_CANDIDATES);
|
|
428
|
-
const
|
|
429
|
-
const
|
|
512
|
+
const traceModulePath = declTrace.modulePath || _firstExisting(projectDir, TRACE_MODULE_CANDIDATES);
|
|
513
|
+
const _declTraceIsDb = declTrace.storePath && /\.(db|sqlite)$/i.test(declTrace.storePath);
|
|
514
|
+
const traceStorePath = (declTrace.storePath && !_declTraceIsDb) ? declTrace.storePath : _firstExisting(projectDir, TRACE_STORE_CANDIDATES);
|
|
515
|
+
const traceDbPath = _declTraceIsDb ? declTrace.storePath : _firstExisting(projectDir, TRACE_DB_CANDIDATES);
|
|
430
516
|
const traceRecords = traceStorePath ? _readJsonArrayIfExists(traceStorePath) : null;
|
|
431
517
|
let traceOptOutRecord = null;
|
|
432
518
|
const traceOptOutPath = path.join(projectDir, '.gsd-t', 'trace-optout.json');
|
|
@@ -434,7 +520,7 @@ function checkLoggingEnvelopes(opts) {
|
|
|
434
520
|
if (fs.existsSync(traceOptOutPath)) traceOptOutRecord = JSON.parse(fs.readFileSync(traceOptOutPath, 'utf8'));
|
|
435
521
|
} catch (_e) { traceOptOutRecord = null; }
|
|
436
522
|
|
|
437
|
-
if (!traceModulePath && !traceStorePath && !traceDbPath) {
|
|
523
|
+
if (!traceModulePath && !traceStorePath && !traceDbPath && !declTrace.externalStore) {
|
|
438
524
|
if (!_isValidTraceOptOut(traceOptOutRecord)) {
|
|
439
525
|
failures.push({ rule: 'trace-default-except-optout', stream: 'trace', detail: 'no trace module or store discoverable and no valid trace opt-out record' });
|
|
440
526
|
}
|
|
@@ -467,11 +553,12 @@ function checkLoggingEnvelopes(opts) {
|
|
|
467
553
|
// legitimately nothing to validate yet.
|
|
468
554
|
|
|
469
555
|
// (ii) Audit discovery.
|
|
470
|
-
const auditModulePath = _firstExisting(projectDir, AUDIT_MODULE_CANDIDATES);
|
|
471
|
-
const
|
|
472
|
-
const
|
|
556
|
+
const auditModulePath = declAudit.modulePath || _firstExisting(projectDir, AUDIT_MODULE_CANDIDATES);
|
|
557
|
+
const _declAuditIsDb = declAudit.storePath && /\.(db|sqlite)$/i.test(declAudit.storePath);
|
|
558
|
+
const auditStorePath = (declAudit.storePath && !_declAuditIsDb) ? declAudit.storePath : _firstExisting(projectDir, AUDIT_STORE_CANDIDATES);
|
|
559
|
+
const auditDbPath = _declAuditIsDb ? declAudit.storePath : _firstExisting(projectDir, AUDIT_DB_CANDIDATES);
|
|
473
560
|
const auditRecords = auditStorePath ? _readJsonArrayIfExists(auditStorePath) : null;
|
|
474
|
-
const hasAuditStore = !!(auditModulePath || auditStorePath || auditDbPath);
|
|
561
|
+
const hasAuditStore = !!(auditModulePath || auditStorePath || auditDbPath || declAudit.externalStore);
|
|
475
562
|
|
|
476
563
|
// (iii) Opt-out file.
|
|
477
564
|
let optOutRecord = null;
|
|
@@ -517,10 +604,12 @@ function checkLoggingEnvelopes(opts) {
|
|
|
517
604
|
exportsDelete: surface ? surface.exportsDelete : false,
|
|
518
605
|
declaresAppendOnly: surface ? surface.declaresAppendOnly : false,
|
|
519
606
|
}));
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
607
|
+
if (!declAudit.retentionIndefinite) {
|
|
608
|
+
failures.push(..._checkRetentionConfigurable({
|
|
609
|
+
hardcoded: surface ? surface.retentionHardcoded : true,
|
|
610
|
+
configurable: surface ? surface.retentionConfigurable : false,
|
|
611
|
+
}));
|
|
612
|
+
}
|
|
524
613
|
} else {
|
|
525
614
|
// Audit store present but no module surface to inspect declared durability rules.
|
|
526
615
|
failures.push({ rule: 'audit-append-only-immutable', stream: 'audit', detail: 'no audit module surface discoverable to verify append-only declaration' });
|
|
@@ -528,12 +617,14 @@ function checkLoggingEnvelopes(opts) {
|
|
|
528
617
|
}
|
|
529
618
|
}
|
|
530
619
|
|
|
531
|
-
return { ok: failures.length === 0, failures };
|
|
620
|
+
return { ok: failures.length === 0, failures, notes };
|
|
532
621
|
}
|
|
533
622
|
|
|
534
623
|
module.exports = {
|
|
535
624
|
checkEnvelope,
|
|
536
625
|
checkLoggingEnvelopes,
|
|
626
|
+
_readManifest,
|
|
627
|
+
_declaredStream,
|
|
537
628
|
// Test surface (not part of the public contract):
|
|
538
629
|
_hasKey,
|
|
539
630
|
_typeOk,
|
|
@@ -110,6 +110,10 @@ function writeDistilledSchemaIfAbsent(projectDir, planPath) {
|
|
|
110
110
|
* a file that already exists in the target project is left byte-for-byte
|
|
111
111
|
* untouched (recorded in `skipped`, never in `created`).
|
|
112
112
|
*/
|
|
113
|
+
// The manifest reader lives in ONE place — the envelope checker — and is reused
|
|
114
|
+
// here (review M115 run 7: two byte-identical copies had already appeared).
|
|
115
|
+
const { _readManifest: readManifest } = require("./gsd-t-logging-envelope-check.cjs");
|
|
116
|
+
|
|
113
117
|
function migrateLogging(projectDir, opts) {
|
|
114
118
|
opts = opts || {};
|
|
115
119
|
if (!projectDir || typeof projectDir !== 'string') {
|
|
@@ -121,12 +125,32 @@ function migrateLogging(projectDir, opts) {
|
|
|
121
125
|
|
|
122
126
|
const created = [];
|
|
123
127
|
const skipped = [];
|
|
128
|
+
const declared = [];
|
|
129
|
+
|
|
130
|
+
// A stream the project DECLARES in .gsd-t/logging-manifest.json already exists
|
|
131
|
+
// somewhere the template path is not (TimeTracking, 2026-09-03: audit lives at
|
|
132
|
+
// server/src/audit.ts → Postgres). Scaffolding src/logging/audit.ts beside it
|
|
133
|
+
// would create a second audit module — a silent twin. A declared stream is
|
|
134
|
+
// skipped and reported as such; a manifest that cannot be read is an ERROR,
|
|
135
|
+
// not "no manifest" (scaffolding over an unreadable declaration is the twin
|
|
136
|
+
// risk with the evidence hidden).
|
|
137
|
+
const manifest = readManifest(projectDir);
|
|
138
|
+
if (manifest.error) throw new Error('migrateLogging: ' + manifest.error);
|
|
139
|
+
const declares = (stream) => !!(manifest.manifest && manifest.manifest[stream] && typeof manifest.manifest[stream] === 'object' && manifest.manifest[stream].module);
|
|
140
|
+
|
|
141
|
+
if (declares('trace')) {
|
|
142
|
+
declared.push('trace: ' + manifest.manifest.trace.module);
|
|
143
|
+
} else {
|
|
144
|
+
const traceResult = copyIfAbsent(projectDir, TRACE_DEST_REL, TRACE_TEMPLATE_PATH);
|
|
145
|
+
(traceResult === 'created' ? created : skipped).push(TRACE_DEST_REL);
|
|
146
|
+
}
|
|
124
147
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
148
|
+
if (declares('audit')) {
|
|
149
|
+
declared.push('audit: ' + manifest.manifest.audit.module);
|
|
150
|
+
} else {
|
|
151
|
+
const auditResult = copyIfAbsent(projectDir, AUDIT_DEST_REL, AUDIT_TEMPLATE_PATH);
|
|
152
|
+
(auditResult === 'created' ? created : skipped).push(AUDIT_DEST_REL);
|
|
153
|
+
}
|
|
130
154
|
|
|
131
155
|
const schemaResult = writeDistilledSchemaIfAbsent(projectDir, opts.planPath);
|
|
132
156
|
(schemaResult.status === 'created' ? created : skipped).push(SCHEMA_DEST_REL);
|
|
@@ -140,12 +164,14 @@ function migrateLogging(projectDir, opts) {
|
|
|
140
164
|
ok: true,
|
|
141
165
|
created,
|
|
142
166
|
skipped,
|
|
167
|
+
declared,
|
|
143
168
|
dispatchedVia: 'bin/gsd-t.js case "migrate-logging" (wired by d1 — see logging-scaffold-seam-contract.md)',
|
|
144
169
|
scaffold,
|
|
145
170
|
};
|
|
146
171
|
}
|
|
147
172
|
|
|
148
173
|
module.exports = {
|
|
174
|
+
readManifest,
|
|
149
175
|
migrateLogging,
|
|
150
176
|
run,
|
|
151
177
|
// Test surface:
|