@tekyzinc/gsd-t 5.16.11 → 5.17.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.
- package/CHANGELOG.md +82 -0
- package/README.md +1 -1
- package/bin/gsd-t-graph-query-cli.cjs +61 -9
- package/bin/gsd-t-graph-scip-upgrade.cjs +90 -5
- package/bin/gsd-t-scip-reader.cjs +25 -6
- package/bin/gsd-t.js +12 -9
- package/package.json +1 -1
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.10] - 2026-09-01
|
|
6
|
+
|
|
7
|
+
### Fixed — the code graph indexed one TypeScript project and called it the whole repo
|
|
8
|
+
|
|
9
|
+
Found in a live TimeTracking session: 8 of 245 files indexed, 96 of 50,283 call
|
|
10
|
+
edges resolved (0.2%, all same-file), and not one backend call among them. The
|
|
11
|
+
root `tsconfig.json`'s `include` listed frontend paths only, so `server/src/`
|
|
12
|
+
was never compiled and every backend `who-calls` returned empty.
|
|
13
|
+
|
|
14
|
+
**Three defects, and fixing any one alone changes nothing.**
|
|
15
|
+
|
|
16
|
+
1. **One indexer run at the repo root.** `--infer-tsconfig` does not cover this —
|
|
17
|
+
it finds *a* tsconfig when the root has none, but still emits one index from
|
|
18
|
+
one root. Now: one run per tsconfig project, merged.
|
|
19
|
+
|
|
20
|
+
2. **Paths keyed to where the indexer ran.** Indexing `server/` emits
|
|
21
|
+
`src/index.ts` while the graph stores `server/src/index.ts`, so a second run
|
|
22
|
+
*without* re-prefixing resolves nothing while looking like a fix. The prefix
|
|
23
|
+
has to apply to the funcId values too (`relPath#name`), not just the map keys.
|
|
24
|
+
|
|
25
|
+
3. **`who-calls` claimed `coverage.complete: true` while returning zero.**
|
|
26
|
+
Coverage counted parse failures only; a file that parsed but was never
|
|
27
|
+
compiler-resolved is equally invisible to a call query and never lands in
|
|
28
|
+
`skippedFiles`. "Nothing calls this" is what gets read right before a rename
|
|
29
|
+
or a delete — and it was firing on auth middleware.
|
|
30
|
+
|
|
31
|
+
Why (3) hid so long: the per-file record handed to the query layer was
|
|
32
|
+
`{file, entities, edges}` with no tier field, so the query layer structurally
|
|
33
|
+
could not tell a resolved file from an unresolved one.
|
|
34
|
+
|
|
35
|
+
Import queries deliberately still report complete at floor tier — import edges
|
|
36
|
+
come from the parse, so `who-imports` and `blast-radius` are genuinely complete
|
|
37
|
+
there, and flagging them would train you to ignore the field.
|
|
38
|
+
|
|
39
|
+
Measured on TimeTracking: 8 → 40 indexed files, 0 → 173 resolved call edges into
|
|
40
|
+
`server/`, 96 → 390 overall, `server/` 0 → 13 of 19 files compiler-accurate.
|
|
41
|
+
Affects 13 registered projects with nested tsconfigs (binvoice 1→5 projects,
|
|
42
|
+
newman 1→7, Tekyz-CRM 1→2).
|
|
43
|
+
|
|
44
|
+
- `bin/gsd-t-graph-scip-upgrade.cjs`: `findTsProjectDirs()` + one indexer run per project
|
|
45
|
+
- `bin/gsd-t-scip-reader.cjs`: `pathPrefix` re-roots fileRefs keys AND funcIds
|
|
46
|
+
- `bin/gsd-t-graph-query-cli.cjs`: per-file tier carried onto records + tier-aware coverage
|
|
47
|
+
- `test/m114-nested-tsconfig-graph.test.js`: 12 tests, each mutation-tested
|
|
48
|
+
|
|
49
|
+
**Re-index to pick this up**: `gsd-t graph index` in any project with a nested
|
|
50
|
+
tsconfig. A graph built before this version under-reports call edges.
|
|
51
|
+
|
|
52
|
+
## [5.16.12] - 2026-08-29
|
|
53
|
+
|
|
54
|
+
### Fixed — a retired hook fired on every tool call, on every machine, since M61
|
|
55
|
+
|
|
56
|
+
An audit of installed hooks against what the installer registers found all 16
|
|
57
|
+
expected hooks present and correct — plus one that should not have been there.
|
|
58
|
+
|
|
59
|
+
M61 deleted `scripts/gsd-t-context-meter.js` (native `/context` replaced it) and
|
|
60
|
+
unwired the subsystem from `init()` and `doctor()`. `install()` was missed, so it
|
|
61
|
+
kept calling `configureContextMeterHooks()` and re-adding a `PostToolUse` hook
|
|
62
|
+
matching `*` on every install and every update.
|
|
63
|
+
|
|
64
|
+
**It never errored, which is why it survived eight months.** The command is
|
|
65
|
+
guarded `[ -f … ] && node … || true`, so on every single tool call it spawned a
|
|
66
|
+
bash + `npm root -g` subprocess, found nothing, and exited 0 silently — a real
|
|
67
|
+
cost with no signal.
|
|
68
|
+
|
|
69
|
+
The fix is two parts, because dropping the registration alone fixes nobody: a
|
|
70
|
+
machine that installed the hook once keeps running it out of its own
|
|
71
|
+
`settings.json`, and the installer is the only thing that reaches those machines.
|
|
72
|
+
|
|
73
|
+
- `bin/gsd-t.js`: removed the `configureContextMeterHooks()` call from `install()`
|
|
74
|
+
- `bin/gsd-t.js`: added the marker to `removeRetiredHooks()`, so existing machines
|
|
75
|
+
get it stripped on their next install — the same mechanism that retired the
|
|
76
|
+
M105 worktree guard
|
|
77
|
+
- `bin/gsd-t.js`: exported `removeRetiredHooks` — it was not testable from
|
|
78
|
+
outside at all, which is part of why this went unnoticed
|
|
79
|
+
- `test/m61-context-meter-hook-retired.test.js`: 6 regression tests, mutation-tested
|
|
80
|
+
by reverting each half of the fix
|
|
81
|
+
- `.gsd-t/contracts/graph-metrics-contract.md`: stale line citation for `doMetrics`
|
|
82
|
+
(:5486 → :5488), shifted by the edit above and caught by the M99 contract-line test
|
|
83
|
+
|
|
84
|
+
`configureContextMeterHooks` and `removeContextMeterHook` stay defined for the
|
|
85
|
+
uninstall path. No migration needed — the next `gsd-t update` removes the hook.
|
|
86
|
+
|
|
5
87
|
## [5.16.11] - 2026-08-27
|
|
6
88
|
|
|
7
89
|
### Fixed — the voice fix was the wrong fix, and the gate was measuring the wrong thing
|
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# GSD-T: Contract-Driven Development for Claude Code
|
|
2
2
|
|
|
3
|
-
**v5.
|
|
3
|
+
**v5.17.10** - 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
|
-
|
|
300
|
-
|
|
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
|
|
303
|
-
return {
|
|
320
|
+
const coverage = {
|
|
304
321
|
complete: false,
|
|
305
|
-
|
|
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
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
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
|
-
|
|
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,
|
|
@@ -118,12 +118,29 @@ function isBuildOutputPath(relPath) {
|
|
|
118
118
|
/**
|
|
119
119
|
* Decode a `.scip` file and build resolution maps.
|
|
120
120
|
*
|
|
121
|
+
* `pathPrefix` re-roots every path in the index to be repo-root-relative.
|
|
122
|
+
* An indexer reports paths relative to WHERE IT RAN: indexing `server/` emits
|
|
123
|
+
* `src/index.ts`, while the graph stores `server/src/index.ts`. Without the
|
|
124
|
+
* prefix, a nested project's refs key on paths the graph has never heard of and
|
|
125
|
+
* resolve nothing — a second indexer run would LOOK like a fix and change
|
|
126
|
+
* nothing. [RULE] scip-paths-reprefixed-to-repo-root
|
|
127
|
+
*
|
|
128
|
+
* The prefix must apply to BOTH the fileRefs keys AND the funcId values, since
|
|
129
|
+
* a funcId is `relPath#name` — prefixing only the keys leaves every funcId
|
|
130
|
+
* pointing at a path the graph does not have.
|
|
131
|
+
*
|
|
121
132
|
* @param {string} scipPath absolute path to an index.scip
|
|
133
|
+
* @param {string} [pathPrefix] repo-relative dir the index was produced from
|
|
134
|
+
* (e.g. "server"); "" or "." for the repo root
|
|
122
135
|
* @returns {{ ok: true, symbolToDef: Map<string,string>,
|
|
123
136
|
* fileRefs: Map<string, Array<{symbol:string, funcId:string, line:number}>> }
|
|
124
137
|
* | { ok: false, reason: string }}
|
|
125
138
|
*/
|
|
126
|
-
function readScipIndex(scipPath) {
|
|
139
|
+
function readScipIndex(scipPath, pathPrefix) {
|
|
140
|
+
// Normalize: "", ".", "./", "server/" and "server" all mean the same thing.
|
|
141
|
+
const rawPrefix = (pathPrefix || '').replace(/^\.\/+/, '').replace(/\/+$/, '');
|
|
142
|
+
const prefix = (rawPrefix === '' || rawPrefix === '.') ? '' : rawPrefix + '/';
|
|
143
|
+
const reroot = prefix ? (p) => prefix + p : (p) => p;
|
|
127
144
|
const proto = loadScipProto();
|
|
128
145
|
if (!proto) {
|
|
129
146
|
return { ok: false, reason: 'scip-decoder-unavailable' };
|
|
@@ -147,22 +164,24 @@ function readScipIndex(scipPath) {
|
|
|
147
164
|
|
|
148
165
|
// First pass: collect every DEFINITION occurrence → symbol → funcId.
|
|
149
166
|
for (const doc of docs) {
|
|
150
|
-
const
|
|
151
|
-
if (!
|
|
167
|
+
const rawPath = doc.relative_path;
|
|
168
|
+
if (!rawPath || isBuildOutputPath(rawPath)) continue;
|
|
169
|
+
const relPath = reroot(rawPath);
|
|
152
170
|
for (const occ of doc.occurrences || []) {
|
|
153
171
|
const isDef = (occ.symbol_roles & SYMBOL_ROLE_DEFINITION) !== 0;
|
|
154
172
|
if (!isDef) continue;
|
|
155
173
|
const name = funcNameFromSymbol(occ.symbol);
|
|
156
174
|
if (!name) continue;
|
|
157
|
-
// funcId = the graph's file#name key
|
|
175
|
+
// funcId = the graph's file#name key (repo-root-relative)
|
|
158
176
|
symbolToDef.set(occ.symbol, `${relPath}#${name}`);
|
|
159
177
|
}
|
|
160
178
|
}
|
|
161
179
|
|
|
162
180
|
// Second pass: collect every REFERENCE occurrence per file, resolved to the def.
|
|
163
181
|
for (const doc of docs) {
|
|
164
|
-
const
|
|
165
|
-
if (!
|
|
182
|
+
const rawPath = doc.relative_path;
|
|
183
|
+
if (!rawPath || isBuildOutputPath(rawPath)) continue;
|
|
184
|
+
const relPath = reroot(rawPath);
|
|
166
185
|
const refs = [];
|
|
167
186
|
for (const occ of doc.occurrences || []) {
|
|
168
187
|
const isDef = (occ.symbol_roles & SYMBOL_ROLE_DEFINITION) !== 0;
|
package/bin/gsd-t.js
CHANGED
|
@@ -1205,11 +1205,16 @@ function removeInterceptHooks(settingsPath) {
|
|
|
1205
1205
|
// session signal, but subagents write those too, so one session with agents
|
|
1206
1206
|
// looked like several colliding sessions and the guard blocked its own user.
|
|
1207
1207
|
//
|
|
1208
|
+
// gsd-t-context-meter — M61, script deleted; native /context replaced it.
|
|
1209
|
+
// init() stopped provisioning it then, but install() kept re-registering it,
|
|
1210
|
+
// so every machine still runs it on EVERY tool call: a bash + `npm root -g`
|
|
1211
|
+
// subprocess spawned to look for a file that no longer ships.
|
|
1212
|
+
//
|
|
1208
1213
|
// Throws on any failure. A retired hook that silently survives keeps blocking
|
|
1209
1214
|
// edits forever, so "could not remove it" must stop the install loudly rather
|
|
1210
1215
|
// than report success.
|
|
1211
1216
|
function removeRetiredHooks(settingsPath) {
|
|
1212
|
-
const RETIRED_HOOK_MARKERS = ["gsd-t-worktree-guard"];
|
|
1217
|
+
const RETIRED_HOOK_MARKERS = ["gsd-t-worktree-guard", CONTEXT_METER_HOOK_MARKER];
|
|
1213
1218
|
const targetPath = settingsPath || SETTINGS_JSON;
|
|
1214
1219
|
if (!fs.existsSync(targetPath)) return { removed: 0 };
|
|
1215
1220
|
|
|
@@ -2285,13 +2290,10 @@ async function doInstall(opts = {}) {
|
|
|
2285
2290
|
heading("Global Bin Tools (~/.claude/bin/)");
|
|
2286
2291
|
installGlobalBinTools();
|
|
2287
2292
|
|
|
2288
|
-
|
|
2289
|
-
|
|
2290
|
-
|
|
2291
|
-
|
|
2292
|
-
else if (cmHook.action === "updated") success("Context meter hook command refreshed");
|
|
2293
|
-
else info("Context meter hook already configured");
|
|
2294
|
-
}
|
|
2293
|
+
// M61: Context Meter retired (scripts/gsd-t-context-meter.js deleted; native
|
|
2294
|
+
// /context replaces it). The configureContextMeterHooks() call that stood here
|
|
2295
|
+
// re-registered a PostToolUse hook pointing at that deleted script on every
|
|
2296
|
+
// install. removeRetiredHooks() below now strips it instead.
|
|
2295
2297
|
|
|
2296
2298
|
heading("Graph-Intercept (PostToolUse on Grep — M97)");
|
|
2297
2299
|
const giHook = configureGraphInterceptHook(SETTINGS_JSON);
|
|
@@ -2318,7 +2320,7 @@ async function doInstall(opts = {}) {
|
|
|
2318
2320
|
|
|
2319
2321
|
const retired = removeRetiredHooks(SETTINGS_JSON);
|
|
2320
2322
|
if (retired.removed > 0) {
|
|
2321
|
-
success(`Removed ${retired.removed} retired hook(s) from settings.json (worktree-collision guard — M105)`);
|
|
2323
|
+
success(`Removed ${retired.removed} retired hook(s) from settings.json (worktree-collision guard — M105; context meter — M61)`);
|
|
2322
2324
|
}
|
|
2323
2325
|
|
|
2324
2326
|
// M105 worktree-collision guard RETIRED (2026-08-08). It detected sessions from
|
|
@@ -5610,6 +5612,7 @@ module.exports = {
|
|
|
5610
5612
|
installContextMeter,
|
|
5611
5613
|
configureContextMeterHooks,
|
|
5612
5614
|
removeContextMeterHook,
|
|
5615
|
+
removeRetiredHooks,
|
|
5613
5616
|
// M97/M98: intercept hook installers
|
|
5614
5617
|
configureGraphInterceptHook,
|
|
5615
5618
|
configureReadInterceptHook,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tekyzinc/gsd-t",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.17.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",
|