@tekyzinc/gsd-t 5.11.21 → 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 CHANGED
@@ -2,6 +2,72 @@
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
+
37
+ ## [5.11.22] - 2026-08-10
38
+
39
+ ### Changed — the probe now slices vertically, by business capability
40
+
41
+ Two runs over the **same** codebase produced 47 slices and 34 slices with **zero
42
+ keys in common**. One cut by technical layer (`api-routes-billing`,
43
+ `lib-billing`, `schema-billing`, `pages-billing`); the other by feature
44
+ (`billing-invoicing-payments`). "Decompose by cohesive responsibility" is
45
+ satisfied by both readings, so the axis was free to flip between runs.
46
+
47
+ Two consequences, both bad:
48
+
49
+ - **Registers cannot be compared run to run.** Nothing corresponds.
50
+ - **The worst defects become invisible.** A cross-tenant access hole is a route
51
+ that never checks the caller's school before reaching the data layer. Cut by
52
+ layer, the route and the data access land in different slices and no single
53
+ agent sees both ends.
54
+
55
+ The axis is now pinned: one slice owns a whole feature — routes, logic, tables
56
+ and screens together. Genuinely cross-cutting concerns owned by no feature
57
+ (authentication, shared middleware, the build) may still be their own slice.
58
+
59
+ A prompt is advice, so a mechanical check backs it: slice keys prefixed with a
60
+ layer name are reported as **SLICING AXIS DRIFT**, naming what it costs. It
61
+ warns rather than halts — a horizontally-sliced scan still finds real defects.
62
+
63
+ **Not yet measured.** The reasoning is sound and the failure it targets is real,
64
+ but no run has proven vertical finds more than horizontal. Slices get larger
65
+ (~200 files rather than ~100 on a 6.9k-file repo), which is what produced
66
+ oversized-output failures earlier. Compare two registers before trusting it.
67
+
68
+ - `templates/workflows/gsd-t-scan.workflow.js`: the axis instruction + drift detector
69
+ - `test/m112-scan-schema-tolerance.test.js`: 5 tests, including the detector matched against the real keys from both runs
70
+
5
71
  ## [5.11.21] - 2026-08-10
6
72
 
7
73
  ### Added — a final sweep that re-runs slices the rush broke
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # GSD-T: Contract-Driven Development for Claude Code
2
2
 
