@tekyzinc/gsd-t 4.13.11 → 4.14.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.
@@ -0,0 +1,484 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+
4
+ /**
5
+ * gsd-t-graph-store-resolver — M99 D1 (single path resolver + migration shim + sink)
6
+ *
7
+ * THE ONLY place a graph store path is derived after M99.
8
+ * D2 and D3 import from this module; they never re-derive a path or contain
9
+ * a raw `.gsd-t/graph.db` / `.gsd-t/graphDB/` literal.
10
+ *
11
+ * Prevents the M96-class silent split-brain (two code paths disagreeing on
12
+ * where the store lives).
13
+ *
14
+ * Exports:
15
+ * resolveGraphDir(projectRoot?) → abs path to `.gsd-t/graphDB/`
16
+ * resolveStorePath(projectRoot?) → abs path to `.gsd-t/graphDB/graph.db`
17
+ * resolveLogsDir(projectRoot?) → abs path to `.gsd-t/graphDB/logs/`
18
+ * deriveProjectRoot(storePath) → repo root (3-up from graphDB/graph.db; 2-up from JSONL)
19
+ * migrateGraphStore(projectRoot?) → copy-verify-swap shim (idempotent, interruption-safe)
20
+ * append_ledger_line(record) → fail-open ledger append with rotation + toggle
21
+ *
22
+ * [RULE] one-resolver-only
23
+ * [RULE] projectroot-depth-corrected-with-move
24
+ * [RULE] jsonl-branch-depth-preserved
25
+ * [RULE] copy-verify-swap-never-orphan
26
+ * [RULE] migration-real-root-only
27
+ * [RULE] fail-open-telemetry
28
+ * [RULE] layer1-shape-kept
29
+ * [RULE] rollover-boundary-proven
30
+ */
31
+
32
+ const fs = require("node:fs");
33
+ const path = require("node:path");
34
+ const os = require("node:os");
35
+
36
+ // ─── Legacy store path constant (migration shim uses this explicitly) ─────────
37
+ // ONLY this module is allowed to reference the old path. All producers route
38
+ // through resolveStorePath(). Exported so the query-cli discovery loop can
39
+ // check for the legacy file without re-deriving the literal.
40
+ const LEGACY_DB_SUBPATH = path.join(".gsd-t", "graph.db"); // spike-local-store: legacy path reference; migration shim only
41
+
42
+ /**
43
+ * Return the absolute legacy store path (pre-M99: .gsd-t/graph.db).
44
+ * Used ONLY for self-heal detection in the discovery loop.
45
+ * @param {string} dir — the directory to check in
46
+ * @returns {string}
47
+ */
48
+ function resolveLegacyStorePath(dir) {
49
+ return path.join(dir, LEGACY_DB_SUBPATH);
50
+ }
51
+
52
+ // ─── New store layout ─────────────────────────────────────────────────────────
53
+ const GRAPH_DIR_NAME = "graphDB";
54
+ const DB_FILENAME = "graph.db";
55
+ const LOGS_DIR_NAME = "logs";
56
+
57
+ // ─── Rotation defaults (production) ──────────────────────────────────────────
58
+ const DEFAULT_MAX_BYTES = 50 * 1024 * 1024; // 50 MB
59
+ const DEFAULT_MAX_ENTRIES = 250_000;
60
+
61
+ // ─── Real-root guard constants ────────────────────────────────────────────────
62
+ // Refuse to migrate inside tmp dirs, the filesystem root, or the home dir.
63
+ const HOME_DIR = os.homedir();
64
+
65
+ /**
66
+ * Resolve the project root.
67
+ * When no explicit root is given, walks up from cwd looking for `.gsd-t/`.
68
+ * Falls back to cwd if no `.gsd-t/` is found.
69
+ *
70
+ * @param {string} [explicitRoot]
71
+ * @returns {string}
72
+ */
73
+ function _findProjectRoot(explicitRoot) {
74
+ if (explicitRoot) return explicitRoot;
75
+ let dir = process.cwd();
76
+ for (let i = 0; i < 12; i++) {
77
+ if (fs.existsSync(path.join(dir, ".gsd-t"))) return dir;
78
+ const parent = path.dirname(dir);
79
+ if (parent === dir) break;
80
+ dir = parent;
81
+ }
82
+ return process.cwd();
83
+ }
84
+
85
+ // ─── T1: Core path resolution ─────────────────────────────────────────────────
86
+
87
+ /**
88
+ * Absolute path to the graphDB directory: `<projectRoot>/.gsd-t/graphDB/`
89
+ * @param {string} [projectRoot]
90
+ * @returns {string}
91
+ */
92
+ function resolveGraphDir(projectRoot) {
93
+ return path.join(_findProjectRoot(projectRoot), ".gsd-t", GRAPH_DIR_NAME);
94
+ }
95
+
96
+ /**
97
+ * Absolute path to the SQLite store: `<projectRoot>/.gsd-t/graphDB/graph.db`
98
+ * @param {string} [projectRoot]
99
+ * @returns {string}
100
+ */
101
+ function resolveStorePath(projectRoot) {
102
+ return path.join(resolveGraphDir(projectRoot), DB_FILENAME);
103
+ }
104
+
105
+ /**
106
+ * Absolute path to the logs directory: `<projectRoot>/.gsd-t/graphDB/logs/`
107
+ * @param {string} [projectRoot]
108
+ * @returns {string}
109
+ */
110
+ function resolveLogsDir(projectRoot) {
111
+ return path.join(resolveGraphDir(projectRoot), LOGS_DIR_NAME);
112
+ }
113
+
114
+ /**
115
+ * Absolute path to a SCIP index file under graphDB/.
116
+ * Red Team HIGH (M99 round 4): SC#1 requires ALL graph artifacts — incl.
117
+ * index.scip / index-python.scip — under `.gsd-t/graphDB/`, but the SCIP
118
+ * upgrader wrote them to `.gsd-t/` via hardcoded literals. Route them here.
119
+ * [RULE] scip-index-under-graphdb
120
+ * @param {string} filename - e.g. "index.scip" | "index-python.scip"
121
+ * @param {string} [projectRoot]
122
+ * @returns {string} `<projectRoot>/.gsd-t/graphDB/<filename>`
123
+ */
124
+ function resolveScipPath(filename, projectRoot) {
125
+ return path.join(resolveGraphDir(projectRoot), filename);
126
+ }
127
+
128
+ /**
129
+ * Derive the project root from a known store path.
130
+ *
131
+ * Two branches — DIFFERENT depths post-migration:
132
+ * `.gsd-t/graphDB/graph.db` → 3 levels up (graphDB/ adds one level vs. old 2-up)
133
+ * `.gsd-t/graph-index/` → 2 levels up (JSONL branch stays at 2-up)
134
+ *
135
+ * [RULE] projectroot-depth-corrected-with-move
136
+ * [RULE] jsonl-branch-depth-preserved
137
+ *
138
+ * @param {string} storePath — abs path to store (either `.db` file or `graph-index/` dir)
139
+ * @returns {string} — absolute path to project root
140
+ */
141
+ function deriveProjectRoot(storePath) {
142
+ if (typeof storePath !== "string") return process.cwd();
143
+
144
+ // New graphDB branch: .gsd-t/graphDB/graph.db → 3 levels up
145
+ if (storePath.endsWith(path.join("graphDB", "graph.db")) || storePath.endsWith("graphDB/graph.db")) {
146
+ return path.dirname(path.dirname(path.dirname(storePath)));
147
+ }
148
+
149
+ // Legacy SQLite: .gsd-t/graph.db → 2 levels up
150
+ if (storePath.endsWith(".db")) {
151
+ return path.dirname(path.dirname(storePath));
152
+ }
153
+
154
+ // JSONL directory branch: .gsd-t/graph-index/ → 2 levels up
155
+ return path.dirname(path.dirname(storePath));
156
+ }
157
+
158
+ // ─── T2: copy-verify-swap migration shim ─────────────────────────────────────
159
+
160
+ /**
161
+ * Real-root guard — refuse to migrate inside tmp, filesystem root, or home.
162
+ * (M94 lesson: fake root → walkTree walked whole disk → OOM)
163
+ *
164
+ * @param {string} root
165
+ * @returns {boolean} true if this looks like a safe real project root
166
+ */
167
+ function _isRealProjectRoot(root) {
168
+ const abs = path.resolve(root);
169
+ if (abs === "/" || abs === HOME_DIR) return false;
170
+ // Reject well-known temp patterns
171
+ const tmp = os.tmpdir();
172
+ if (abs.startsWith(tmp + path.sep) || abs === tmp) return false;
173
+ // Also reject macOS-style /private/tmp and /var/folders
174
+ if (abs.startsWith("/private/tmp") || abs.startsWith("/var/folders")) return false;
175
+ return true;
176
+ }
177
+
178
+ /**
179
+ * Verify that a SQLite file at `dbPath` is readable (can open + query).
180
+ *
181
+ * @param {string} dbPath
182
+ * @returns {boolean}
183
+ */
184
+ function _verifySqliteReadable(dbPath) {
185
+ try {
186
+ const { requireBetterSqlite } = require("./gsd-t-require-store.cjs");
187
+ const Database = requireBetterSqlite();
188
+ const db = new Database(dbPath, { readonly: true });
189
+ // Quick sanity: try to read the schema
190
+ db.prepare("SELECT count(*) FROM sqlite_master").get();
191
+ db.close();
192
+ return true;
193
+ } catch (_e) {
194
+ return false;
195
+ }
196
+ }
197
+
198
+ /**
199
+ * Checkpoint a SQLite store's write-ahead log INTO its main .db file, then
200
+ * truncate the WAL — so the .db becomes fully self-contained (no -wal/-shm
201
+ * dependency). Best-effort: returns true on success, false on any failure
202
+ * (caller falls back to copying the db+wal+shm triple).
203
+ *
204
+ * WHY (Red Team HIGH, M99): the migration's STEP-3 renamed graph.db BEFORE its
205
+ * -wal/-shm. A kill in that window left graph.db present but its -wal orphaned →
206
+ * SQLite reopened at the last checkpoint and SILENTLY dropped uncheckpointed rows
207
+ * (reproduced 4000→2000). Checkpointing the WAL into the .db BEFORE the copy means
208
+ * there is no sidecar to orphan: the single self-contained .db rename is genuinely
209
+ * atomic. WAL is the normal live state, so this path is the common case.
210
+ * [RULE] migration-wal-checkpoint-before-copy
211
+ *
212
+ * @param {string} dbPath
213
+ * @returns {boolean} true if the WAL was checkpointed + truncated
214
+ */
215
+ function _checkpointWal(dbPath) {
216
+ try {
217
+ const { requireBetterSqlite } = require("./gsd-t-require-store.cjs");
218
+ const Database = requireBetterSqlite();
219
+ const db = new Database(dbPath); // writable — checkpoint mutates the .db
220
+ // TRUNCATE: fold all WAL frames into the .db AND reset the -wal to zero length.
221
+ db.pragma("wal_checkpoint(TRUNCATE)");
222
+ db.close();
223
+ return true;
224
+ } catch (_e) {
225
+ return false;
226
+ }
227
+ }
228
+
229
+ /**
230
+ * WAL-safe copy: copies `src.db`, `src.db-wal`, and `src.db-shm` to the
231
+ * target directory atomically (all three must exist for a consistent snapshot).
232
+ *
233
+ * Strategy: copy all three files. A missing -wal / -shm is fine (WAL may be
234
+ * checkpointed). We copy the triple so any uncommitted WAL entries travel with
235
+ * the main db file (pre-mortem #6: WAL-interruption safety).
236
+ *
237
+ * @param {string} srcDb — absolute path to source `.db` file
238
+ * @param {string} dstDir — absolute path to destination directory (must exist)
239
+ * @returns {string} — absolute path to the copied `.db` file
240
+ */
241
+ function _copyDbWithWal(srcDb, dstDir) {
242
+ const basename = path.basename(srcDb);
243
+ const dstDb = path.join(dstDir, basename);
244
+ fs.copyFileSync(srcDb, dstDb);
245
+ for (const suf of ["-wal", "-shm"]) {
246
+ const walSrc = srcDb + suf;
247
+ if (fs.existsSync(walSrc)) {
248
+ fs.copyFileSync(walSrc, path.join(dstDir, basename + suf));
249
+ }
250
+ }
251
+ return dstDb;
252
+ }
253
+
254
+ /**
255
+ * migrateGraphStore — copy-verify-swap from `.gsd-t/graph.db` → `.gsd-t/graphDB/graph.db`
256
+ *
257
+ * Invariants:
258
+ * 1. Idempotent — second run returns `{ migrated: false, reason: 'already-migrated' }`.
259
+ * 2. Interruption-safe — at EVERY point either the old store OR the new store is readable.
260
+ * We retain the old file until the new one is verified readable, then swap (move).
261
+ * 3. Real-root-only — refuses to run inside tmpdir / home / fs-root when called without
262
+ * explicit projectRoot (the auto-fire/self-heal case). Callers that pass an explicit
263
+ * projectRoot (tests, CPUA) opt in to bypass the guard via `{ forceAllow: true }`.
264
+ * 4. No-op when the legacy file does not exist.
265
+ *
266
+ * [RULE] copy-verify-swap-never-orphan
267
+ * [RULE] migration-real-root-only
268
+ * [RULE] discovery-loop-end-to-end
269
+ *
270
+ * @param {string} [projectRoot]
271
+ * @param {{ forceAllow?: boolean }} [opts] — pass `{ forceAllow: true }` to bypass real-root guard (test opt-in)
272
+ * @returns {{ migrated: boolean, reason: string }}
273
+ */
274
+ function migrateGraphStore(projectRoot, opts) {
275
+ const root = _findProjectRoot(projectRoot);
276
+ const forceAllow = opts && opts.forceAllow === true;
277
+
278
+ // Real-root guard (M94 lesson): only applies when NOT force-allowed
279
+ if (!forceAllow && !_isRealProjectRoot(root)) {
280
+ return { migrated: false, reason: "real-root-guard: not a real project root" };
281
+ }
282
+
283
+ const legacyPath = path.join(root, LEGACY_DB_SUBPATH);
284
+ const targetDir = resolveGraphDir(root);
285
+ const targetPath = resolveStorePath(root);
286
+
287
+ // Idempotent: already migrated
288
+ if (fs.existsSync(targetPath) && _verifySqliteReadable(targetPath)) {
289
+ return { migrated: false, reason: "already-migrated" };
290
+ }
291
+
292
+ // Nothing to migrate
293
+ if (!fs.existsSync(legacyPath)) {
294
+ return { migrated: false, reason: "no-legacy-store" };
295
+ }
296
+
297
+ // Ensure target directory exists
298
+ fs.mkdirSync(targetDir, { recursive: true });
299
+
300
+ // STEP 1: Copy legacy store (+ WAL/SHM) to a temp file inside the target dir.
301
+ // We use a temp name so a crash here leaves the legacy store untouched.
302
+ const tmpDb = path.join(targetDir, "graph.db.migrating");
303
+ const tmpWal = tmpDb + "-wal";
304
+ const tmpShm = tmpDb + "-shm";
305
+
306
+ // Clean any previous partial migration
307
+ for (const f of [tmpDb, tmpWal, tmpShm]) {
308
+ try { if (fs.existsSync(f)) fs.unlinkSync(f); } catch {}
309
+ }
310
+
311
+ // STEP 0 (Red Team HIGH fix): checkpoint the legacy WAL INTO graph.db first, so
312
+ // the .db is fully self-contained before we copy. Then a single self-contained
313
+ // .db rename has no -wal/-shm to orphan if interrupted mid-swap (the old STEP-3
314
+ // window that silently dropped uncheckpointed rows, 4000→2000). [RULE]
315
+ // migration-wal-checkpoint-before-copy. Best-effort: if checkpoint fails (e.g.
316
+ // a concurrent writer holds the lock), we STILL copy the db+wal+shm triple below
317
+ // so a clean migration remains WAL-complete — the interruption window only narrows
318
+ // when the checkpoint succeeded (the normal case), never widens.
319
+ const checkpointed = _checkpointWal(legacyPath);
320
+
321
+ // Copy the .db (now self-contained if checkpointed). Also copy any remaining
322
+ // -wal/-shm: harmless when checkpointed (the -wal is zero-length post-TRUNCATE),
323
+ // and the safety net when the checkpoint could not run.
324
+ fs.copyFileSync(legacyPath, tmpDb);
325
+ for (const suf of ["-wal", "-shm"]) {
326
+ const src = legacyPath + suf;
327
+ if (fs.existsSync(src)) fs.copyFileSync(src, tmpDb + suf);
328
+ }
329
+
330
+ // STEP 2: Verify the copy is readable BEFORE committing the rename.
331
+ // If this throws / returns false, the legacy store is still intact.
332
+ if (!_verifySqliteReadable(tmpDb)) {
333
+ // Clean up and bail — old store still intact
334
+ for (const f of [tmpDb, tmpWal, tmpShm]) {
335
+ try { if (fs.existsSync(f)) fs.unlinkSync(f); } catch {}
336
+ }
337
+ return { migrated: false, reason: "verify-failed: copy not readable" };
338
+ }
339
+
340
+ // STEP 3: Atomic rename (swap) — after this, the new path is live.
341
+ // The old store remains for now (we delete it after confirming).
342
+ // Node rename is atomic on most filesystems within the same mountpoint.
343
+ //
344
+ // ORDER MATTERS (Red Team HIGH defense-in-depth): rename the -wal/-shm sidecars
345
+ // BEFORE the main .db. If the checkpoint above succeeded the sidecars are empty
346
+ // and order is moot; but if it could NOT run, a kill between renames must never
347
+ // leave graph.db present without its matching -wal (that orphans uncheckpointed
348
+ // rows). Sidecars-first means an interrupted swap leaves EITHER no new .db (old
349
+ // store still the only live one — self-heal re-runs) OR a complete .db+wal pair,
350
+ // never a .db missing its wal. [RULE] migration-sidecars-renamed-before-db
351
+ for (const suf of ["-wal", "-shm"]) {
352
+ const tmp = tmpDb + suf;
353
+ if (fs.existsSync(tmp)) {
354
+ try { fs.renameSync(tmp, targetPath + suf); } catch {}
355
+ }
356
+ }
357
+ fs.renameSync(tmpDb, targetPath);
358
+
359
+ // STEP 4: Final verify of the swapped-in store
360
+ if (!_verifySqliteReadable(targetPath)) {
361
+ // Undo: move back if possible (the legacy is still there)
362
+ try { fs.renameSync(targetPath, legacyPath + ".restore"); } catch {}
363
+ return { migrated: false, reason: "post-swap-verify-failed" };
364
+ }
365
+
366
+ // SUCCESS — old store can be removed (but we leave it for now; a future
367
+ // cleanup pass can remove it once users confirm the migration is stable).
368
+ // We do NOT delete legacyPath here to honour the "never-orphan" invariant
369
+ // in case of any partial filesystem issue. The no-raw-literals test enforces
370
+ // that only the resolver looks at the legacy path.
371
+
372
+ return { migrated: true, reason: "copy-verify-swap complete" };
373
+ }
374
+
375
+ // ─── T3: append_ledger_line — Layer-1 telemetry sink ─────────────────────────
376
+
377
+ /**
378
+ * Find the current ledger file path, handling rotation.
379
+ *
380
+ * File naming: `graph-events-001.jsonl`, `graph-events-002.jsonl`, ...
381
+ *
382
+ * Rotation backstop: when the current file exceeds maxBytes OR maxEntries,
383
+ * we seal it and start the next one. This is a RUNAWAY backstop (a full
384
+ * analysis session should fit in one file); routine rotation does NOT happen.
385
+ *
386
+ * [RULE] rollover-boundary-proven
387
+ *
388
+ * @param {string} logsDir
389
+ * @param {number} maxBytes
390
+ * @param {number} maxEntries
391
+ * @returns {string} — absolute path to the active ledger file
392
+ */
393
+ function _resolveLedgerPath(logsDir, maxBytes, maxEntries) {
394
+ // List existing ledger files, sorted ascending
395
+ let files = [];
396
+ try {
397
+ files = fs.readdirSync(logsDir)
398
+ .filter((f) => /^graph-events-\d+\.jsonl$/.test(f))
399
+ .sort();
400
+ } catch (_e) {
401
+ // logsDir does not exist yet — will be created at write time
402
+ }
403
+
404
+ if (files.length === 0) {
405
+ return path.join(logsDir, "graph-events-001.jsonl");
406
+ }
407
+
408
+ const current = path.join(logsDir, files[files.length - 1]);
409
+
410
+ // Check size and entry count of current file
411
+ let tooBig = false;
412
+ let tooMany = false;
413
+ try {
414
+ const stat = fs.statSync(current);
415
+ if (stat.size >= maxBytes) tooBig = true;
416
+ } catch (_e) { /* not exists yet — ok */ }
417
+
418
+ if (!tooBig) {
419
+ try {
420
+ const content = fs.readFileSync(current, "utf8");
421
+ const entryCount = content.split("\n").filter((l) => l.trim().length > 0).length;
422
+ if (entryCount >= maxEntries) tooMany = true;
423
+ } catch (_e) { /* ok */ }
424
+ }
425
+
426
+ if (!tooBig && !tooMany) return current;
427
+
428
+ // Need a new file: increment the sequence number
429
+ const lastFile = files[files.length - 1];
430
+ const match = lastFile.match(/^graph-events-(\d+)\.jsonl$/);
431
+ const nextNum = match ? parseInt(match[1], 10) + 1 : 1;
432
+ const nextName = `graph-events-${String(nextNum).padStart(3, "0")}.jsonl`;
433
+ return path.join(logsDir, nextName);
434
+ }
435
+
436
+ /**
437
+ * append_ledger_line — fail-open ledger append.
438
+ *
439
+ * Writes one JSON line to `graphDB/logs/graph-events-NNN.jsonl`.
440
+ * Honors `GSDT_GRAPH_TELEMETRY`: default ON; `"0"` → OFF (zero lines written).
441
+ * Sized rotation backstop: 50 MB OR 250,000 entries (test-overridable via env vars).
442
+ * A throw inside the sink is SWALLOWED and never propagates.
443
+ *
444
+ * [RULE] fail-open-telemetry
445
+ * [RULE] rollover-boundary-proven
446
+ *
447
+ * @param {object} record
448
+ * @param {string} [projectRoot] — optional override for tests
449
+ */
450
+ function append_ledger_line(record, projectRoot) {
451
+ // Toggle: default ON; "0" → OFF
452
+ if (process.env.GSDT_GRAPH_TELEMETRY === "0") return;
453
+
454
+ try {
455
+ const logsDir = resolveLogsDir(projectRoot);
456
+ fs.mkdirSync(logsDir, { recursive: true });
457
+
458
+ // Test-only threshold overrides (pre-mortem #5)
459
+ const maxBytes = process.env.GSDT_GRAPH_TELEMETRY_MAXBYTES
460
+ ? parseInt(process.env.GSDT_GRAPH_TELEMETRY_MAXBYTES, 10)
461
+ : DEFAULT_MAX_BYTES;
462
+ const maxEntries = process.env.GSDT_GRAPH_TELEMETRY_MAXENTRIES
463
+ ? parseInt(process.env.GSDT_GRAPH_TELEMETRY_MAXENTRIES, 10)
464
+ : DEFAULT_MAX_ENTRIES;
465
+
466
+ const ledgerPath = _resolveLedgerPath(logsDir, maxBytes, maxEntries);
467
+ fs.appendFileSync(ledgerPath, JSON.stringify(record) + "\n");
468
+ } catch (_e) {
469
+ // FAIL-OPEN: telemetry must never block or alter any graph/grep/read decision
470
+ }
471
+ }
472
+
473
+ // ─── Exports ──────────────────────────────────────────────────────────────────
474
+
475
+ module.exports = {
476
+ resolveGraphDir,
477
+ resolveStorePath,
478
+ resolveLogsDir,
479
+ resolveScipPath,
480
+ resolveLegacyStorePath,
481
+ deriveProjectRoot,
482
+ migrateGraphStore,
483
+ append_ledger_line,
484
+ };
@@ -220,8 +220,15 @@ function _detectDefaultTrack2(projectDir, notes) {
220
220
  try { return fs.existsSync(path.join(projectDir, rel)); } catch (_) { return false; }
221
221
  };
