@tekyzinc/gsd-t 5.16.12 → 5.17.11

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,88 @@
2
2
 
3
3
  All notable changes to GSD-T are documented here. Updated with each release.
4
4
 
5
+ ## [5.17.11] - 2026-09-01
6
+
7
+ ### Fixed — every arrow-function export was invisible to the code graph
8
+
9
+ `who-calls logAudit` answered "no callers" for a function called throughout a
10
+ server, while `verifyToken` — three lines away in the same directory — resolved
11
+ fine. The difference was how they were declared:
12
+
13
+ - `export function verifyToken()` → SCIP emits a METHOD descriptor, `verifyToken().`
14
+ - `export const logAudit = async () => {}` → SCIP emits a TERM descriptor, `logAudit.`
15
+
16
+ TypeScript treats an arrow function assigned to a const as a variable, so it gets
17
+ a term. `funcNameFromSymbol` required the `().` shape and discarded everything
18
+ else — on a real server that meant **3,584 term symbols dropped against 1,624
19
+ kept**, more than twice as much data thrown away as used.
20
+
21
+ The references were in the SCIP index the whole time; GSD-T was deleting them on
22
+ read. This was diagnosed in the field as a "scip-typescript coverage limitation" —
23
+ it was not, it was ours.
24
+
25
+ Non-callable terms (`Array.`, imported types) are now admitted and cost nothing:
26
+ a name only resolves when a call site is looking for that exact name, so a type
27
+ reference never matches one. Parameters (`name().(p)`) stay excluded — they end
28
+ in `)`, not `.`.
29
+
30
+ Measured on the same repo, on top of 5.17.10: `server/src/index.ts` went from 30
31
+ to **116** unique resolved symbols, compiler-accurate files 38 → **130**, call
32
+ edges into `server/` 173 → **591**. `logAudit`, `withTransaction` and
33
+ `getAuditContext` all return correct callers, verified against the source.
34
+
35
+ - `bin/gsd-t-scip-reader.cjs`: accept term descriptors alongside method ones
36
+ - `test/m114-nested-tsconfig-graph.test.js`: 4 more tests, mutation-tested
37
+
38
+ **Re-index to pick this up**: `gsd-t graph index`.
39
+
40
+ ## [5.17.10] - 2026-09-01
41
+
42
+ ### Fixed — the code graph indexed one TypeScript project and called it the whole repo
43
+
44
+ Found in a live TimeTracking session: 8 of 245 files indexed, 96 of 50,283 call
45
+ edges resolved (0.2%, all same-file), and not one backend call among them. The
46
+ root `tsconfig.json`'s `include` listed frontend paths only, so `server/src/`
47
+ was never compiled and every backend `who-calls` returned empty.
48
+
49
+ **Three defects, and fixing any one alone changes nothing.**
50
+
51
+ 1. **One indexer run at the repo root.** `--infer-tsconfig` does not cover this —
52
+ it finds *a* tsconfig when the root has none, but still emits one index from
53
+ one root. Now: one run per tsconfig project, merged.
54
+
55
+ 2. **Paths keyed to where the indexer ran.** Indexing `server/` emits
56
+ `src/index.ts` while the graph stores `server/src/index.ts`, so a second run
57
+ *without* re-prefixing resolves nothing while looking like a fix. The prefix
58
+ has to apply to the funcId values too (`relPath#name`), not just the map keys.
59
+
60
+ 3. **`who-calls` claimed `coverage.complete: true` while returning zero.**
61
+ Coverage counted parse failures only; a file that parsed but was never
62
+ compiler-resolved is equally invisible to a call query and never lands in
63
+ `skippedFiles`. "Nothing calls this" is what gets read right before a rename
64
+ or a delete — and it was firing on auth middleware.
65
+
66
+ Why (3) hid so long: the per-file record handed to the query layer was
67
+ `{file, entities, edges}` with no tier field, so the query layer structurally
68
+ could not tell a resolved file from an unresolved one.
69
+
70
+ Import queries deliberately still report complete at floor tier — import edges
71
+ come from the parse, so `who-imports` and `blast-radius` are genuinely complete
72
+ there, and flagging them would train you to ignore the field.
73
+
74
+ Measured on TimeTracking: 8 → 40 indexed files, 0 → 173 resolved call edges into
75
+ `server/`, 96 → 390 overall, `server/` 0 → 13 of 19 files compiler-accurate.
76
+ Affects 13 registered projects with nested tsconfigs (binvoice 1→5 projects,
77
+ newman 1→7, Tekyz-CRM 1→2).
78
+
79
+ - `bin/gsd-t-graph-scip-upgrade.cjs`: `findTsProjectDirs()` + one indexer run per project
80
+ - `bin/gsd-t-scip-reader.cjs`: `pathPrefix` re-roots fileRefs keys AND funcIds
81
+ - `bin/gsd-t-graph-query-cli.cjs`: per-file tier carried onto records + tier-aware coverage
82
+ - `test/m114-nested-tsconfig-graph.test.js`: 12 tests, each mutation-tested
83
+
84
+ **Re-index to pick this up**: `gsd-t graph index` in any project with a nested
85
+ tsconfig. A graph built before this version under-reports call edges.
86
+
5
87
  ## [5.16.12] - 2026-08-29
