@tekyzinc/gsd-t 5.17.13 → 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 CHANGED
@@ -2,6 +2,32 @@
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
+
5
31
  ## [5.17.13] - 2026-09-03
6
32
 
7
33
  ### 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.17.13** - A methodology for reliable, parallelizable development using Claude Code with optional Agent Teams support.
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
- for (const f of files) {
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
+ }
@@ -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 traceStorePath = _firstExisting(projectDir, TRACE_STORE_CANDIDATES);
429
- const traceDbPath = _firstExisting(projectDir, TRACE_DB_CANDIDATES);
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 auditStorePath = _firstExisting(projectDir, AUDIT_STORE_CANDIDATES);
472
- const auditDbPath = _firstExisting(projectDir, AUDIT_DB_CANDIDATES);
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
- failures.push(..._checkRetentionConfigurable({
521
- hardcoded: surface ? surface.retentionHardcoded : true,
522
- configurable: surface ? surface.retentionConfigurable : false,
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
- const traceResult = copyIfAbsent(projectDir, TRACE_DEST_REL, TRACE_TEMPLATE_PATH);
126
- (traceResult === 'created' ? created : skipped).push(TRACE_DEST_REL);
127
-
128
- const auditResult = copyIfAbsent(projectDir, AUDIT_DEST_REL, AUDIT_TEMPLATE_PATH);
129
- (auditResult === 'created' ? created : skipped).push(AUDIT_DEST_REL);
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:
@@ -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.13",
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