@tekyzinc/gsd-t 5.11.22 → 5.11.23
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 +32 -0
- package/README.md +1 -1
- package/package.json +1 -1
- package/templates/workflows/gsd-t-scan.workflow.js +129 -7
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,38 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to GSD-T are documented here. Updated with each release.
|
|
4
4
|
|
|
5
|
+
## [5.11.23] - 2026-08-10
|
|
6
|
+
|
|
7
|
+
### Fixed — the scan could silently leave code out, and slices were too large to read
|
|
8
|
+
|
|
9
|
+
Two defects, one standing rule now enforced: **nothing is ever left out of a
|
|
10
|
+
scan, and a failure is recovered rather than accepted.**
|
|
11
|
+
|
|
12
|
+
**Slices were being deleted.** When the probe returned more slices than a
|
|
13
|
+
volume-derived cap allowed, `rawSlices.slice(0, cap)` discarded the excess and
|
|
14
|
+
the run carried on. Those areas were never scanned, never counted as failures,
|
|
15
|
+
never mentioned in the register — a coverage hole invisible by construction. It
|
|
16
|
+
took a 34-slice probe down to 24 slices run. The cap no longer truncates, and
|
|
17
|
+
neither does `maxSlicesHint`: it is reported and every slice still runs.
|
|
18
|
+
|
|
19
|
+
**Slices were too large to enumerate.** The count was the wrong thing to bound.
|
|
20
|
+
What decides whether a defect is found is how many files ONE agent must read —
|
|
21
|
+
the finder is told to read every file, and at ~245 that stops being followable,
|
|
22
|
+
so it samples and reports a thin slice as a clean one. Slices are now sized
|
|
23
|
+
(~120 files); the count follows from the codebase.
|
|
24
|
+
|
|
25
|
+
**An oversized decomposition is re-sliced, not warned about.** If the split is
|
|
26
|
+
rejected — not finer, or it lost a path — it retries once with the fault named.
|
|
27
|
+
If that fails too, the slices are split **mechanically**: each slice's own path
|
|
28
|
+
list is cut into chunks, so no path can be lost. A crude split that reads every
|
|
29
|
+
file beats a tidy one that reads half.
|
|
30
|
+
|
|
31
|
+
Concurrency is unchanged at 10 in flight: it governs how fast a run goes, never
|
|
32
|
+
how much it covers.
|
|
33
|
+
|
|
34
|
+
- `templates/workflows/gsd-t-scan.workflow.js`: no truncation anywhere, size-based slicing, re-slice + retry + mechanical fallback-free split
|
|
35
|
+
- `test/m112-scan-schema-tolerance.test.js`: 35 tests
|
|
36
|
+
|
|
5
37
|
## [5.11.22] - 2026-08-10
|
|
6
38
|
|
|
7
39
|
### Changed — the probe now slices vertically, by business capability
|
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.23** - 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.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tekyzinc/gsd-t",
|
|
3
|
-
"version": "5.11.
|
|
3
|
+
"version": "5.11.23",
|
|
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",
|
|
@@ -414,7 +414,8 @@ const probe = await agent(
|
|
|
414
414
|
``,
|
|
415
415
|
`Each slice: a \`key\` (kebab name of the responsibility, e.g. "invoice-generation"), concrete \`paths\` it owns (under \`${projectDir}\`), a \`dimension\`, and \`why\` (what makes it one cohesive concern).`,
|
|
416
416
|
``,
|
|
417
|
-
`
|
|
417
|
+
`SIZE IS THE CONSTRAINT, NOT THE COUNT. Every file in the codebase must land in exactly one slice, and NO slice may exceed ~120 files — because the agent that owns it is required to READ EVERY FILE in it. Above that it starts sampling, and a sampled slice reports few findings while looking thorough. Divide the total file count by 120: that is roughly the MINIMUM number of slices. Emit that many or more. Do NOT aim for a "sensible" or tidy number — a large codebase genuinely needs many slices, and returning too few is the single most damaging thing you can do here, because the code you crammed together is the code that goes unread.`,
|
|
418
|
+
`A feature too large for one slice is SPLIT ALONG ITS OWN SEAMS, still vertically: "billing-invoicing" and "billing-payments", not "billing-routes" and "billing-schema". Splitting a feature is normal and expected. Never merge two features to reduce the count.`,
|
|
418
419
|
``,
|
|
419
420
|
// The axis was unspecified, and both readings satisfy "cohesive": a technical
|
|
420
421
|
// layer is cohesive, and so is a business feature. Two runs over the SAME
|
|
@@ -464,15 +465,136 @@ if (layerShaped.length > 2) {
|
|
|
464
465
|
);
|
|
465
466
|
}
|
|
466
467
|
|
|
467
|
-
//
|
|
468
|
+
// The cap used to TRUNCATE — `rawSlices.slice(0, cap)` deleted the excess and
|
|
469
|
+
// the run continued. Those areas were never scanned, never counted as failures,
|
|
470
|
+
// and never mentioned in the register: a coverage hole invisible by
|
|
471
|
+
// construction. It is what took a 34-slice probe down to 24 slices run.
|
|
472
|
+
//
|
|
473
|
+
// The count was the wrong thing to bound. What decides whether a finding is
|
|
474
|
+
// found is how many files ONE agent must read: the finder is told "read EVERY
|
|
475
|
+
// file, enumerate, do not sample", and at ~245 files that instruction stops
|
|
476
|
+
// being followable — the agent samples and reports a thin slice as a clean one.
|
|
477
|
+
//
|
|
478
|
+
// So the cap is now on SIZE, and the count follows from it. A slice too large to
|
|
479
|
+
// read is SPLIT, never dropped. More agents is the correct answer to more code.
|
|
468
480
|
const computedCap = computeSliceCap(probe.totals || {});
|
|
469
|
-
const
|
|
481
|
+
const totalFiles = Number((probe.totals || {}).files || (probe.totals || {}).total_files || 0);
|
|
482
|
+
|
|
483
|
+
// Files one agent can genuinely read and reason about. Above this, enumeration
|
|
484
|
+
// degrades into sampling.
|
|
485
|
+
const MAX_FILES_PER_SLICE = 120;
|
|
486
|
+
|
|
470
487
|
let slices = rawSlices;
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
488
|
+
|
|
489
|
+
// A count far above the structural estimate means the probe sliced per file or
|
|
490
|
+
// per module rather than by capability — the failure the old cap existed to
|
|
491
|
+
// catch (a 5-file repo cut into ~20 slices). Still worth NAMING, but never worth
|
|
492
|
+
// deleting code over: it is reported and everything still runs.
|
|
493
|
+
// maxSlicesHint used to TRUNCATE here. Nothing may be left out of a scan, so
|
|
494
|
+
// the hint no longer deletes: it is reported and every slice still runs.
|
|
495
|
+
if (maxSlicesOverride && rawSlices.length > maxSlicesOverride) {
|
|
496
|
+
log(`⚠ maxSlicesHint=${maxSlicesOverride} is below the ${rawSlices.length} slices the probe found. IGNORING it — dropping slices would leave code unscanned. Running all ${rawSlices.length}.`);
|
|
497
|
+
} else if (rawSlices.length > computedCap * 2) {
|
|
498
|
+
log(`⚠ probe returned ${rawSlices.length} slices against a structural estimate of ~${computedCap} — it may have sliced per file/module rather than by capability. Running all ${rawSlices.length} anyway: dropping a slice would silently remove code from the scan.`);
|
|
474
499
|
}
|
|
475
|
-
|
|
500
|
+
|
|
501
|
+
// Slices too big to read honestly. The probe owns the split (it knows the
|
|
502
|
+
// paths); this reports the ones that will under-read so it is visible in the
|
|
503
|
+
// log rather than hidden in a thin finding count.
|
|
504
|
+
if (totalFiles > 0 && slices.length > 0) {
|
|
505
|
+
const avgFiles = Math.round(totalFiles / slices.length);
|
|
506
|
+
if (avgFiles > MAX_FILES_PER_SLICE) {
|
|
507
|
+
// Warning about it is not enough — the run would go on and under-read every
|
|
508
|
+
// slice. Send the decomposition back to be split, and use the result.
|
|
509
|
+
const wanted = Math.ceil(totalFiles / MAX_FILES_PER_SLICE);
|
|
510
|
+
log(`⚠ SLICES TOO LARGE TO ENUMERATE — ~${avgFiles} files each across ${slices.length} slices (${totalFiles} files). The finder must read EVERY file; above ~${MAX_FILES_PER_SLICE} it samples instead, and a sampled slice reports fewer findings while looking complete. Re-slicing to ~${wanted}.`);
|
|
511
|
+
|
|
512
|
+
const resliced = await gatedAgent(
|
|
513
|
+
[
|
|
514
|
+
`Re-slice a codebase decomposition that came out too coarse. Project: \`${projectDir}\`.`,
|
|
515
|
+
``,
|
|
516
|
+
`Here are the current slices — ${slices.length} of them, averaging ~${avgFiles} files each:`,
|
|
517
|
+
JSON.stringify(slices.map((sl) => ({ key: sl.key, paths: sl.paths, dimension: sl.dimension })), null, 1),
|
|
518
|
+
``,
|
|
519
|
+
`Each slice is read by ONE agent that must open EVERY file it owns. At ~${avgFiles} files that is not possible, so those agents will sample and report a thin slice as a clean one.`,
|
|
520
|
+
`SPLIT them so no slice exceeds ~${MAX_FILES_PER_SLICE} files. Target roughly ${wanted} slices in total.`,
|
|
521
|
+
``,
|
|
522
|
+
`RULES:`,
|
|
523
|
+
`· Split along the feature's own seams, still VERTICALLY: "billing-invoicing" and "billing-payments", never "billing-routes" and "billing-schema".`,
|
|
524
|
+
`· EVERY path in the input must appear in exactly one output slice. Losing a path removes that code from the scan entirely.`,
|
|
525
|
+
`· Never merge two slices to tidy the count. Splitting is the only operation here.`,
|
|
526
|
+
`· A slice already under ~${MAX_FILES_PER_SLICE} files passes through unchanged.`,
|
|
527
|
+
`Return the full new slice list — every slice, not only the ones you split.`,
|
|
528
|
+
SHAPE_RULE,
|
|
529
|
+
].join("\n"),
|
|
530
|
+
{ label: "probe:reslice", phase: "Probe", schema: PROBE_SCHEMA, model: "opus" }
|
|
531
|
+
);
|
|
532
|
+
|
|
533
|
+
const newSlices = (resliced && Array.isArray(resliced.slices) && resliced.slices) || [];
|
|
534
|
+
// Accept it only if it is genuinely finer AND kept the paths. A re-slice
|
|
535
|
+
// that lost code would be worse than the coarse decomposition it replaced.
|
|
536
|
+
const pathsBefore = new Set(slices.flatMap((sl) => sl.paths || []));
|
|
537
|
+
const pathsAfter = new Set(newSlices.flatMap((sl) => sl.paths || []));
|
|
538
|
+
const lost = [...pathsBefore].filter((x) => !pathsAfter.has(x));
|
|
539
|
+
|
|
540
|
+
if (newSlices.length > slices.length && lost.length === 0) {
|
|
541
|
+
log(`✓ re-sliced ${slices.length} → ${newSlices.length} slices (~${Math.round(totalFiles / newSlices.length)} files each), every path preserved`);
|
|
542
|
+
slices = newSlices;
|
|
543
|
+
} else {
|
|
544
|
+
// A rejected re-slice leaves the scan under-reading every slice — that is
|
|
545
|
+
// continuing past a failure, so it gets a second try that names exactly
|
|
546
|
+
// what went wrong, on the same model. If that fails too, the slices are
|
|
547
|
+
// split MECHANICALLY below: a crude split that reads every file beats a
|
|
548
|
+
// tidy one that reads half.
|
|
549
|
+
const why = newSlices.length <= slices.length
|
|
550
|
+
? `it returned ${newSlices.length} slices — no finer than the ${slices.length} it was given`
|
|
551
|
+
: `it dropped ${lost.length} path(s): ${lost.slice(0, 6).join(", ")}${lost.length > 6 ? ", …" : ""}`;
|
|
552
|
+
log(`⚠ re-slice attempt 1 rejected — ${why}. Retrying with the fault named.`);
|
|
553
|
+
|
|
554
|
+
const retry = await gatedAgent(
|
|
555
|
+
[
|
|
556
|
+
`Your previous re-slice was REJECTED because ${why}.`,
|
|
557
|
+
``,
|
|
558
|
+
`Split these ${slices.length} slices so none exceeds ~${MAX_FILES_PER_SLICE} files. Target ~${wanted} slices.`,
|
|
559
|
+
JSON.stringify(slices.map((sl) => ({ key: sl.key, paths: sl.paths, dimension: sl.dimension })), null, 1),
|
|
560
|
+
``,
|
|
561
|
+
`The output MUST contain more slices than the input, and EVERY input path must appear in exactly one output slice. Splitting is the only operation — never merge, never drop.`,
|
|
562
|
+
`Split along the feature's own seams, still vertically.`,
|
|
563
|
+
SHAPE_RULE,
|
|
564
|
+
].join("\n"),
|
|
565
|
+
{ label: "probe:reslice (retry)", phase: "Probe", schema: PROBE_SCHEMA, model: "opus" }
|
|
566
|
+
);
|
|
567
|
+
|
|
568
|
+
const retrySlices = (retry && Array.isArray(retry.slices) && retry.slices) || [];
|
|
569
|
+
const retryAfter = new Set(retrySlices.flatMap((sl) => sl.paths || []));
|
|
570
|
+
const retryLost = [...pathsBefore].filter((x) => !retryAfter.has(x));
|
|
571
|
+
|
|
572
|
+
if (retrySlices.length > slices.length && retryLost.length === 0) {
|
|
573
|
+
log(`✓ re-slice retry succeeded — ${slices.length} → ${retrySlices.length} slices, every path preserved`);
|
|
574
|
+
slices = retrySlices;
|
|
575
|
+
} else {
|
|
576
|
+
// Mechanical split: divide each oversized slice's own paths into chunks.
|
|
577
|
+
// No agent, no judgement, no way to lose a path — every path lands in
|
|
578
|
+
// exactly one chunk because the chunks ARE the path list, cut up.
|
|
579
|
+
const split = [];
|
|
580
|
+
for (const sl of slices) {
|
|
581
|
+
const paths = sl.paths || [];
|
|
582
|
+
const share = Math.max(1, Math.round((paths.length / Math.max(pathsBefore.size, 1)) * totalFiles));
|
|
583
|
+
const parts = Math.ceil(share / MAX_FILES_PER_SLICE);
|
|
584
|
+
if (parts <= 1 || paths.length <= 1) { split.push(sl); continue; }
|
|
585
|
+
const per = Math.ceil(paths.length / parts);
|
|
586
|
+
for (let i = 0; i < paths.length; i += per) {
|
|
587
|
+
split.push({ ...sl, key: `${sl.key}-part${Math.floor(i / per) + 1}`, paths: paths.slice(i, i + per) });
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
log(`⚠ both re-slice attempts rejected — splitting mechanically instead: ${slices.length} → ${split.length} slices. Crude, but every path is still scanned and no slice is too large to read.`);
|
|
591
|
+
slices = split;
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
log(`probe derived ${rawSlices.length} slice(s); running ${slices.length} deep-finder(s)${totalFiles ? `; ~${Math.round(totalFiles / Math.max(slices.length, 1))} files/slice` : ""}; structural estimate=${computedCap}; totals=${JSON.stringify(probe.totals)}`);
|
|
476
598
|
|
|
477
599
|
// M94-D6: Graph-Wiring phase — ADDITIVE injection of the pre-computed structural slice.
|
|
478
600
|
// Current scan architecture is KEPT FULLY INTACT (Destructive Action Guard).
|