@tekyzinc/gsd-t 5.11.26 → 5.11.28
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 +69 -0
- package/README.md +1 -1
- package/bin/gsd-t-graph-query-cli.cjs +60 -2
- package/package.json +1 -1
- package/templates/workflows/gsd-t-scan.workflow.js +106 -4
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,75 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to GSD-T are documented here. Updated with each release.
|
|
4
4
|
|
|
5
|
+
## [5.11.28] - 2026-08-11
|
|
6
|
+
|
|
7
|
+
### Fixed — the graph stored the alias edges, then could not find them (two bugs)
|
|
8
|
+
|
|
9
|
+
`who-imports src/lib/db.ts` returned 5 importers on hilo-figma-atos.
|
|
10
|
+
`grep -rl '@/lib/db' src` returned 793. Found by David, who halted the scan
|
|
11
|
+
rather than running it on a graph that was answering wrongly.
|
|
12
|
+
|
|
13
|
+
v5.11.26 taught the INDEXER to expand `@/lib/db` into `src/lib/db`, and it does
|
|
14
|
+
— the edges are in the database, correctly. It never met the QUERY side, which
|
|
15
|
+
is what turns `src/lib/db` into the real file id `src/lib/db.ts`. Two
|
|
16
|
+
independent bugs there, and fixing either alone still returns nothing:
|
|
17
|
+
|
|
18
|
+
1. The resolver opened with `if (!dst.startsWith(".")) return dst` — an expanded
|
|
19
|
+
alias has no leading dot, so it was classed as an external package like
|
|
20
|
+
"react" and returned untouched. The extension was never appended.
|
|
21
|
+
2. The set of known files was built from FUNCTION nodes, so a module exporting
|
|
22
|
+
only constants, types, or re-exports was not in it. Atos's `src/lib/db.ts`
|
|
23
|
+
exports a constant. The indexer already records every file it walked in the
|
|
24
|
+
`files` table — the authoritative list was stored all along.
|
|
25
|
+
|
|
26
|
+
- `bin/gsd-t-graph-query-cli.cjs`: resolve non-relative targets against the file
|
|
27
|
+
set (packages still pass through untouched, and an expanded alias is NOT
|
|
28
|
+
re-joined to the importer's directory); read the file set from the `files`
|
|
29
|
+
table, announcing the degradation on an older graph that lacks it.
|
|
30
|
+
- `test/m112-alias-query-resolution.test.js`: 7 regressions, all going through
|
|
31
|
+
the QUERY rather than the stored edge.
|
|
32
|
+
|
|
33
|
+
Every v5.11.26 test asserted the edge was STORED correctly. Not one asked
|
|
34
|
+
whether it could then be FOUND — the feature was tested at the write and called
|
|
35
|
+
done. These tests close that gap.
|
|
36
|
+
|
|
37
|
+
Requires `gsd-t graph index` only if the graph predates the `files` table;
|
|
38
|
+
otherwise the fix applies to existing indexes immediately.
|
|
39
|
+
|
|
40
|
+
## [5.11.27] - 2026-08-11
|
|
41
|
+
|
|
42
|
+
### Fixed — the scan's volume probe returned a stand-in, and the whole run was built on it
|
|
43
|
+
|
|
44
|
+
HiloAviation, this morning. The probe answered `totals: {"trackedFiles": 5036}`
|
|
45
|
+
and `slices: [{"key": "test", "paths": ["src/"]}]` — one slice for a 4,900-file
|
|
46
|
+
application, keyed "test", and none of the six measurements the prompt asked for.
|
|
47
|
+
The schema accepted it (one slice is legal, `totals` takes any object), so the
|
|
48
|
+
scan read that single slice, produced 3 findings, and headed the register
|
|
49
|
+
"Coverage: FULL".
|
|
50
|
+
|
|
51
|
+
The probe had no retry. Every finder gets re-run when it answers `{"findings":[]}`;
|
|
52
|
+
the probe — which decides what every finder will ever look at — got one call, and
|
|
53
|
+
whatever came back became the plan.
|
|
54
|
+
|
|
55
|
+
The tell is not the slice count; a small repo really is one slice. It is that the
|
|
56
|
+
fields explicitly requested are absent: the shape of an answer with none of the
|
|
57
|
+
work behind it. So the result is now inspected before it is trusted, retried on
|
|
58
|
+
opus with the fault named, and a second stand-in HALTS the scan rather than
|
|
59
|
+
scanning it — a register that under-counts while claiming full coverage is worse
|
|
60
|
+
than no register.
|
|
61
|
+
|
|
62
|
+
- `templates/workflows/gsd-t-scan.workflow.js`: `probePlaceholderFaults()` +
|
|
63
|
+
retry-on-opus + halt; the prompt is now a reusable constant so the retry sends
|
|
64
|
+
the same task; the slice count and totals are logged at probe time, so a thin
|
|
65
|
+
plan is visible before the run rather than inferred afterwards from a thin
|
|
66
|
+
register.
|
|
67
|
+
- `test/m112-probe-placeholder.test.js`: 11 regressions, including the verbatim
|
|
68
|
+
payload that produced the 3-finding scan.
|
|
69
|
+
|
|
70
|
+
The whole-tree tell only fires when the measurements are missing too. A small
|
|
71
|
+
project genuinely is one slice covering `src/`, and halting that scan would be a
|
|
72
|
+
false alarm on every small repo — worse than the bug being fixed.
|
|
73
|
+
|
|
5
74
|
## [5.11.26] - 2026-08-11
|
|
6
75
|
|
|
7
76
|
### Fixed — a quarter of every import edge pointed at a string no file matched
|
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# GSD-T: Contract-Driven Development for Claude Code
|
|
2
2
|
|
|
3
|
-
**v5.11.
|
|
3
|
+
**v5.11.28** - 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.
|
|
@@ -437,10 +437,68 @@ function loadSqliteStore(dbPath) {
|
|
|
437
437
|
// TS/JS routinely omit the extension; the graph keys on real file ids.
|
|
438
438
|
// Every indexed file appears as the `file` of its nodes (the indexer does not
|
|
439
439
|
// emit dedicated FILE nodes), so collect the distinct file set from there.
|
|
440
|
-
|
|
440
|
+
// [RULE] query-file-set-comes-from-files-table-not-inferred-from-functions
|
|
441
|
+
//
|
|
442
|
+
// hilo-figma-atos, 2026-08-11, the second half of the same miss. This set
|
|
443
|
+
// decides whether an extensionless import target can be matched to a real
|
|
444
|
+
// file. Built from node rows, it contains only files that declare a
|
|
445
|
+
// FUNCTION — so `src/lib/db.ts`, which exports a constant, was not in it,
|
|
446
|
+
// and no query could ever resolve an import of it. A file of constants,
|
|
447
|
+
// types, or re-exports is exactly the kind of shared module a whole codebase
|
|
448
|
+
// imports, and it was invisible.
|
|
449
|
+
//
|
|
450
|
+
// The indexer records every file it walked in the `files` table. That is the
|
|
451
|
+
// authoritative list; inferring one from functions was always an
|
|
452
|
+
// approximation of a fact already stored.
|
|
453
|
+
let fileIds;
|
|
454
|
+
const hasFilesTable = db
|
|
455
|
+
.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='files'")
|
|
456
|
+
.get();
|
|
457
|
+
if (hasFilesTable) {
|
|
458
|
+
fileIds = new Set(
|
|
459
|
+
db.prepare("SELECT file FROM files").all().map((r) => norm(r.file)).filter(Boolean)
|
|
460
|
+
);
|
|
461
|
+
} else {
|
|
462
|
+
// A graph built before the files table existed. Node-derived is what this
|
|
463
|
+
// has always done — announced, because it under-reports imports of any
|
|
464
|
+
// file that declares no function.
|
|
465
|
+
fileIds = new Set(nodes.map((n) => norm(n.file)).filter(Boolean));
|
|
466
|
+
process.stderr.write(
|
|
467
|
+
"[graph] this index predates the files table — imports of constant-only/type-only files may be missed; re-run `gsd-t graph index`\n"
|
|
468
|
+
);
|
|
469
|
+
}
|
|
441
470
|
const EXTS = ["", ".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".py", "/index.ts", "/index.tsx", "/index.js"];
|
|
471
|
+
// [RULE] query-resolves-expanded-alias-not-only-relative
|
|
472
|
+
//
|
|
473
|
+
// hilo-figma-atos, 2026-08-11: `who-imports src/lib/db.ts` returned 5 of 793
|
|
474
|
+
// real importers. The 788 missing ones all wrote `@/lib/db`.
|
|
475
|
+
//
|
|
476
|
+
// The two halves of the alias fix did not meet. The indexer (v5.11.26)
|
|
477
|
+
// expands `@/lib/db` to `src/lib/db` — correct, and stored. But this
|
|
478
|
+
// resolver, which is what appends the file extension, skipped it: the gate
|
|
479
|
+
// below returned early for anything not starting with ".", treating an
|
|
480
|
+
// expanded project path exactly like the package import `react`. So the edge
|
|
481
|
+
// sat in the database as `src/lib/db` while the query asked for
|
|
482
|
+
// `src/lib/db.ts`, and the two never met.
|
|
483
|
+
//
|
|
484
|
+
// A relative specifier is resolved against its source file's directory; an
|
|
485
|
+
// expanded alias is ALREADY project-relative and must not be — joining it to
|
|
486
|
+
// the importer's directory would produce `src/app/src/lib/db`. So the two
|
|
487
|
+
// take different routes to the same place: add the extension either way.
|
|
442
488
|
const resolveDst = (srcFile, dst) => {
|
|
443
|
-
if (typeof dst !== "string" || !dst
|
|
489
|
+
if (typeof dst !== "string" || !dst) return dst;
|
|
490
|
+
|
|
491
|
+
if (!dst.startsWith(".")) {
|
|
492
|
+
// Not relative. Either a package ("react", "next/navigation") or an
|
|
493
|
+
// alias the indexer already expanded to a project path. The file set is
|
|
494
|
+
// what tells them apart — a package matches nothing in it.
|
|
495
|
+
const base = norm(dst);
|
|
496
|
+
for (const ext of EXTS) {
|
|
497
|
+
if (fileIds.has(base + ext)) return base + ext;
|
|
498
|
+
}
|
|
499
|
+
return dst; // genuinely external — keep it exactly as written
|
|
500
|
+
}
|
|
501
|
+
|
|
444
502
|
const base = path.posix.normalize(path.posix.join(path.posix.dirname(norm(srcFile)), norm(dst)));
|
|
445
503
|
// Prefer an actual indexed file id (try the bare path, then common extensions).
|
|
446
504
|
for (const ext of EXTS) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tekyzinc/gsd-t",
|
|
3
|
-
"version": "5.11.
|
|
3
|
+
"version": "5.11.28",
|
|
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",
|
|
@@ -399,7 +399,7 @@ log(`preflight ok — branch=${pre.branch}, repo=${repoName}, priorRegister=${pr
|
|
|
399
399
|
|
|
400
400
|
// Volume probe — an agent measures the codebase (its own Bash) and carves slices.
|
|
401
401
|
phase("Probe");
|
|
402
|
-
const
|
|
402
|
+
const PROBE_PROMPT =
|
|
403
403
|
[
|
|
404
404
|
`⛔ TARGET DIRECTORY IS FIXED: you MUST scan ONLY the project at the absolute path \`${projectDir}\`. Before any measurement, \`cd ${projectDir}\` (or pass that exact path to every Read/Grep/Bash). Do NOT scan your current working directory, the GSD-T package, or any other tree — every file you measure and every slice path you emit MUST be under \`${projectDir}\`. If \`${projectDir}\` does not exist or is empty, return a single slice noting that.`,
|
|
405
405
|
``,
|
|
@@ -437,14 +437,116 @@ const probe = await agent(
|
|
|
437
437
|
``,
|
|
438
438
|
`Measure with real tooling and report in \`totals\`: files, loc, routes, tables, components, featureDomains (distinct business/feature areas). Read \`${projectDir}/package.json\` for the stack. Return JSON per the schema: totals + slices.`,
|
|
439
439
|
SHAPE_RULE,
|
|
440
|
-
].join("\n")
|
|
441
|
-
|
|
442
|
-
|
|
440
|
+
].join("\n");
|
|
441
|
+
|
|
442
|
+
// Did the probe MEASURE the codebase, or return something shaped like an answer?
|
|
443
|
+
//
|
|
444
|
+
// [RULE] probe-placeholder-is-rejected-not-scanned
|
|
445
|
+
//
|
|
446
|
+
// HiloAviation, 2026-08-11. The probe returned:
|
|
447
|
+
//
|
|
448
|
+
// totals : {"trackedFiles": 5036}
|
|
449
|
+
// slices : [{"key": "test", "paths": ["src/"]}]
|
|
450
|
+
//
|
|
451
|
+
// One slice for a 4,900-file application, keyed "test", and a totals object
|
|
452
|
+
// carrying none of the six numbers the prompt asked for. The schema accepted it
|
|
453
|
+
// — one slice is legal, and totals takes any object — so the scan ran that one
|
|
454
|
+
// slice, found 3 findings, and the register's header read "Coverage: FULL".
|
|
455
|
+
//
|
|
456
|
+
// The tell is not the slice count. It is that the fields explicitly named in the
|
|
457
|
+
// prompt are simply absent: the agent produced the SHAPE of an answer without
|
|
458
|
+
// doing the work, the same instinct that sends `{"findings":[]}` through a
|
|
459
|
+
// finder. So the probe result is inspected before it is trusted, and a probe
|
|
460
|
+
// that did not measure is retried on a stronger model rather than scanned.
|
|
461
|
+
function probePlaceholderFaults(result) {
|
|
462
|
+
const faults = [];
|
|
463
|
+
const totals = (result && result.totals) || {};
|
|
464
|
+
const slices = (result && Array.isArray(result.slices) && result.slices) || [];
|
|
465
|
+
|
|
466
|
+
// Every number the prompt asks for by name. None present means the agent
|
|
467
|
+
// reported whatever it had to hand instead of measuring.
|
|
468
|
+
const asked = ["files", "loc", "routes", "tables", "components", "featureDomains"];
|
|
469
|
+
const present = asked.filter((k) => totals[k] !== undefined && totals[k] !== null);
|
|
470
|
+
if (present.length === 0) {
|
|
471
|
+
faults.push(
|
|
472
|
+
`totals contains none of the six requested measurements (${asked.join(", ")}) — it holds ` +
|
|
473
|
+
`${JSON.stringify(totals).slice(0, 160)}`
|
|
474
|
+
);
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
// A single slice whose path is the whole source tree has decomposed nothing —
|
|
478
|
+
// but ONLY when the totals are missing too. A small repo genuinely is one
|
|
479
|
+
// slice covering src/, and halting that scan would be a false alarm on every
|
|
480
|
+
// small project. What separates the two is whether the agent measured: a real
|
|
481
|
+
// probe reports the numbers AND explains the single slice. Both tells together
|
|
482
|
+
// are one judgment, not two independent ones.
|
|
483
|
+
const WHOLE_TREE = new Set([".", "./", "*", "src", "src/", "./src", "./src/", projectDir]);
|
|
484
|
+
if (slices.length === 1 && present.length === 0) {
|
|
485
|
+
const paths = Array.isArray(slices[0].paths) ? slices[0].paths : [];
|
|
486
|
+
if (paths.length && paths.every((p) => WHOLE_TREE.has(String(p).trim().replace(/\/+$/, "/")) || WHOLE_TREE.has(String(p).trim()))) {
|
|
487
|
+
faults.push(
|
|
488
|
+
`the one slice "${slices[0].key}" owns the entire source tree (${JSON.stringify(paths)}) — ` +
|
|
489
|
+
`that is not a decomposition`
|
|
490
|
+
);
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
return faults;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
let probe = await agent(PROBE_PROMPT, {
|
|
498
|
+
label: "volume-probe", phase: "Probe", schema: PROBE_SCHEMA, model: "sonnet",
|
|
499
|
+
});
|
|
500
|
+
|
|
501
|
+
let probeFaults = probePlaceholderFaults(probe);
|
|
502
|
+
if (probeFaults.length) {
|
|
503
|
+
log(`⚠ PROBE DID NOT MEASURE — the answer has the right shape but not the work behind it:`);
|
|
504
|
+
for (const f of probeFaults) log(` · ${f}`);
|
|
505
|
+
log(` retrying on opus with the fault named.`);
|
|
506
|
+
|
|
507
|
+
const retry = await agent(
|
|
508
|
+
[
|
|
509
|
+
PROBE_PROMPT,
|
|
510
|
+
``,
|
|
511
|
+
`!! A PREVIOUS ATTEMPT AT THIS EXACT TASK WAS REJECTED. What was wrong with it:`,
|
|
512
|
+
...probeFaults.map((f) => ` · ${f}`),
|
|
513
|
+
``,
|
|
514
|
+
`RUN THE MEASUREMENTS. Do not answer from a guess, and do not return a minimal stand-in to see whether it is accepted — it will not be.`,
|
|
515
|
+
`Count the files. Count the lines. Find the routes, the database tables, the components, and the distinct business areas. Those NUMBERS go in \`totals\`.`,
|
|
516
|
+
`Then carve the codebase into slices by business capability, one slice per real area of the product. A single slice owning the whole tree is not an answer.`,
|
|
517
|
+
].join("\n"),
|
|
518
|
+
{ label: "volume-probe (retry on opus)", phase: "Probe", schema: PROBE_SCHEMA, model: "opus" }
|
|
519
|
+
);
|
|
520
|
+
|
|
521
|
+
const retryFaults = probePlaceholderFaults(retry);
|
|
522
|
+
if (retry && Array.isArray(retry.slices) && retry.slices.length && retryFaults.length === 0) {
|
|
523
|
+
log(`✓ probe recovered on opus — ${retry.slices.length} slice(s), totals=${JSON.stringify(retry.totals)}`);
|
|
524
|
+
probe = retry;
|
|
525
|
+
} else {
|
|
526
|
+
// Both attempts produced a stand-in. Scanning it would read a fraction of the
|
|
527
|
+
// codebase and print "Coverage: FULL" over the result — a register that lies
|
|
528
|
+
// is worse than no register.
|
|
529
|
+
log(`✗ PROBE FAILED — two attempts, neither measured the codebase.`);
|
|
530
|
+
for (const f of (retryFaults.length ? retryFaults : probeFaults)) log(` · ${f}`);
|
|
531
|
+
return {
|
|
532
|
+
status: "failed",
|
|
533
|
+
reason: "probe-placeholder",
|
|
534
|
+
message:
|
|
535
|
+
"The probe never measured the codebase, on either attempt. Nothing was scanned: a run on this " +
|
|
536
|
+
"answer would have covered a fraction of the project while reporting full coverage.",
|
|
537
|
+
probe: retry || probe,
|
|
538
|
+
};
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
|
|
443
542
|
const rawSlices = (probe && Array.isArray(probe.slices) && probe.slices) || [];
|
|
444
543
|
if (!rawSlices.length) {
|
|
445
544
|
log("probe returned no slices — halting");
|
|
446
545
|
return { status: "failed", reason: "no-slices", probe };
|
|
447
546
|
}
|
|
547
|
+
// What the plan actually is, before anything runs on it. A thin plan was only
|
|
548
|
+
// ever visible afterwards, in a thin register.
|
|
549
|
+
log(`probe: ${rawSlices.length} slice(s) — totals=${JSON.stringify(probe.totals || {})}`);
|
|
448
550
|
// Did the probe slice the way it was told? A layer prefix is the tell.
|
|
449
551
|
//
|
|
450
552
|
// The instruction alone is not enough — a prompt is advice, and this axis flipped
|