@tekyzinc/gsd-t 4.13.12 → 4.15.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.
@@ -71,6 +71,10 @@ const fs = require("node:fs");
71
71
  const path = require("node:path");
72
72
  const os = require("node:os");
73
73
 
74
+ // ─── M99 D1: Single store resolver (routing ALL path derivations here) ────────
75
+ // [RULE] one-resolver-only — no raw .gsd-t/graph.db literals outside this module
76
+ const _resolver = require("./gsd-t-graph-store-resolver.cjs");
77
+
74
78
  // ─── ANSI colours ────────────────────────────────────────────────────────────
75
79
  const C = {
76
80
  reset: "\x1b[0m",
@@ -87,25 +91,40 @@ function colorize(str, ...codes) {
87
91
  }
88
92
 
89
93
  // ─── Store location ───────────────────────────────────────────────────────────
90
- // The store path is resolved from the environment or a default discovery path.
91
- // D3 writes to a store directory; we read from it.
92
- // Convention: GSD_T_GRAPH_STORE env var overrides; otherwise look for
93
- // .gsd-t/graph-index/ relative to cwd.
94
+ // ALL path resolution routes through the M99 D1 resolver module.
95
+ // [RULE] one-resolver-only no raw .gsd-t/graph.db literals in this file.
96
+ //
97
+ // Discovery order (per resolver contract):
98
+ // 1. GSD_T_GRAPH_STORE env var override
99
+ // 2. cwd-walk: .gsd-t/graphDB/graph.db (new layout post-M99)
100
+ // 3. cwd-walk: .gsd-t/graph.db (legacy — triggers self-heal migration via migrateGraphStore)
101
+ // 4. cwd-walk: .gsd-t/graph-index/ (JSONL baseline)
94
102
 
95
103
  function resolveStorePath() {
96
104
  if (process.env.GSD_T_GRAPH_STORE) {
97
105
  return process.env.GSD_T_GRAPH_STORE;
98
106
  }
99
- // Walk up from cwd to find the store. K1 PICKED SQLite, so the indexer
100
- // (gsd-t-graph-index.cjs) writes `.gsd-t/graph.db`. Discover THAT first; fall
101
- // back to the legacy `.gsd-t/graph-index/` JSONL directory for the baseline
102
- // store. (The original CLI only looked for the JSONL dir — so a freshly-built
103
- // SQLite index reported graph-unavailable: the index wrote .gsd-t/graph.db but
104
- // the query never discovered it.)
107
+ // Walk up from cwd to find the store.
108
+ // Check the NEW graphDB layout first; fall back to legacy for self-heal;
109
+ // then JSONL for the baseline store. This is THE primary discovery path —
110
+ // post-migration it MUST find graphDB/graph.db, not the legacy .gsd-t/graph.db.
111
+ // [RULE] discovery-loop-end-to-end (pre-mortem #1)
105
112
  let dir = process.cwd();
106
113
  for (let i = 0; i < 10; i++) {
107
- const dbPath = path.join(dir, ".gsd-t", "graph.db");
108
- if (fs.existsSync(dbPath)) return dbPath;
114
+ // New layout: .gsd-t/graphDB/graph.db (M99+)
115
+ const newDbPath = _resolver.resolveStorePath(dir);
116
+ if (fs.existsSync(newDbPath)) return newDbPath;
117
+ // Legacy layout: check via resolver's legacy path helper (no raw literals here)
118
+ const legacyDbPath = _resolver.resolveLegacyStorePath(dir); // [RULE] one-resolver-only
119
+ if (fs.existsSync(legacyDbPath)) {
120
+ // Self-heal: migrate to graphDB/ on first touch (T2 wire-in)
121
+ try { _resolver.migrateGraphStore(dir); } catch (_e) { /* fail-open */ }
122
+ // After migration, new path should exist; fall through to next iteration
123
+ if (fs.existsSync(newDbPath)) return newDbPath;
124
+ // Migration didn't complete (or skipped) — return legacy for backward compat
125
+ return legacyDbPath;
126
+ }
127
+ // JSONL baseline (pre-SQLite projects)
109
128
  const jsonlDir = path.join(dir, ".gsd-t", "graph-index");
110
129
  if (fs.existsSync(jsonlDir)) return jsonlDir;
111
130
  const parent = path.dirname(dir);
@@ -221,12 +240,14 @@ function loadJsonlStore(storePath) {
221
240
  function loadSkippedFiles(storePath) {
222
241
  if (!storePath) return new Set();
223
242
  try {
224
- // The SQLite DB is written by D3 at .gsd-t/graph.db (adjacent to graph-index/ dir).
225
- // storePath may be .gsd-t/graph-index/ (JSONL) or the db path itself.
243
+ // storePath may be .gsd-t/graph-index/ (JSONL) or a .db path itself.
244
+ // All db path resolution goes through the M99 resolver. [RULE] one-resolver-only
226
245
  let dbPath = null;
227
- // Try storePath as a directory: look for graph.db one level up (sibling of graph-index/)
246
+ // Try storePath as a directory: look for graph.db via resolver (new graphDB/ layout first)
228
247
  if (fs.existsSync(storePath) && fs.statSync(storePath).isDirectory()) {
229
- const candidate = path.join(path.dirname(storePath), "graph.db");
248
+ // Derive project root from the JSONL dir (2-up), then resolve via resolver
249
+ const projRoot = _resolver.deriveProjectRoot(storePath);
250
+ const candidate = _resolver.resolveStorePath(projRoot);
230
251
  if (fs.existsSync(candidate)) dbPath = candidate;
231
252
  } else if (storePath.endsWith(".db") && fs.existsSync(storePath)) {
232
253
  dbPath = storePath;
@@ -456,8 +477,13 @@ function loadStore(storePath) {
456
477
  if (!storePath) return { ok: false, reason: "graph-unavailable" };
457
478
 
458
479
  // SQLite is the PICKED store (K1) — read it first; fall back to JSONL baseline.
480
+ // Sibling-db fallback uses resolver (not a raw path.join). [RULE] one-resolver-only
481
+ const _siblingDb = storePath.endsWith(".db") ? null : (() => {
482
+ const projRoot = _resolver.deriveProjectRoot(storePath);
483
+ return _resolver.resolveStorePath(projRoot);
484
+ })();
459
485
  const loaded = (storePath.endsWith(".db") ? loadSqliteStore(storePath) : loadJsonlStore(storePath))
460
- || loadJsonlStore(storePath) || (storePath.endsWith(".db") ? null : loadSqliteStore(path.join(path.dirname(storePath), "graph.db")));
486
+ || loadJsonlStore(storePath) || (_siblingDb && fs.existsSync(_siblingDb) ? loadSqliteStore(_siblingDb) : null);
461
487
  if (!loaded) return { ok: false, reason: "graph-unavailable" };
462
488
 
463
489
  try {
@@ -507,13 +533,10 @@ function runFreshnessCheck(storePath) {
507
533
 
508
534
  try {
509
535
  // D4's real signatures take (db, projectRoot, ...) — NOT (storePath). The
510
- // store path is `.gsd-t/graph.db`; projectRoot is the repo containing `.gsd-t`.
511
- // (Earlier this called compute_touched_files(storePath) / freshness_check_on_query
512
- // (touched, storePath) a cross-domain signature mismatch that threw on every
513
- // real query: freshness_check_on_query read `.edits` off the storePath string.)
514
- const projectRoot = storePath.endsWith(".db")
515
- ? path.dirname(path.dirname(storePath)) // .gsd-t/graph.db → repo root
516
- : path.dirname(path.dirname(storePath)); // .gsd-t/graph-index → repo root
536
+ // projectRoot is derived via the M99 resolver (branch-aware depth fix).
537
+ // [RULE] projectroot-depth-corrected-with-move [RULE] jsonl-branch-depth-preserved
538
+ // Pre-mortem #3: .db branch is 3-up post-graphDB/ move; JSONL branch stays 2-up.
539
+ const projectRoot = _resolver.deriveProjectRoot(storePath);
517
540
  // Pass the exact storePath (canonical `.gsd-t/graph.db`) so openDb opens the
518
541
  // real store, not a re-derived nested path. projectRoot is still used for the
519
542
  // working-tree walk.
@@ -529,7 +552,18 @@ function runFreshnessCheck(storePath) {
529
552
  let parseAndPut = null;
530
553
  try { parseAndPut = require(path.join(__dirname, "gsd-t-graph-index.cjs")).parse_and_put; } catch { /* optional */ }
531
554
  freshnessModule.freshness_check_on_query(db, projectRoot, touched, parseAndPut);
532
- return { ok: true };
555
+ // Surface the touched-set so the query CLI can record freshness telemetry:
556
+ // edits = indexed files whose content-hash drifted (re-indexed before answering —
557
+ // i.e. a STALE query that was refreshed), adds = new files, deletes = removed.
558
+ // [RULE] freshness-telemetry-surfaced
559
+ const t = touched || {};
560
+ return {
561
+ ok: true,
562
+ editsCount: Array.isArray(t.edits) ? t.edits.length : 0,
563
+ addsCount: Array.isArray(t.adds) ? t.adds.length : 0,
564
+ deletesCount: Array.isArray(t.deletes) ? t.deletes.length : 0,
565
+ reindexedFiles: Array.isArray(t.edits) ? t.edits.slice(0, 20) : [],
566
+ };
533
567
  } catch (_e) {
534
568
  return { ok: false, reason: "graph-unavailable" };
535
569
  }
@@ -1216,7 +1250,60 @@ if (require.main === module) {
1216
1250
  const verb = args[0];
1217
1251
  const target = args[1];
1218
1252
 
1253
+ // ─── Graph usage telemetry (M99, layer 1) ─────────────────────────────────
1254
+ // Every graph read funnels through emit(); append ONE JSON line per query via
1255
+ // the resolver's append_ledger_line to graphDB/logs/graph-events-NNN.jsonl so
1256
+ // graph usage + health is observable (hit/miss/graph-unavailable, tier, latency,
1257
+ // reindex). This is the chokepoint: one ledger captures 100% of graph reads
1258
+ // regardless of consumer. (M99 moved the sink here from the retired
1259
+ // .gsd-t/metrics/graph-events.jsonl path — see gsd-t-graph-store-resolver.cjs.)
1260
+ // FAIL-OPEN — a telemetry write must NEVER block or alter a query answer.
1261
+ // Consumer label comes from GSDT_GRAPH_CONSUMER (set by the calling workflow/hook);
1262
+ // defaults to "cli" for a bare invocation. [RULE] graph-telemetry-fail-open
1263
+ const _t0 = Date.now();
1264
+ let _telemStorePath = null; // set when storePath resolves; avoids const TDZ in the logger
1265
+ let _telemFreshness = null; // { reindexedCount, staleOnQuery, addsCount, deletesCount, reindexedFiles }
1266
+ // ─── Layer-1 sink: moved to graphDB/logs/ via the M99 resolver sink ──────────
1267
+ // Record SHAPE is KEPT byte-for-byte (kind:"query" + all fields from graph-metrics-contract.md).
1268
+ // Only the sink path + rotation/toggle is the supersede (no Divergence flag).
1269
+ // [RULE] layer1-shape-kept [RULE] fail-open-telemetry
1270
+ function _logGraphEvent(envelope) {
1271
+ try {
1272
+ const sp = (typeof _telemStorePath === "string" && _telemStorePath) ? _telemStorePath : null;
1273
+ // Derive projectRoot via the resolver (branch-aware). If no storePath yet, use cwd.
1274
+ const projRoot = sp ? _resolver.deriveProjectRoot(sp) : process.cwd();
1275
+ const e = envelope || {};
1276
+ const outcome = e.ok
1277
+ ? (Array.isArray(e.results) && e.results.length === 0 ? "hit-empty" : "hit")
1278
+ : (e.reason || "error");
1279
+ const rec = {
1280
+ kind: "query",
1281
+ ts: new Date().toISOString(),
1282
+ verb: verb || null,
1283
+ target: target || null,
1284
+ outcome,
1285
+ tier: e.tier || null,
1286
+ resultCount: Array.isArray(e.results) ? e.results.length
1287
+ : (typeof e.fileCount === "number" ? e.fileCount : null),
1288
+ candidateCount: Array.isArray(e.candidates) ? e.candidates.length : null,
1289
+ latencyMs: Date.now() - _t0,
1290
+ consumer: process.env.GSDT_GRAPH_CONSUMER || "cli",
1291
+ via: process.env.GSDT_GRAPH_VIA || null,
1292
+ // Freshness signal: did this query run against code that had changed?
1293
+ // reindexedCount>0 ⇒ a STALE query that was auto-refreshed before answering.
1294
+ staleOnQuery: _telemFreshness ? _telemFreshness.staleOnQuery : null,
1295
+ reindexedCount: _telemFreshness ? _telemFreshness.reindexedCount : null,
1296
+ addsCount: _telemFreshness ? _telemFreshness.addsCount : null,
1297
+ deletesCount: _telemFreshness ? _telemFreshness.deletesCount : null,
1298
+ reindexedFiles: (_telemFreshness && _telemFreshness.reindexedCount) ? _telemFreshness.reindexedFiles : null,
1299
+ };
1300
+ // Delegate to the shared sink (graphDB/logs/ + rotation + GSDT_GRAPH_TELEMETRY toggle)
1301
+ _resolver.append_ledger_line(rec, projRoot);
1302
+ } catch { /* fail-open: telemetry never blocks a query */ }
1303
+ }
1304
+
1219
1305
  function emit(envelope) {
1306
+ _logGraphEvent(envelope);
1220
1307
  process.stdout.write(JSON.stringify(envelope) + "\n");
1221
1308
  }
1222
1309
 
@@ -1239,9 +1326,23 @@ if (require.main === module) {
1239
1326
  }
1240
1327
 
1241
1328
  const storePath = resolveStorePath();
1329
+ _telemStorePath = storePath;
1242
1330
 
1243
1331
  // ── Step 1: D4 freshness check INLINE — [RULE] stale-file-reindexed-before-answer ──
1244
1332
  const freshnessResult = runFreshnessCheck(storePath);
1333
+ // Capture freshness telemetry: how many files were stale + re-indexed on THIS query.
1334
+ // This is the "freshness reindexing on code updates" + "stale query" signal — a query
1335
+ // that triggered a re-index means the caller queried against code that had changed.
1336
+ try {
1337
+ const r = freshnessResult || {};
1338
+ _telemFreshness = {
1339
+ reindexedCount: typeof r.editsCount === "number" ? r.editsCount : 0,
1340
+ staleOnQuery: (typeof r.editsCount === "number" ? r.editsCount : 0) > 0,
1341
+ addsCount: typeof r.addsCount === "number" ? r.addsCount : 0,
1342
+ deletesCount: typeof r.deletesCount === "number" ? r.deletesCount : 0,
1343
+ reindexedFiles: Array.isArray(r.reindexedFiles) ? r.reindexedFiles : [],
1344
+ };
1345
+ } catch { /* fail-open */ }
1245
1346
  if (!freshnessResult.ok) {
1246
1347
  fail({ ok: false, reason: "graph-unavailable" });
1247
1348
  }
@@ -1265,20 +1366,21 @@ if (require.main === module) {
1265
1366
  const queryResult = queryWhoCalls(index, target);
1266
1367
  if (queryResult.ambiguous) {
1267
1368
  // [RULE] who-calls-function-identity-disambiguated
1268
- process.stdout.write(JSON.stringify({
1369
+ emit({
1269
1370
  ok: false,
1270
1371
  reason: "ambiguous-function",
1271
1372
  verb,
1272
1373
  target,
1273
1374
  candidates: queryResult.candidates,
1274
- }) + "\n");
1375
+ });
1275
1376
  process.exit(2);
1276
1377
  }
1277
1378
  emit({ ok: true, verb, target, results: queryResult.results, tier: queryResult.tier, coverage: queryResult.coverage });
1278
1379
 
1279
1380
  } else if (verb === "body") {
1280
1381
  if (!target) fail({ ok: false, reason: "missing-target", verb });
1281
- const projectRoot = path.dirname(path.dirname(storePath));
1382
+ // [RULE] projectroot-depth-corrected-with-move — resolver is branch-aware
1383
+ const projectRoot = _resolver.deriveProjectRoot(storePath);
1282
1384
  let bodyResult = queryBody(index, target, projectRoot);
1283
1385
 
1284
1386
  // [RULE] body-end-line-required — a pre-M98 node lacks end_line and its file
@@ -1313,9 +1415,7 @@ if (require.main === module) {
1313
1415
 
1314
1416
  if (bodyResult.ambiguous) {
1315
1417
  // [RULE] body-ambiguous-never-merged
1316
- process.stdout.write(JSON.stringify({
1317
- ok: false, reason: "ambiguous-function", verb, target, candidates: bodyResult.candidates,
1318
- }) + "\n");
1418
+ emit({ ok: false, reason: "ambiguous-function", verb, target, candidates: bodyResult.candidates });
1319
1419
  process.exit(2);
1320
1420
  }
1321
1421
  if (bodyResult.notFound) fail({ ok: false, reason: "not-found", verb, target, file: bodyResult.file });
@@ -44,6 +44,10 @@
44
44
  const fs = require('fs');
45
45
  const path = require('path');
46
46
  const { execSync, spawnSync } = require('child_process');
47
+ // Red Team HIGH (M99 round 4 — SC#1): SCIP indexes must live under .gsd-t/graphDB/,
48
+ // not loose in .gsd-t/. Route every index.scip / index-python.scip path through the
49
+ // single resolver. [RULE] scip-index-under-graphdb
50
+ const { resolveScipPath } = require('./gsd-t-graph-store-resolver.cjs');
47
51
 
48
52
  // ── ANSI helpers ─────────────────────────────────────────────────────────────
49
53
 
@@ -133,7 +137,11 @@ function _resetScipCache(override) {
133
137
  function runScipTypescript(projectRoot, outPath) {
134
138
  // M95: emit to a REAL file (outPath) so the index can be READ and call edges
135
139
  // resolved — not /dev/null. The old /dev/null path only proved invocability.
136
- const out = outPath || path.join(projectRoot, '.gsd-t', 'index.scip');
140
+ const out = outPath || resolveScipPath('index.scip', projectRoot);
141
+ // Ensure the output dir (now .gsd-t/graphDB/) exists — scip-typescript's
142
+ // --output open() fails if the parent dir is absent (the old .gsd-t/ always
143
+ // existed; graphDB/ may not on a fresh build). [RULE] scip-index-under-graphdb
144
+ try { fs.mkdirSync(path.dirname(out), { recursive: true }); } catch {}
137
145
  // --infer-tsconfig: resolve a tsconfig even when one isn't at the repo root
138
146
  // (monorepos / nested layouts — e.g. web/tsconfig.json). Without it, projects
139
147
  // whose tsconfig lives in a subdir got 0 resolved call edges. [RULE] scip-infer-nested-tsconfig
@@ -149,7 +157,8 @@ function runScipTypescript(projectRoot, outPath) {
149
157
  function runScipPython(projectRoot, outPath) {
150
158
  // Emit to a REAL file (separate from the TS index) so it can be READ + merged.
151
159
  // --project-name is required-ish for stable symbols; derive from the dir name.
152
- const out = outPath || path.join(projectRoot, '.gsd-t', 'index-python.scip');
160
+ const out = outPath || resolveScipPath('index-python.scip', projectRoot);
161
+ try { fs.mkdirSync(path.dirname(out), { recursive: true }); } catch {} // graphDB/ may not exist yet
153
162
  const projName = path.basename(projectRoot).replace(/[^A-Za-z0-9_-]/g, '-') || 'project';
154
163
  // scip-python REQUIRES a project version — it crashes (makeModuleInit) when the
155
164
  // version is undefined (no pyproject.toml). Pass an explicit fallback version.
@@ -275,13 +284,13 @@ function buildScipResolver(repoRoot, opts = {}) {
275
284
 
276
285
  if (avail.typescript && langs.typescript) {
277
286
  try {
278
- const run = runScipTypescript(repoRoot, path.join(repoRoot, '.gsd-t', 'index.scip'));
287
+ const run = runScipTypescript(repoRoot, resolveScipPath('index.scip', repoRoot));
279
288
  if (run && run.ok) { mergeRead(readScipIndex(run.scipPath)); ranIndexers.push('typescript'); }
280
289
  } catch { /* degrade to floor for TS */ }
281
290
  }
282
291
  if (avail.python && langs.python) {
283
292
  try {
284
- const run = runScipPython(repoRoot, path.join(repoRoot, '.gsd-t', 'index-python.scip'));
293
+ const run = runScipPython(repoRoot, resolveScipPath('index-python.scip', repoRoot));
285
294
  if (run && run.ok) { mergeRead(readScipIndex(run.scipPath)); ranIndexers.push('python'); }
286
295
  } catch { /* degrade to floor for Python */ }
287
296
  }
@@ -326,7 +335,7 @@ function buildScipResolver(repoRoot, opts = {}) {
326
335
  return { edges: out, resolved };
327
336
  }
328
337
 
329
- return { ok: true, indexers: ranIndexers, scipPath: path.join(repoRoot, '.gsd-t', 'index.scip'), resolveFileEdges };
338
+ return { ok: true, indexers: ranIndexers, scipPath: resolveScipPath('index.scip', repoRoot), resolveFileEdges };
330
339
  }
331
340
 
332
341
  /** No-op resolver used when SCIP is unavailable (floor mode). */
@@ -234,7 +234,7 @@ function candidateJsonl(dir, graph) {
234
234
 
235
235
  // ─── Candidate: SQLite (better-sqlite3 recursive CTE) ───────────────────
236
236
  function candidateSqlite(dir, graph) {
237
- const dbPath = path.join(dir, "graph.db");
237
+ const dbPath = path.join(dir, "graph.db"); // spike-local-store: throwaway bench dir
238
238
 
239
239
  const { mod: Database, err: sqliteErr } = tryRequire("better-sqlite3");
240
240
  if (!Database) {