@tekyzinc/gsd-t 5.17.12 → 5.17.14
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 +60 -0
- package/README.md +1 -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 +29 -6
- package/bin/gsd-t-logging-envelope-check.cjs +96 -13
- package/bin/gsd-t-migrate-logging.cjs +42 -5
- package/bin/gsd-t.js +5 -0
- package/commands/gsd-t-migrate-logging.md +1 -0
- package/package.json +1 -1
- package/templates/CLAUDE-global.md +1 -0
- package/templates/workflows/gsd-t-phase.workflow.js +7 -0
- package/templates/workflows/gsd-t-verify.workflow.js +8 -2
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,66 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to GSD-T are documented here. Updated with each release.
|
|
4
4
|
|
|
5
|
+
## [5.17.14] - 2026-09-03
|
|
6
|
+
|
|
7
|
+
### Fixed — two more gate defects surfaced by TimeTracking (TD-395 round 2), plus a silent library and a crashing finalizer
|
|
8
|
+
|
|
9
|
+
- `bin/gsd-t-fallback-detect.cjs`: `--scan` now covers only what git treats as the project's
|
|
10
|
+
source (tracked + untracked-not-ignored). Git-ignored files — where GSD-T's propagated
|
|
11
|
+
`bin/*.cjs` tools live — are skipped and counted (`skippedIgnored`); outside a git repo
|
|
12
|
+
nothing is filtered and the envelope says so (`gitSourceFilter:false`). The gate had
|
|
13
|
+
flagged GSD-T's own freshly-synced checker as the project's fallbacks.
|
|
14
|
+
- `bin/gsd-t-logging-envelope-check.cjs`: `.gsd-t/logging-manifest.json` lets a project
|
|
15
|
+
DECLARE a stream the candidate paths cannot see (`{ "audit": { "module":
|
|
16
|
+
"server/src/audit.ts", "store": { "kind": "postgres", "table": "tb_audit_log" },
|
|
17
|
+
"retention": "indefinite" } }`). The declaration is checked — a missing path or malformed
|
|
18
|
+
file FAILs (`logging-manifest-invalid`); an external store's rows are not inspected offline
|
|
19
|
+
and that is reported in `notes`, with the module surface as the enforced evidence.
|
|
20
|
+
- `bin/gsd-t-migrate-logging.cjs`: a declared stream is skipped (reported under `declared`),
|
|
21
|
+
so declaring an existing audit never scaffolds a second audit module beside it.
|
|
22
|
+
- `bin/gsd-t-file-disjointness.cjs`: run directly it produced no output and exit 0, which a
|
|
23
|
+
partition finalizer read as a clean check. It now exits 64 and names `gsd-t parallel --dry-run`.
|
|
24
|
+
- Contract `logging-verify-gate-contract.md` §discovery (0), `CLAUDE-global.md`,
|
|
25
|
+
`commands/gsd-t-migrate-logging.md` rippled. 12 tests.
|
|
26
|
+
- `/cpua` now verifies the global install advanced ON DISK and halts if not — the 5.17.13
|
|
27
|
+
release saw `npm install -g` report success while leaving the old version in place.
|
|
28
|
+
|
|
29
|
+
Also: M115 PLANNED (25 atomic tasks, headline bound, two pre-mortem findings closed). Not built.
|
|
30
|
+
|
|
31
|
+
## [5.17.13] - 2026-09-03
|
|
32
|
+
|
|
33
|
+
### Fixed — the verify gate failed projects for GSD-T's own gaps (TD-395), and a lost finalizer crashed the phase workflow
|
|
34
|
+
|
|
35
|
+
Reported from TimeTracking: verify could not reach Red Team / QA because the gate
|
|
36
|
+
ahead of the triad failed on things the project could not fix. Two of them were ours.
|
|
37
|
+
|
|
38
|
+
- `bin/gsd-t.js`: `gsd-t-logging-envelope-check.cjs` joins `PROJECT_BIN_TOOLS`. The
|
|
39
|
+
verify gate reaches it via `__dirname` inside the project's own `bin/`, so it existed
|
|
40
|
+
on the author's machine and in no project (5th propagation gap of this class).
|
|
41
|
+
- `test/verify-gate-tools-propagated.test.js`: every `__dirname` tool the gate dispatches
|
|
42
|
+
to must be in the propagation set — structural, with a negative case. Closes the class.
|
|
43
|
+
- `bin/gsd-t-graph-use-gate.cjs`: a workflow stamps its WIRED claim under its own name
|
|
44
|
+
(`phase`, `verify`) while its workers' Bash-run queries land under the query CLI's default
|
|
45
|
+
label `cli`, which the gate exempted. TimeTracking's ledger: 302 queries under `cli`,
|
|
46
|
+
gate FAILED as zero. A query from an id with no wiring claim of its own now counts as
|
|
47
|
+
evidence for every claimant whose claim precedes it; a query before the claim is not
|
|
48
|
+
evidence; another claimant's queries are never borrowed. 4 tests.
|
|
49
|
+
- `templates/workflows/gsd-t-verify.workflow.js`: verify stamped its claim at Preflight and
|
|
50
|
+
ran its own gate immediately after — a fresh claim with zero queries, failing by
|
|
51
|
+
construction every run. The stamp now lands after the gate, before the first structural
|
|
52
|
+
query.
|
|
53
|
+
- `templates/workflows/gsd-t-phase.workflow.js`: a finalizer agent lost to an API 529
|
|
54
|
+
resolved to null and the run died with a TypeError at `result.competition = …`. It now
|
|
55
|
+
ends with `status: "failed"` and a resume hint (a halt with a message, not a fallback).
|
|
56
|
+
|
|
57
|
+
Not a GSD-T change: the gate's Playwright step runs `npx playwright test` exactly as the
|
|
58
|
+
project's own `npm test` does; a config that throws without `TEST_DATABASE_URL` must load
|
|
59
|
+
`.env.local` itself.
|
|
60
|
+
|
|
61
|
+
Also in this release: **M115 Test-Plan-First Requirements Interrogation** is DEFINED and
|
|
62
|
+
PARTITIONED (5 domains, 3 risk-first waves) with its A1 blind-replay answer key secured at
|
|
63
|
+
`test/fixtures/m115-blind-replay/`. Nothing of M115 is built yet.
|
|
64
|
+
|
|
5
65
|
## [5.17.12] - 2026-09-02
|
|
6
66
|
|
|
7
67
|
### Fixed — an import written `.js` never resolved to its `.ts` source, so `who-imports` answered "nothing imports this"
|
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# GSD-T: Contract-Driven Development for Claude Code
|
|
2
2
|
|
|
3
|
-
**v5.17.
|
|
3
|
+
**v5.17.14** - 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.
|
|
@@ -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
|
+
}
|
|
@@ -94,7 +94,7 @@ function readLedger(files, sinceTs) {
|
|
|
94
94
|
|
|
95
95
|
const ensure = (id) => {
|
|
96
96
|
if (!consumers[id]) {
|
|
97
|
-
consumers[id] = { consumer: id, queryCount: 0, wiringModes: [], firstWiringTs: null };
|
|
97
|
+
consumers[id] = { consumer: id, queryCount: 0, queryTs: [], wiringModes: [], firstWiringTs: null };
|
|
98
98
|
}
|
|
99
99
|
return consumers[id];
|
|
100
100
|
};
|
|
@@ -122,7 +122,9 @@ function readLedger(files, sinceTs) {
|
|
|
122
122
|
const kind = String(ev.kind || '');
|
|
123
123
|
|
|
124
124
|
if (kind === EVIDENCE_KIND) {
|
|
125
|
-
ensure(id)
|
|
125
|
+
const c = ensure(id);
|
|
126
|
+
c.queryCount++;
|
|
127
|
+
c.queryTs.push(typeof ev.ts === 'string' ? ev.ts : '');
|
|
126
128
|
} else if (kind === WIRING_KIND) {
|
|
127
129
|
const c = ensure(id);
|
|
128
130
|
c.wiringModes.push(String(ev.graphWiringMode || ''));
|
|
@@ -143,24 +145,45 @@ function evaluate(consumers) {
|
|
|
143
145
|
const violations = [];
|
|
144
146
|
const checked = [];
|
|
145
147
|
|
|
148
|
+
// [RULE] graph-use-attributes-unlabelled-queries-by-time
|
|
149
|
+
// A workflow stamps its WIRED claim under its own name ("phase", "verify", …),
|
|
150
|
+
// but the graph queries its workers run through Bash carry the query CLI's
|
|
151
|
+
// default label ("cli") — nothing in the worker's shell exports
|
|
152
|
+
// GSDT_GRAPH_CONSUMER. TimeTracking (2026-09-03, TD-395): 302 queries under
|
|
153
|
+
// "cli", the three WIRED claims under "phase"/"verify", and the gate FAILED a
|
|
154
|
+
// run that had consulted the graph 302 times — attribution, not absence.
|
|
155
|
+
// So a query logged by an id that made NO wiring claim of its own (cli, a hook)
|
|
156
|
+
// counts as evidence for every claimant whose WIRED claim PRECEDES it. A query
|
|
157
|
+
// before the claim is not evidence: nothing had claimed anything yet.
|
|
158
|
+
const unlabelledQueryTs = [];
|
|
159
|
+
for (const id of Object.keys(consumers)) {
|
|
160
|
+
const c = consumers[id];
|
|
161
|
+
if (c.wiringModes.length === 0) unlabelledQueryTs.push(...(c.queryTs || []));
|
|
162
|
+
}
|
|
163
|
+
|
|
146
164
|
for (const id of Object.keys(consumers)) {
|
|
147
165
|
if (NON_CONSUMER_IDS.has(id)) continue;
|
|
148
166
|
const c = consumers[id];
|
|
149
167
|
if (c.wiringModes.length === 0) continue; // never declared; nothing claimed, nothing to prove
|
|
150
168
|
|
|
151
169
|
const claimedWired = c.wiringModes.some((m) => m.toLowerCase() === 'wired');
|
|
152
|
-
|
|
170
|
+
const since = c.firstWiringTs || '';
|
|
171
|
+
const attributed = unlabelledQueryTs.filter((ts) => ts && ts >= since).length;
|
|
172
|
+
const evidenceCount = c.queryCount + attributed;
|
|
173
|
+
checked.push({ consumer: id, wiringModes: c.wiringModes, queryCount: c.queryCount, attributedQueryCount: attributed });
|
|
153
174
|
|
|
154
|
-
if (claimedWired &&
|
|
175
|
+
if (claimedWired && evidenceCount === 0) {
|
|
155
176
|
violations.push({
|
|
156
177
|
consumer: id,
|
|
157
178
|
wiringMode: 'WIRED',
|
|
158
179
|
queryCount: 0,
|
|
180
|
+
attributedQueryCount: 0,
|
|
159
181
|
firstWiringTs: c.firstWiringTs,
|
|
160
182
|
evidence:
|
|
161
183
|
`consumer "${id}" logged graphWiringMode=WIRED but issued 0 graph queries ` +
|
|
162
|
-
`(kind:"query") in this window
|
|
163
|
-
`the
|
|
184
|
+
`(kind:"query") in this window, and no unlabelled query (cli / hook) was logged after ` +
|
|
185
|
+
`the claim — a WIRED claim with no query evidence means the structural question ` +
|
|
186
|
+
`was answered some other way.`,
|
|
164
187
|
});
|
|
165
188
|
}
|
|
166
189
|
}
|
|
@@ -416,17 +416,95 @@ 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: 'manifest is not a JSON object' };
|
|
444
|
+
return { present: true, manifest: m, error: null };
|
|
445
|
+
} catch (err) {
|
|
446
|
+
return { present: true, manifest: null, error: 'manifest 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
|
+
if (d.module !== undefined) {
|
|
459
|
+
const abs = typeof d.module === 'string' && d.module ? path.join(projectDir, d.module) : null;
|
|
460
|
+
if (!abs || !fs.existsSync(abs)) {
|
|
461
|
+
failures.push({ rule: 'logging-manifest-invalid', stream, detail: 'declared ' + stream + ' module not found: ' + String(d.module) });
|
|
462
|
+
} else {
|
|
463
|
+
out.modulePath = abs;
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
if (d.store !== undefined) {
|
|
467
|
+
if (typeof d.store === 'string' && d.store) {
|
|
468
|
+
const abs = path.join(projectDir, d.store);
|
|
469
|
+
if (!fs.existsSync(abs)) failures.push({ rule: 'logging-manifest-invalid', stream, detail: 'declared ' + stream + ' store not found: ' + d.store });
|
|
470
|
+
else out.storePath = abs;
|
|
471
|
+
} else if (d.store && typeof d.store === 'object' && typeof d.store.kind === 'string' && d.store.kind) {
|
|
472
|
+
out.externalStore = d.store;
|
|
473
|
+
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');
|
|
474
|
+
} else {
|
|
475
|
+
failures.push({ rule: 'logging-manifest-invalid', stream, detail: 'declared ' + stream + ' store must be a repo-relative path or {kind, table}' });
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
if (d.retention !== undefined) {
|
|
479
|
+
if (d.retention === 'indefinite') {
|
|
480
|
+
out.retentionIndefinite = true;
|
|
481
|
+
notes.push(stream + ' retention declared indefinite (never purged) — retention rule satisfied by policy');
|
|
482
|
+
} else {
|
|
483
|
+
failures.push({ rule: 'logging-manifest-invalid', stream, detail: 'retention may only be declared "indefinite"; anything else is read from the module surface' });
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
return out;
|
|
487
|
+
}
|
|
488
|
+
|
|
419
489
|
function checkLoggingEnvelopes(opts) {
|
|
420
490
|
opts = opts || {};
|
|
421
491
|
const projectDir = opts.projectDir || '.';
|
|
422
492
|
const failures = [];
|
|
493
|
+
const notes = [];
|
|
494
|
+
|
|
495
|
+
// (0) Declaration file — checked before any path guessing.
|
|
496
|
+
const mf = _readManifest(projectDir);
|
|
497
|
+
if (mf.error) failures.push({ rule: 'logging-manifest-invalid', stream: 'both', detail: mf.error });
|
|
498
|
+
const declTrace = _declaredStream(projectDir, mf.manifest, 'trace', failures, notes);
|
|
499
|
+
const declAudit = _declaredStream(projectDir, mf.manifest, 'audit', failures, notes);
|
|
423
500
|
|
|
424
501
|
// (i) Trace discovery — default for every project EXCEPT explicit opt-out
|
|
425
502
|
// (M100 correction: stateless CLI/library class has no runtime data-flow to trace,
|
|
426
503
|
// 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
|
|
504
|
+
const traceModulePath = declTrace.modulePath || _firstExisting(projectDir, TRACE_MODULE_CANDIDATES);
|
|
505
|
+
const _declTraceIsDb = declTrace.storePath && /\.(db|sqlite)$/i.test(declTrace.storePath);
|
|
506
|
+
const traceStorePath = (declTrace.storePath && !_declTraceIsDb) ? declTrace.storePath : _firstExisting(projectDir, TRACE_STORE_CANDIDATES);
|
|
507
|
+
const traceDbPath = _declTraceIsDb ? declTrace.storePath : _firstExisting(projectDir, TRACE_DB_CANDIDATES);
|
|
430
508
|
const traceRecords = traceStorePath ? _readJsonArrayIfExists(traceStorePath) : null;
|
|
431
509
|
let traceOptOutRecord = null;
|
|
432
510
|
const traceOptOutPath = path.join(projectDir, '.gsd-t', 'trace-optout.json');
|
|
@@ -434,7 +512,7 @@ function checkLoggingEnvelopes(opts) {
|
|
|
434
512
|
if (fs.existsSync(traceOptOutPath)) traceOptOutRecord = JSON.parse(fs.readFileSync(traceOptOutPath, 'utf8'));
|
|
435
513
|
} catch (_e) { traceOptOutRecord = null; }
|
|
436
514
|
|
|
437
|
-
if (!traceModulePath && !traceStorePath && !traceDbPath) {
|
|
515
|
+
if (!traceModulePath && !traceStorePath && !traceDbPath && !declTrace.externalStore) {
|
|
438
516
|
if (!_isValidTraceOptOut(traceOptOutRecord)) {
|
|
439
517
|
failures.push({ rule: 'trace-default-except-optout', stream: 'trace', detail: 'no trace module or store discoverable and no valid trace opt-out record' });
|
|
440
518
|
}
|
|
@@ -467,11 +545,12 @@ function checkLoggingEnvelopes(opts) {
|
|
|
467
545
|
// legitimately nothing to validate yet.
|
|
468
546
|
|
|
469
547
|
// (ii) Audit discovery.
|
|
470
|
-
const auditModulePath = _firstExisting(projectDir, AUDIT_MODULE_CANDIDATES);
|
|
471
|
-
const
|
|
472
|
-
const
|
|
548
|
+
const auditModulePath = declAudit.modulePath || _firstExisting(projectDir, AUDIT_MODULE_CANDIDATES);
|
|
549
|
+
const _declAuditIsDb = declAudit.storePath && /\.(db|sqlite)$/i.test(declAudit.storePath);
|
|
550
|
+
const auditStorePath = (declAudit.storePath && !_declAuditIsDb) ? declAudit.storePath : _firstExisting(projectDir, AUDIT_STORE_CANDIDATES);
|
|
551
|
+
const auditDbPath = _declAuditIsDb ? declAudit.storePath : _firstExisting(projectDir, AUDIT_DB_CANDIDATES);
|
|
473
552
|
const auditRecords = auditStorePath ? _readJsonArrayIfExists(auditStorePath) : null;
|
|
474
|
-
const hasAuditStore = !!(auditModulePath || auditStorePath || auditDbPath);
|
|
553
|
+
const hasAuditStore = !!(auditModulePath || auditStorePath || auditDbPath || declAudit.externalStore);
|
|
475
554
|
|
|
476
555
|
// (iii) Opt-out file.
|
|
477
556
|
let optOutRecord = null;
|
|
@@ -517,10 +596,12 @@ function checkLoggingEnvelopes(opts) {
|
|
|
517
596
|
exportsDelete: surface ? surface.exportsDelete : false,
|
|
518
597
|
declaresAppendOnly: surface ? surface.declaresAppendOnly : false,
|
|
519
598
|
}));
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
599
|
+
if (!declAudit.retentionIndefinite) {
|
|
600
|
+
failures.push(..._checkRetentionConfigurable({
|
|
601
|
+
hardcoded: surface ? surface.retentionHardcoded : true,
|
|
602
|
+
configurable: surface ? surface.retentionConfigurable : false,
|
|
603
|
+
}));
|
|
604
|
+
}
|
|
524
605
|
} else {
|
|
525
606
|
// Audit store present but no module surface to inspect declared durability rules.
|
|
526
607
|
failures.push({ rule: 'audit-append-only-immutable', stream: 'audit', detail: 'no audit module surface discoverable to verify append-only declaration' });
|
|
@@ -528,12 +609,14 @@ function checkLoggingEnvelopes(opts) {
|
|
|
528
609
|
}
|
|
529
610
|
}
|
|
530
611
|
|
|
531
|
-
return { ok: failures.length === 0, failures };
|
|
612
|
+
return { ok: failures.length === 0, failures, notes };
|
|
532
613
|
}
|
|
533
614
|
|
|
534
615
|
module.exports = {
|
|
535
616
|
checkEnvelope,
|
|
536
617
|
checkLoggingEnvelopes,
|
|
618
|
+
_readManifest,
|
|
619
|
+
_declaredStream,
|
|
537
620
|
// Test surface (not part of the public contract):
|
|
538
621
|
_hasKey,
|
|
539
622
|
_typeOk,
|
|
@@ -110,6 +110,21 @@ 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
|
+
// .gsd-t/logging-manifest.json — same reader the envelope checker uses (kept in
|
|
114
|
+
// step with bin/gsd-t-logging-envelope-check.cjs _readManifest; a stream is
|
|
115
|
+
// "declared" when its `module` is named).
|
|
116
|
+
function readManifest(projectDir) {
|
|
117
|
+
const p = path.join(projectDir, '.gsd-t', 'logging-manifest.json');
|
|
118
|
+
if (!fs.existsSync(p)) return { present: false, manifest: null, error: null };
|
|
119
|
+
try {
|
|
120
|
+
const m = JSON.parse(fs.readFileSync(p, 'utf8'));
|
|
121
|
+
if (!m || typeof m !== 'object' || Array.isArray(m)) return { present: true, manifest: null, error: '.gsd-t/logging-manifest.json is not a JSON object' };
|
|
122
|
+
return { present: true, manifest: m, error: null };
|
|
123
|
+
} catch (err) {
|
|
124
|
+
return { present: true, manifest: null, error: '.gsd-t/logging-manifest.json is not valid JSON: ' + (err && err.message ? err.message : String(err)) };
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
113
128
|
function migrateLogging(projectDir, opts) {
|
|
114
129
|
opts = opts || {};
|
|
115
130
|
if (!projectDir || typeof projectDir !== 'string') {
|
|
@@ -121,12 +136,32 @@ function migrateLogging(projectDir, opts) {
|
|
|
121
136
|
|
|
122
137
|
const created = [];
|
|
123
138
|
const skipped = [];
|
|
139
|
+
const declared = [];
|
|
140
|
+
|
|
141
|
+
// A stream the project DECLARES in .gsd-t/logging-manifest.json already exists
|
|
142
|
+
// somewhere the template path is not (TimeTracking, 2026-09-03: audit lives at
|
|
143
|
+
// server/src/audit.ts → Postgres). Scaffolding src/logging/audit.ts beside it
|
|
144
|
+
// would create a second audit module — a silent twin. A declared stream is
|
|
145
|
+
// skipped and reported as such; a manifest that cannot be read is an ERROR,
|
|
146
|
+
// not "no manifest" (scaffolding over an unreadable declaration is the twin
|
|
147
|
+
// risk with the evidence hidden).
|
|
148
|
+
const manifest = readManifest(projectDir);
|
|
149
|
+
if (manifest.error) throw new Error('migrateLogging: ' + manifest.error);
|
|
150
|
+
const declares = (stream) => !!(manifest.manifest && manifest.manifest[stream] && typeof manifest.manifest[stream] === 'object' && manifest.manifest[stream].module);
|
|
151
|
+
|
|
152
|
+
if (declares('trace')) {
|
|
153
|
+
declared.push('trace: ' + manifest.manifest.trace.module);
|
|
154
|
+
} else {
|
|
155
|
+
const traceResult = copyIfAbsent(projectDir, TRACE_DEST_REL, TRACE_TEMPLATE_PATH);
|
|
156
|
+
(traceResult === 'created' ? created : skipped).push(TRACE_DEST_REL);
|
|
157
|
+
}
|
|
124
158
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
159
|
+
if (declares('audit')) {
|
|
160
|
+
declared.push('audit: ' + manifest.manifest.audit.module);
|
|
161
|
+
} else {
|
|
162
|
+
const auditResult = copyIfAbsent(projectDir, AUDIT_DEST_REL, AUDIT_TEMPLATE_PATH);
|
|
163
|
+
(auditResult === 'created' ? created : skipped).push(AUDIT_DEST_REL);
|
|
164
|
+
}
|
|
130
165
|
|
|
131
166
|
const schemaResult = writeDistilledSchemaIfAbsent(projectDir, opts.planPath);
|
|
132
167
|
(schemaResult.status === 'created' ? created : skipped).push(SCHEMA_DEST_REL);
|
|
@@ -140,12 +175,14 @@ function migrateLogging(projectDir, opts) {
|
|
|
140
175
|
ok: true,
|
|
141
176
|
created,
|
|
142
177
|
skipped,
|
|
178
|
+
declared,
|
|
143
179
|
dispatchedVia: 'bin/gsd-t.js case "migrate-logging" (wired by d1 — see logging-scaffold-seam-contract.md)',
|
|
144
180
|
scaffold,
|
|
145
181
|
};
|
|
146
182
|
}
|
|
147
183
|
|
|
148
184
|
module.exports = {
|
|
185
|
+
readManifest,
|
|
149
186
|
migrateLogging,
|
|
150
187
|
run,
|
|
151
188
|
// Test surface:
|
package/bin/gsd-t.js
CHANGED
|
@@ -3467,6 +3467,11 @@ const PROJECT_BIN_TOOLS = [
|
|
|
3467
3467
|
"cli-preflight.cjs", "parallel-cli.cjs", "parallel-cli-tee.cjs",
|
|
3468
3468
|
"gsd-t-context-brief.cjs",
|
|
3469
3469
|
"gsd-t-verify-gate.cjs", "gsd-t-verify-gate-judge.cjs",
|
|
3470
|
+
// M100 D3 — trace+audit envelope gate. verify-gate dispatches to it via __dirname,
|
|
3471
|
+
// so a project with the gate but not this file fails `logging-envelope` on every
|
|
3472
|
+
// verify (TimeTracking TD-395, 2026-09-03 — the 5th propagation gap of this class;
|
|
3473
|
+
// test/verify-gate-tools-propagated.test.js now asserts the whole set structurally).
|
|
3474
|
+
"gsd-t-logging-envelope-check.cjs",
|
|
3470
3475
|
// v5.4.10 — integer-identity primary-key gate. gsd-t-verify-gate.cjs dispatches to
|
|
3471
3476
|
// it via an absolute path in the Track 2 plan, so a project that has the verify gate
|
|
3472
3477
|
// but NOT this file gets an ENOENT on every verify. Ships alongside the gate itself.
|
|
@@ -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.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tekyzinc/gsd-t",
|
|
3
|
-
"version": "5.17.
|
|
3
|
+
"version": "5.17.14",
|
|
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
|
|
|
@@ -1320,6 +1320,13 @@ if (!competitionOn) {
|
|
|
1320
1320
|
].filter(Boolean).join("\n"),
|
|
1321
1321
|
{ label: `${phaseName}:finalize`, phase: "Finalize", schema: FINALIZE_SCHEMA, model: "opus" }
|
|
1322
1322
|
).catch((e) => ({ status: "failed", artifacts: [], summary: `finalizer error: ${e && e.message}` }));
|
|
1323
|
+
// A finalizer that RESOLVES to nothing (API 529 overload observed 2026-09-02, run
|
|
1324
|
+
// wf_66339f4d-545) used to crash the whole run with a TypeError at
|
|
1325
|
+
// `result.competition = ...` — a stack trace instead of a status. This is a HALT,
|
|
1326
|
+
// not a fallback: the run still ends failed, it just says so in the envelope.
|
|
1327
|
+
if (!result || typeof result !== "object") {
|
|
1328
|
+
result = { status: "failed", artifacts: [], summary: "finalizer returned no result (transient API error or empty agent return) — resume the run with resumeFromRunId; cached stages replay, only finalize re-runs" };
|
|
1329
|
+
}
|
|
1323
1330
|
|
|
1324
1331
|
// Re-validate the FINALIZED partition (Invariant 4). If salvage reintroduced an
|
|
1325
1332
|
// overlap, the finalized graft is invalid → block completion with a clear reason.
|
|
@@ -301,8 +301,6 @@ if (!pre.ok) {
|
|
|
301
301
|
return { status: "failed", reason: "preflight-failed", preflight: pre.envelope };
|
|
302
302
|
}
|
|
303
303
|
const brief = await generateBrief(projectDir, { kind: "verify", milestone, id: `verify-${(milestone || "m").toLowerCase()}` });
|
|
304
|
-
// M99 D2: persist graphWiringMode for the verify consumer. [RULE] wiring-mode-three-states
|
|
305
|
-
await persistWiringMode("Preflight");
|
|
306
304
|
|
|
307
305
|
phase("Verify-Gate");
|
|
308
306
|
const vg = await runVerifyGate(projectDir);
|
|
@@ -316,6 +314,14 @@ if (!vg.ok) {
|
|
|
316
314
|
}
|
|
317
315
|
log(`verify-gate green`);
|
|
318
316
|
|
|
317
|
+
// M99 D2: persist graphWiringMode for the verify consumer. [RULE] wiring-mode-three-states
|
|
318
|
+
// Stamped AFTER the verify-gate, not before it (TimeTracking TD-395, 2026-09-03): the
|
|
319
|
+
// gate's graph-use check judges every WIRED claim on the queries that FOLLOW it, and
|
|
320
|
+
// verify's first graph query (dead-code, just below) comes after the gate. Stamping at
|
|
321
|
+
// Preflight made every verify run fail its own gate — a fresh claim, zero queries, by
|
|
322
|
+
// construction. Now the claim precedes the structural queries and the NEXT run judges it.
|
|
323
|
+
await persistWiringMode("Verify-Gate");
|
|
324
|
+
|
|
319
325
|
// ─── M94-D10-T5: Graph Structural Slice — dead-code + dangling (ADDITIVE, announced-degradation) ──
|
|
320
326
|
// [RULE] qa-verify-use-orphan-dangling-verbs — query dead-code + dangling structurally.
|
|
321
327
|
// [RULE] verify-integrate-graph-additive-announced-not-hard-fail — PRE-MORTEM Finding 3
|