222
222
 
223
- // typecheck — tsc
224
- if (has('node_modules/.bin/tsc') || has('tsconfig.json')) {
223
+ // typecheck — tsc. Run ONLY when tsc is actually INSTALLED (the binary exists).
224
+ // A bare `tsconfig.json` is NOT sufficient: scip-typescript auto-creates an empty
225
+ // `{}` tsconfig at the root of any project it indexes (including zero-dep plain-JS
226
+ // repos like GSD-T itself). The old `has('tsconfig.json')` OR-trigger then ran
227
+ // `npx --no-install tsc` with no tsc present → "TypeScript not installed" → a
228
+ // FALSE verify-gate FAIL on a non-TS project. A genuine TS project ships tsc in
229
+ // node_modules/.bin/, so requiring the binary cannot disable typecheck where it
230
+ // truly applies. (We still require a tsconfig so tsc has a config to read.)
231
+ if (has('node_modules/.bin/tsc') && has('tsconfig.json')) {
225
232
  plan.push({
226
233
  id: 'tsc',
227
234
  cmd: 'npx',
package/bin/gsd-t.js CHANGED
@@ -3882,9 +3882,54 @@ function doGraph(args) {
3882
3882
  case "blast-radius": { const e = _graphQueryCli(["blast-radius", args[1] || ""]); log(JSON.stringify(e, null, 2)); break; }
3883
3883
  case "body": { const e = _graphQueryCli(["body", args[1] || ""]); log(JSON.stringify(e, null, 2)); break; }
3884
3884
  case "tasks": doGraphTaskOutput(args[1] || "table"); break;
3885
+ case "metrics": { // M99 D3-T2: append-only arm — rollup the telemetry ledger
3886
+ const _metricsRollup = require("./gsd-t-graph-metrics-rollup.cjs");
3887
+ const _metricsJson = (args[1] === "--json" || args[2] === "--json");
3888
+ _metricsRollup.printRollup(undefined, { json: _metricsJson });
3889
+ break;
3890
+ }
3891
+ case "wiring-log": {
3892
+ // M99 D2: thin CLI shim for persisting a kind:'wiring' ledger line from a
3893
+ // Workflow agent() Bash call (avoids embedding require() in workflow strings,
3894
+ // which trips the M71 sandbox lint).
3895
+ //
3896
+ // Usage A (explicit mode):
3897
+ // gsd-t graph wiring-log --consumer <consumer> --mode <mode> [--project <dir>]
3898
+ //
3899
+ // Usage B (auto mode — WIRED if store exists, else fallback-announced):
3900
+ // gsd-t graph wiring-log --consumer <consumer> --auto [--project <dir>]
3901
+ //
3902
+ // Exits 0 always (fail-open).
3903
+ const _projIdx = args.indexOf("--project");
3904
+ const _projDir = _projIdx !== -1 ? args[_projIdx + 1] : process.cwd();
3905
+ const _consumerIdx = args.indexOf("--consumer");
3906
+ const _consumerArg = _consumerIdx !== -1 ? args[_consumerIdx + 1] : "cli";
3907
+ const _modeIdx = args.indexOf("--mode");
3908
+ const _modeArg = _modeIdx !== -1 ? args[_modeIdx + 1] : null;
3909
+ const _autoFlag = args.includes("--auto");
3910
+ try {
3911
+ const _resolver = require("./gsd-t-graph-store-resolver.cjs");
3912
+ let _mode;
3913
+ if (_autoFlag) {
3914
+ const _sp = _resolver.resolveStorePath(_projDir);
3915
+ _mode = (_sp && fs.existsSync(_sp)) ? "WIRED" : "fallback-announced";
3916
+ } else if (_modeArg) {
3917
+ _mode = _modeArg;
3918
+ } else {
3919
+ process.stdout.write("ledger-write-skipped: need --mode or --auto\n");
3920
+ process.exit(0);
3921
+ }
3922
+ const _record = { kind: "wiring", ts: new Date().toISOString(), consumer: _consumerArg, graphWiringMode: _mode };
3923
+ _resolver.append_ledger_line(_record, _projDir);
3924
+ process.stdout.write(`ok: ${_mode}\n`);
3925
+ } catch (_e) {
3926
+ process.stdout.write(`ledger-write-skipped: ${_e && _e.message ? _e.message : String(_e)}\n`);
3927
+ }
3928
+ process.exit(0);
3929
+ }
3885
3930
  default:
3886
3931
  error(`Unknown graph subcommand: ${sub}`);
3887
- info("Usage: gsd-t graph [index|status|query|who-imports|who-calls|blast-radius|body|tasks]");
3932
+ info("Usage: gsd-t graph [index|status|query|who-imports|who-calls|blast-radius|body|tasks|metrics|wiring-log]");
3888
3933
  info(" gsd-t graph --output json|table (task DAG)");
3889
3934
  }
3890
3935
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tekyzinc/gsd-t",
3
- "version": "4.13.11",
3
+ "version": "4.14.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",