3
- **v5.11.21** - A methodology for reliable, parallelizable development using Claude Code with optional Agent Teams support.
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.21",
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,26 @@ 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
- `Decompose HONESTLY by cohesive responsibility: not so coarse that an agent can't read its whole slice, not so fine that you emit one slice per file. A well-decomposed system has a finite, sensible number of real responsibilities find them. (A volume-derived backstop cap is enforced after you return ONLY to catch over-slicing; a clean sub-domain decomposition lands under it. Report accurate \`totals\`they set the backstop. If your count is truncated, you sliced too finely.)`,
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.`,
419
+ ``,
420
+ // The axis was unspecified, and both readings satisfy "cohesive": a technical
421
+ // layer is cohesive, and so is a business feature. Two runs over the SAME
422
+ // codebase (HiloAviation, 2026-08-10) produced 47 slices and 34 slices with
423
+ // ZERO keys in common — one cut by layer (api-routes-billing, lib-billing,
424
+ // schema-billing, pages-billing), the other by feature (billing-invoicing-
425
+ // payments). Two consequences, both bad:
426
+ //
427
+ // · Registers cannot be compared run to run. Nothing corresponds.
428
+ // · The worst defects become invisible. A cross-tenant access hole is a
429
+ // route that fails to check the caller's school before reaching the data
430
+ // layer — cut by layer, the route and the data access land in different
431
+ // slices and NO agent sees both ends.
432
+ //
433
+ // So the axis is pinned: vertical, by feature. Slices get larger (~200 files
434
+ // instead of ~100 on a 6.9k-file repo), which the backstop cap still bounds.
435
+ `SLICE VERTICALLY, BY BUSINESS CAPABILITY — never by technical layer. One slice owns a whole feature end to end: its routes, its business logic, its database tables and its screens together. Correct: "billing-invoicing-payments" (one slice covering all of billing). WRONG: "api-routes-billing" + "lib-billing" + "schema-billing" + "pages-billing" (the same feature split four ways). Do NOT prefix keys with a layer name (api-*, lib-*, schema-*, pages-*, components-*) — a layer prefix means you sliced the wrong way. The reason is not tidiness: the worst defects live in the seam BETWEEN layers (a route that never checks the caller's tenant before hitting the data layer), and an agent that owns only one layer cannot see both ends of that seam.`,
436
+ `The only slices that may be layer-shaped are genuinely cross-cutting concerns owned by no feature — authentication, the shared middleware, the build pipeline. If a slice belongs to a feature, it goes in that feature's slice.`,
418
437
  ``,
419
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.`,
420
439
  SHAPE_RULE,
@@ -426,15 +445,156 @@ if (!rawSlices.length) {
426
445
  log("probe returned no slices — halting");
427
446
  return { status: "failed", reason: "no-slices", probe };
428
447
  }
429
- // Volume-derived cap as a runaway backstop (the probe over-slices without it).
448
+ // Did the probe slice the way it was told? A layer prefix is the tell.
449
+ //
450
+ // The instruction alone is not enough — a prompt is advice, and this axis flipped
451
+ // silently between two runs of the same codebase. Naming the drift out loud is
452
+ // what makes it visible; the run continues, because a horizontally-sliced scan
453
+ // still finds real defects, it just misses the cross-layer ones and cannot be
454
+ // compared to the last register.
455
+ const LAYER_PREFIX = /^(api|api-routes|routes|lib|libs|schema|schemas|db|pages|page|components?|ui|hooks?|utils?|services?|models?|controllers?|middleware|types?)[-_]/i;
456
+ const layerShaped = rawSlices.filter((sl) => LAYER_PREFIX.test(String(sl.key || "")));
457
+ if (layerShaped.length > 2) {
458
+ log(
459
+ `⚠ SLICING AXIS DRIFT — ${layerShaped.length} of ${rawSlices.length} slices are named after a technical layer ` +
460
+ `(${layerShaped.slice(0, 5).map((sl) => sl.key).join(", ")}${layerShaped.length > 5 ? ", …" : ""}). ` +
461
+ `Slices are meant to run VERTICALLY, one per business capability. Sliced by layer, a defect in the seam between ` +
462
+ `layers — a route that never checks the caller's tenant before reaching the data layer — is invisible, because no ` +
463
+ `single agent sees both ends. This register also cannot be compared to one from a vertically-sliced run: the ` +
464
+ `slices do not correspond.`
465
+ );
466
+ }
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.
430
480
  const computedCap = computeSliceCap(probe.totals || {});
431
- const sliceCap = maxSlicesOverride || computedCap;
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
+
432
487
  let slices = rawSlices;
433
- if (rawSlices.length > sliceCap) {
434
- slices = rawSlices.slice(0, sliceCap);
435
- log(`⚠ SLICE CAP ENFORCED: probe returned ${rawSlices.length} cohesive slices; volume-derived backstop=${computedCap}${maxSlicesOverride ? ` (override ${maxSlicesOverride})` : ""}. Truncated to ${sliceCap} to bound the agent fan-out. Dropped ${rawSlices.length - sliceCap}: ${rawSlices.slice(sliceCap).map((s) => s.key).join(", ")}. (Probe over-sliced — it should group by cohesive sub-domain, not per file/module.)`);
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.`);
499
+ }
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
+ }
436
595
  }
437
- log(`probe derived ${rawSlices.length} slice(s); backstop cap=${computedCap}; running ${slices.length} deep-finder(s); totals=${JSON.stringify(probe.totals)}`);
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)}`);
438
598
 
439
599
  // M94-D6: Graph-Wiring phase — ADDITIVE injection of the pre-computed structural slice.
440
600
  // Current scan architecture is KEPT FULLY INTACT (Destructive Action Guard).