6
88
 
7
89
  ### Fixed — a retired hook fired on every tool call, on every machine, since M61
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # GSD-T: Contract-Driven Development for Claude Code
2
2
 
3
- **v5.16.12** - A methodology for reliable, parallelizable development using Claude Code with optional Agent Teams support.
3
+ **v5.17.11** - 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.
@@ -295,16 +295,51 @@ function loadSkippedFiles(storePath) {
295
295
  * @param {Set<string>} skippedFiles — files that failed to parse (D3-T6)
296
296
  * @returns {{ complete: boolean, unparsedContributors?: number, note?: string }}
297
297
  */
298
- function computeCoverage(skippedFiles) {
299
- if (!skippedFiles || skippedFiles.size === 0) {
300
- return { complete: true };
298
+ function computeCoverage(skippedFiles, opts = {}) {
299
+ const n = (skippedFiles && skippedFiles.size) || 0;
300
+
301
+ // M114 — an UNRESOLVED file is as invisible to who-calls as an unparsed one.
302
+ // Parsing a file yields its call SITES; only compiler-accurate resolution says
303
+ // WHICH function each site targets. A floor-tier file parses fine, contributes
304
+ // zero resolved call edges, and never lands in skippedFiles — so coverage used
305
+ // to report complete:true while who-calls returned empty for an entire backend.
306
+ // "Nothing calls this" is what gets read right before a rename or a delete, so
307
+ // a false all-clear here is worse than no answer.
308
+ // Applies to CALL-edge queries only: IMPORT edges come from the parse, so
309
+ // who-imports and blast-radius are genuinely complete at floor tier.
310
+ const unresolved = opts.callEdgesUnresolved ? (opts.unresolvedFiles || 0) : 0;
311
+
312
+ if (n === 0 && !unresolved) return { complete: true };
313
+
314
+ const notes = [];
315
+ if (n) notes.push(`${n} file(s) unparsed`);
316
+ if (unresolved) {
317
+ notes.push(`${unresolved} file(s) parsed but not compiler-resolved — ` +
318
+ `call edges there are unknown, not absent`);
301
319
  }
302
- const n = skippedFiles.size;
303
- return {
320
+ const coverage = {
304
321
  complete: false,
305
- unparsedContributors: n,
306
- note: `result may be incomplete — ${n} file(s) unparsed`,
322
+ note: `result may be incomplete — ${notes.join('; ')}`,
307
323
  };
324
+ if (n) coverage.unparsedContributors = n;
325
+ if (unresolved) coverage.unresolvedContributors = unresolved;
326
+ return coverage;
327
+ }
328
+
329
+ /**
330
+ * Count files whose tier is NOT compiler-accurate — i.e. files that parsed but
331
+ * whose call targets were never resolved. These are invisible to who-calls.
332
+ *
333
+ * @param {{fileTier?: Map<string,string>}} index
334
+ * @returns {number}
335
+ */
336
+ function countUnresolvedFiles(index) {
337
+ if (!index || !index.fileTier) return 0;
338
+ let n = 0;
339
+ for (const tier of index.fileTier.values()) {
340
+ if (tier && tier !== 'compiler-accurate') n++;
341
+ }
342
+ return n;
308
343
  }
309
344
 
310
345
  /**
@@ -332,6 +367,9 @@ function buildIndex(records, skippedFiles) {
332
367
  const funcEntities = new Map();
333
368
  /** @type {Set<string>} */
334
369
  const allFiles = new Set();
370
+ /** @type {Map<string,string>} file → tier (M114: who-calls needs the TARGET
371
+ * file's tier, not just the repo-wide dominant one). */
372
+ const fileTier = new Map();
335
373
 
336
374
  let dominantTier = "compiler-accurate";
337
375
  let hasFloor = false;
@@ -339,6 +377,7 @@ function buildIndex(records, skippedFiles) {
339
377
 
340
378
  for (const rec of records) {
341
379
  allFiles.add(rec.file);
380
+ if (rec.tier) fileTier.set(rec.file, rec.tier);
342
381
 
343
382
  if (rec.tier === "tree-sitter-floor") hasFloor = true;
344
383
  if (rec.tier === "tree-sitter-floor-STALE-SCIP") hasStaleScip = true;
@@ -384,6 +423,7 @@ function buildIndex(records, skippedFiles) {
384
423
  forwardCallEdges,
385
424
  funcEntities,
386
425
  allFiles,
426
+ fileTier,
387
427
  tier: dominantTier,
388
428
  skippedFiles: skippedFiles instanceof Set ? skippedFiles : new Set(),
389
429
  };
@@ -516,6 +556,17 @@ function loadSqliteStore(dbPath) {
516
556
  for (const n of nodes) {
517
557
  if (n.func_id) rec(n.file).entities.push({ funcId: n.func_id, name: n.name, file: n.file, tier: n.tier, endLine: n.end_line });
518
558
  }
559
+ // M114 — carry each file's TIER onto its record. Without this the record is
560
+ // {file, entities, edges} with no tier, so the query layer cannot tell a
561
+ // compiler-resolved file from an unresolved one and coverage cannot report
562
+ // incompleteness. A file is only compiler-accurate if every entity is; a
563
+ // single floor entity means some call target in it is unknown.
564
+ for (const n of nodes) {
565
+ if (!n.file) continue;
566
+ const r = rec(n.file);
567
+ if (n.tier && n.tier !== 'compiler-accurate') r.tier = n.tier;
568
+ else if (!r.tier) r.tier = n.tier || 'compiler-accurate';
569
+ }
519
570
  for (const e of edges) {
520
571
  // src for an IMPORT edge is the source FILE; for a CALL edge it's a funcId
521
572
  // (file#fn@line). The owning file record is the src's file part.
@@ -676,7 +727,7 @@ function queryWhoImports(index, target) {
676
727
  */
677
728
  function queryWhoCalls(index, identity) {
678
729
  const isFuncId = identity.includes("#");
679
- const coverage = computeCoverage(index.skippedFiles);
730
+ const coverage = computeCoverage(index.skippedFiles, { callEdgesUnresolved: true, unresolvedFiles: countUnresolvedFiles(index) });
680
731
 
681
732
  if (isFuncId) {
682
733
  // File-qualified identity — exact funcId lookup (tolerate @line suffix:
@@ -909,7 +960,7 @@ function queryBlastRadius(index, target) {
909
960
  }
910
961
 
911
962
  const results = Array.from(visited).sort();
912
- const coverage = computeCoverage(index.skippedFiles);
963
+ const coverage = computeCoverage(index.skippedFiles, { callEdgesUnresolved: true, unresolvedFiles: countUnresolvedFiles(index) });
913
964
  return { results, tier: index.tier, coverage };
914
965
  }
915
966
 
@@ -1295,6 +1346,7 @@ module.exports = {
1295
1346
  queryDangling,
1296
1347
  queryTestImpl,
1297
1348
  computeCoverage,
1349
+ countUnresolvedFiles,
1298
1350
  loadStore,
1299
1351
  runFreshnessCheck,
1300
1352
  resolveStorePath,
@@ -243,6 +243,59 @@ function isRustCrossCrateEdge(edge, relPath) {
243
243
  return false;
244
244
  }
245
245
 
246
+ // ── TypeScript project discovery (M114) ──────────────────────────────────────
247
+
248
+ /**
249
+ * Find every TypeScript project root in the repo — each directory holding a
250
+ * `tsconfig.json`.
251
+ *
252
+ * Why: scip-typescript indexes ONE tsconfig per run, and a tsconfig's `include`
253
+ * governs what it can see. A repo whose root tsconfig covers only the frontend
254
+ * indexes only the frontend; `server/src/` is never compiled, so not one backend
255
+ * call resolves and `who-calls` answers empty for the entire backend.
256
+ * `--infer-tsconfig` does NOT cover this — it finds *a* tsconfig when the root
257
+ * has none, but still produces a single index from a single root.
258
+ *
259
+ * Returns repo-relative dirs, root first (`''` for the repo root itself), so the
260
+ * root project's symbols win when two projects define the same name.
261
+ *
262
+ * A nested project inside another's `include` gets indexed twice; that is
263
+ * harmless — the second read overwrites identical keys with identical values.
264
+ *
265
+ * [RULE] scip-indexes-every-tsconfig-project
266
+ *
267
+ * @param {string} repoRoot
268
+ * @returns {string[]} repo-relative project dirs, e.g. ['', 'server']
269
+ */
270
+ function findTsProjectDirs(repoRoot) {
271
+ const SKIP = new Set(['node_modules', '.git', 'dist', 'build', 'out', '.venv', 'venv',
272
+ 'site-packages', '__pycache__', '.next', 'coverage', 'Pods', '.dart_tool', 'vendor']);
273
+ function isSkip(name) {
274
+ if (SKIP.has(name)) return true;
275
+ return ['dist', 'build', 'out'].some(p => name.length > p.length && name.startsWith(p) &&
276
+ (name[p.length] === '-' || name[p.length] === '.' || name[p.length] === '_'));
277
+ }
278
+ const found = [];
279
+ function walk(dir, depth) {
280
+ if (depth > 4) return; // deep enough for monorepo packages/*/x
281
+ let entries;
282
+ try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch { return; }
283
+ for (const e of entries) {
284
+ if (e.isFile() && e.name === 'tsconfig.json') {
285
+ const rel = path.relative(repoRoot, dir).split(path.sep).join('/');
286
+ found.push(rel);
287
+ }
288
+ }
289
+ for (const e of entries) {
290
+ if (e.isDirectory() && !isSkip(e.name)) walk(path.join(dir, e.name), depth + 1);
291
+ }
292
+ }
293
+ walk(repoRoot, 0);
294
+ // Root ('') first so its symbols are read before nested ones.
295
+ found.sort((a, b) => (a === '' ? -1 : b === '' ? 1 : a.localeCompare(b)));
296
+ return found;
297
+ }
298
+
246
299
  // ── Repo-level SCIP resolver (M95) ────────────────────────────────────────────
247
300
 
248
301
  /**
@@ -282,11 +335,32 @@ function buildScipResolver(repoRoot, opts = {}) {
282
335
  }
283
336
  }
284
337
 
338
+ // TypeScript: one indexer run PER tsconfig project, each re-prefixed back to
339
+ // repo-root-relative paths before merging. A single root run misses every
340
+ // nested project (see findTsProjectDirs), and a second run without the prefix
341
+ // resolves nothing — both halves are required for either to work.
342
+ const tsProjects = [];
285
343
  if (avail.typescript && langs.typescript) {
286
- try {
287
- const run = runScipTypescript(repoRoot, resolveScipPath('index.scip', repoRoot));
288
- if (run && run.ok) { mergeRead(readScipIndex(run.scipPath)); ranIndexers.push('typescript'); }
289
- } catch { /* degrade to floor for TS */ }
344
+ const dirs = findTsProjectDirs(repoRoot);
345
+ // No tsconfig anywhere → one root run with --infer-tsconfig (prior behavior).
346
+ const targets = dirs.length ? dirs : [''];
347
+ for (const rel of targets) {
348
+ const absRoot = rel ? path.join(repoRoot, rel) : repoRoot;
349
+ // One .scip per project — a shared filename would have each run clobber
350
+ // the last, leaving only the final project's symbols.
351
+ const outName = rel ? `index-ts-${rel.replace(/[^A-Za-z0-9_-]/g, '-')}.scip` : 'index.scip';
352
+ try {
353
+ const run = runScipTypescript(absRoot, resolveScipPath(outName, repoRoot));
354
+ if (run && run.ok) {
355
+ const read = readScipIndex(run.scipPath, rel);
356
+ if (read && read.ok) {
357
+ mergeRead(read);
358
+ tsProjects.push({ dir: rel || '.', files: read.fileRefs.size });
359
+ }
360
+ }
361
+ } catch { /* this project degrades to floor; others still contribute */ }
362
+ }
363
+ if (tsProjects.length) ranIndexers.push('typescript');
290
364
  }
291
365
  if (avail.python && langs.python) {
292
366
  try {
@@ -335,7 +409,17 @@ function buildScipResolver(repoRoot, opts = {}) {
335
409
  return { edges: out, resolved };
336
410
  }
337
411
 
338
- return { ok: true, indexers: ranIndexers, scipPath: resolveScipPath('index.scip', repoRoot), resolveFileEdges };
412
+ // tsProjects reports which tsconfig projects actually contributed refs. A
413
+ // project that ran but yielded 0 files is the signature of this bug class —
414
+ // surfaced rather than swallowed, so a re-index can be inspected.
415
+ return {
416
+ ok: true,
417
+ indexers: ranIndexers,
418
+ tsProjects,
419
+ indexedFiles: fileRefs.size,
420
+ scipPath: resolveScipPath('index.scip', repoRoot),
421
+ resolveFileEdges,
422
+ };
339
423
  }
340
424
 
341
425
  /** No-op resolver used when SCIP is unavailable (floor mode). */
@@ -495,6 +579,7 @@ if (require.main === module) {
495
579
  module.exports = {
496
580
  tryScipUpgrade,
497
581
  buildScipResolver,
582
+ findTsProjectDirs,
498
583
  detectScip,
499
584
  _resetScipCache,
500
585
  isRustCrossCrateEdge,
@@ -81,15 +81,33 @@ function loadScipProto() {
81
81
  */
82
82
  function funcNameFromSymbol(symbol) {
83
83
  if (!symbol || typeof symbol !== 'string') return null;
84
- // Only function/method occurrences (end with "()."). Parameters are "name().(p)"
85
- // they don't end with "()." so they're excluded.
86
- if (!/\(\)\.$/.test(symbol)) return null;
87
- // The callable name is the last descriptor segment. Top-level functions are
84
+
85
+ // The name is the last descriptor segment. Top-level functions are
88
86
  // ".../`file.ts`/name()."; methods are ".../`file.ts`/Class#method()." — so the
89
87
  // name follows the LAST '/' OR '#', whichever is later (methods key on '#').
90
88
  const cut = Math.max(symbol.lastIndexOf('/'), symbol.lastIndexOf('#'));
91
89
  const descriptor = cut === -1 ? symbol : symbol.slice(cut + 1);
92
- const m = descriptor.match(/^([A-Za-z_$][\w$]*)\(\)\.$/);
90
+
91
+ // TWO callable descriptor shapes, and missing the second one silently halved
92
+ // the graph. [RULE] scip-accepts-term-and-method-descriptors
93
+ //
94
+ // `name().` — a METHOD descriptor: `export function name() {}`
95
+ // `name.` — a TERM descriptor: `export const name = async () => {}`
96
+ //
97
+ // TypeScript treats an arrow function assigned to a const as a VARIABLE, so
98
+ // SCIP emits a term. Requiring "()." therefore discarded every arrow-function
99
+ // export: on a real server that was 3,584 term symbols dropped against 1,624
100
+ // kept, so `who-calls logAudit` answered "no callers" for a function called
101
+ // throughout the codebase — while `verifyToken`, three lines away but written
102
+ // as `export function`, resolved fine.
103
+ //
104
+ // Non-callable terms (`Array.`, an imported type) are admitted here and cost
105
+ // nothing: a name only resolves when a CALL SITE is looking for that exact
106
+ // name, so a type reference never matches one.
107
+ //
108
+ // Parameters (`name().(p)`) stay excluded — they end in `)`, not `.`.
109
+ let m = descriptor.match(/^([A-Za-z_$][\w$]*)\(\)\.$/); // function / method
110
+ if (!m) m = descriptor.match(/^([A-Za-z_$][\w$]*)\.$/); // term (const arrow fn)
93
111
  return m ? m[1] : null;
94
112
  }
95
113
 
@@ -118,12 +136,29 @@ function isBuildOutputPath(relPath) {
118
136
  /**
119
137
  * Decode a `.scip` file and build resolution maps.
120
138
  *
139
+ * `pathPrefix` re-roots every path in the index to be repo-root-relative.
140
+ * An indexer reports paths relative to WHERE IT RAN: indexing `server/` emits
141
+ * `src/index.ts`, while the graph stores `server/src/index.ts`. Without the
142
+ * prefix, a nested project's refs key on paths the graph has never heard of and
143
+ * resolve nothing — a second indexer run would LOOK like a fix and change
144
+ * nothing. [RULE] scip-paths-reprefixed-to-repo-root
145
+ *
146
+ * The prefix must apply to BOTH the fileRefs keys AND the funcId values, since
147
+ * a funcId is `relPath#name` — prefixing only the keys leaves every funcId
148
+ * pointing at a path the graph does not have.
149
+ *
121
150
  * @param {string} scipPath absolute path to an index.scip
151
+ * @param {string} [pathPrefix] repo-relative dir the index was produced from
152
+ * (e.g. "server"); "" or "." for the repo root
122
153
  * @returns {{ ok: true, symbolToDef: Map<string,string>,
123
154
  * fileRefs: Map<string, Array<{symbol:string, funcId:string, line:number}>> }
124
155
  * | { ok: false, reason: string }}
125
156
  */
126
- function readScipIndex(scipPath) {
157
+ function readScipIndex(scipPath, pathPrefix) {
158
+ // Normalize: "", ".", "./", "server/" and "server" all mean the same thing.
159
+ const rawPrefix = (pathPrefix || '').replace(/^\.\/+/, '').replace(/\/+$/, '');
160
+ const prefix = (rawPrefix === '' || rawPrefix === '.') ? '' : rawPrefix + '/';
161
+ const reroot = prefix ? (p) => prefix + p : (p) => p;
127
162
  const proto = loadScipProto();
128
163
  if (!proto) {
129
164
  return { ok: false, reason: 'scip-decoder-unavailable' };
@@ -147,22 +182,24 @@ function readScipIndex(scipPath) {
147
182
 
148
183
  // First pass: collect every DEFINITION occurrence → symbol → funcId.
149
184
  for (const doc of docs) {
150
- const relPath = doc.relative_path;
151
- if (!relPath || isBuildOutputPath(relPath)) continue;
185
+ const rawPath = doc.relative_path;
186
+ if (!rawPath || isBuildOutputPath(rawPath)) continue;
187
+ const relPath = reroot(rawPath);
152
188
  for (const occ of doc.occurrences || []) {
153
189
  const isDef = (occ.symbol_roles & SYMBOL_ROLE_DEFINITION) !== 0;
154
190
  if (!isDef) continue;
155
191
  const name = funcNameFromSymbol(occ.symbol);
156
192
  if (!name) continue;
157
- // funcId = the graph's file#name key
193
+ // funcId = the graph's file#name key (repo-root-relative)
158
194
  symbolToDef.set(occ.symbol, `${relPath}#${name}`);
159
195
  }
160
196
  }
161
197
 
162
198
  // Second pass: collect every REFERENCE occurrence per file, resolved to the def.
163
199
  for (const doc of docs) {
164
- const relPath = doc.relative_path;
165
- if (!relPath || isBuildOutputPath(relPath)) continue;
200
+ const rawPath = doc.relative_path;
201
+ if (!rawPath || isBuildOutputPath(rawPath)) continue;
202
+ const relPath = reroot(rawPath);
166
203
  const refs = [];
167
204
  for (const occ of doc.occurrences || []) {
168
205
  const isDef = (occ.symbol_roles & SYMBOL_ROLE_DEFINITION) !== 0;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tekyzinc/gsd-t",
3
- "version": "5.16.12",
3
+ "version": "5.17.11",
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",