@tekyzinc/gsd-t 4.0.18 → 4.0.19

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,23 @@
2
2
 
3
3
  All notable changes to GSD-T are documented here. Updated with each release.
4
4
 
5
+ ## [4.0.19] - 2026-06-02 (M72 Scan Dropped-Slice Recovery + Coverage Honesty — patch)
6
+
7
+ ### Fixed — a scan that under-covers no longer presents partial results as complete
8
+
9
+ The first real Hilo run (v4.0.18) surfaced 133 findings but 7 of 19 deep-finder slices had silently FAILED — they returned no schema-valid output (the runtime nudged them twice then dropped them), and the workflow treated a dropped slice identically to a genuinely-clean one (`findings: []`). The register confidently presented ~⅔ coverage as complete. For a quality tool, silent partial coverage is worse than a shallow scan.
10
+
11
+ - `templates/workflows/gsd-t-scan.workflow.js`:
12
+ - **Retry**: each finder is now retried once on a null/invalid result (`runFinder`).
13
+ - **Detect, don't conflate**: a slice that still fails is flagged `failed:true` — never merged with an empty-but-successful slice. Coverage accounting computes `failedSlices`, `slicesSucceeded`, `coverageComplete` deterministically; dropped slices' (absent) findings are excluded from the count.
14
+ - **Surface loudly**: synthesis is REQUIRED (not relying on the agent's notice) to put a "⚠ PARTIAL COVERAGE — N of M areas not scanned" banner at the top of the register and list the un-scanned slices. The workflow return status downgrades to `"complete-partial-coverage"` (not `"complete"`) and includes `slicesFailed[]`.
15
+ - **Synthesis robustness**: instructed to write the register INCREMENTALLY (header+summary, then append each severity section) so a multi-hundred-item register can't stall on one giant Write; synthesis-input truncation raised 200KB→500KB.
16
+ - `test/m72-coverage-accounting.test.js`: +4 tests (dropped slice excluded + flagged; clean slice not a gap; null pipeline result = failed; full coverage = complete). Coverage logic also verified by a real sandbox diagnostic run.
17
+
18
+ Effect: a scan with any failed slice is clearly marked incomplete; resuming the run re-scans only the failed slices (cached successes are reused).
19
+
20
+ Suite: 1297 pass / 0 fail / 4 skip — zero regressions.
21
+
5
22
  ## [4.0.18] - 2026-06-02 (M71 Runtime-Native Scan Workflow — patch)
6
23
 
7
24
  ### Fixed — the scan Workflow now actually RUNS in the Workflow sandbox (it never did)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tekyzinc/gsd-t",
3
- "version": "4.0.18",
3
+ "version": "4.0.19",
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",
@@ -261,28 +261,55 @@ const deep = budget && budget.total && budget.total > 300000 ? "MAXIMUM" : "thor
261
261
 
262
262
  // Deep scan — pipeline: per-slice deep finder → single verify (no barrier).
263
263
  phase("Deep Scan");
264
+
265
+ // M72: a finder that returns no schema-valid output (runtime nudged it twice then
266
+ // dropped it) must NOT be silently treated as a clean slice — that presents PARTIAL
267
+ // coverage as complete (7/19 slices dropped this way on the Hilo run). So: (1) the
268
+ // finder call is RETRIED once on a null/invalid result; (2) a slice that still fails
269
+ // is flagged `failed:true` and tracked — never conflated with a genuinely-empty slice.
270
+ function finderPrompt(slice) {
271
+ return [
272
+ `⛔ Scan ONLY files under the absolute project path \`${projectDir}\`. \`cd ${projectDir}\` first; never read outside this tree.`,
273
+ `You are a DEEP tech-debt finder for ONE slice of a scan of \`${projectDir}\`: \`${slice.key}\` (dimension: ${slice.dimension}).`,
274
+ `Owned paths (relative to \`${projectDir}\`): ${JSON.stringify(slice.paths)}.`,
275
+ slice.why ? `Why this slice matters: ${slice.why}` : ``,
276
+ ``,
277
+ `MANDATE: ENUMERATE, do NOT sample. Read EVERY file under your owned paths (use Read/Grep). You own only this slice, so go to the bottom of it.`,
278
+ `Depth = ${deep}. "thorough" = every file, every non-trivial real defect (high+medium confidence). "MAXIMUM" = also lower-confidence/speculative items worth review.`,
279
+ `Surface: bugs, security holes, missing validation, broken invariants, race conditions, dead/duplicated code, N+1s, untested critical paths, contract drift, domain-specific correctness (money math, state-machine gaps, timezone bugs, idempotency holes).`,
280
+ `For each finding: title, severity (CRITICAL/HIGH/MEDIUM/LOW), human area label, concrete file:line refs, detail, impact, remediation, honest confidence. If a substantial slice yields only 1-2 findings, re-check before concluding it's clean. Empty findings array ONLY if genuinely clean.`,
281
+ `CRITICAL: you MUST return a JSON object matching the schema (slice + findings array) as your FINAL output — even if findings is empty. Do not end without the structured result.`,
282
+ ].filter(Boolean).join("\n");
283
+ }
284
+ async function runFinder(slice) {
285
+ // up to 2 attempts; a null/invalid (non-array findings) result counts as a drop.
286
+ for (let attempt = 1; attempt <= 2; attempt++) {
287
+ try {
288
+ const r = await agent(finderPrompt(slice), {
289
+ label: attempt === 1 ? `find:${slice.key}` : `find:${slice.key} (retry)`,
290
+ phase: "Deep Scan", schema: FINDER_SCHEMA, model: "sonnet",
291
+ });
292
+ if (r && Array.isArray(r.findings)) return r; // valid (incl. empty)
293
+ log(`⚠ finder slice "${slice.key}" attempt ${attempt} returned no valid output${attempt < 2 ? " — retrying" : ""}`);
294
+ } catch (e) {
295
+ log(`⚠ finder slice "${slice.key}" attempt ${attempt} threw: ${e && e.message}${attempt < 2 ? " — retrying" : ""}`);
296
+ }
297
+ }
298
+ return null; // both attempts failed → dropped slice
299
+ }
300
+
264
301
  const sliceResults = await pipeline(
265
302
  slices,
266
- (slice) => agent(
267
- [
268
- `⛔ Scan ONLY files under the absolute project path \`${projectDir}\`. \`cd ${projectDir}\` first; never read outside this tree.`,
269
- `You are a DEEP tech-debt finder for ONE slice of a scan of \`${projectDir}\`: \`${slice.key}\` (dimension: ${slice.dimension}).`,
270
- `Owned paths (relative to \`${projectDir}\`): ${JSON.stringify(slice.paths)}.`,
271
- slice.why ? `Why this slice matters: ${slice.why}` : ``,
272
- ``,
273
- `MANDATE: ENUMERATE, do NOT sample. Read EVERY file under your owned paths (use Read/Grep). The legacy scan failed because one agent sampled ~5 issues across the whole repo and stopped — you own only this slice, so go to the bottom of it.`,
274
- `Depth = ${deep}. "thorough" = every file, every non-trivial real defect (high+medium confidence). "MAXIMUM" = also lower-confidence/speculative items worth review.`,
275
- `Surface: bugs, security holes, missing validation, broken invariants, race conditions, dead/duplicated code, N+1s, untested critical paths, contract drift, domain-specific correctness (money math, state-machine gaps, timezone bugs, idempotency holes).`,
276
- `For each finding: title, severity (CRITICAL/HIGH/MEDIUM/LOW), human area label, concrete file:line refs, detail, impact, remediation, honest confidence. If a substantial slice yields only 1-2 findings, re-check before concluding it's clean. Empty findings array if genuinely clean. Return JSON per the schema.`,
277
- ].filter(Boolean).join("\n"),
278
- { label: `find:${slice.key}`, phase: "Deep Scan", schema: FINDER_SCHEMA, model: "sonnet" }
279
- ),
303
+ (slice) => runFinder(slice),
280
304
  async (finderResult, originalItem) => {
281
305
  const slice = originalItem || {};
282
306
  const sliceKey = slice.key || (finderResult && finderResult.slice) || "unknown-slice";
283
- if (!finderResult || !Array.isArray(finderResult.findings)) return { slice: sliceKey, findings: [] };
307
+ // M72: distinguish a FAILED finder (null after retries) from a genuinely-clean slice.
308
+ if (!finderResult || !Array.isArray(finderResult.findings)) {
309
+ return { slice: sliceKey, findings: [], failed: true };
310
+ }
284
311
  if (verifyMode === "none" || finderResult.findings.length === 0) {
285
- return { slice: sliceKey, findings: finderResult.findings || [] };
312
+ return { slice: sliceKey, findings: finderResult.findings || [], failed: false };
286
313
  }
287
314
  const verified = await parallel(
288
315
  finderResult.findings.map((f) => async () => {
@@ -302,11 +329,25 @@ const sliceResults = await pipeline(
302
329
  }
303
330
  })
304
331
  );
305
- return { slice: sliceKey, findings: verified.filter(Boolean) };
332
+ return { slice: sliceKey, findings: verified.filter(Boolean), failed: false };
306
333
  }
307
334
  );
308
- const allFindings = sliceResults.filter(Boolean).flatMap((r) => (r.findings || []).map((f) => ({ ...f, slice: r.slice })));
309
- log(`deep scan complete: ${allFindings.length} verified findings across ${sliceResults.filter(Boolean).length} slices`);
335
+
336
+ // M72: coverage accounting a dropped pipeline result (null) OR a failed:true slice
337
+ // is a COVERAGE GAP. Surface it deterministically; never present partial as complete.
338
+ const resultsByIndex = sliceResults; // pipeline preserves order
339
+ const failedSlices = [];
340
+ slices.forEach((s, i) => {
341
+ const r = resultsByIndex[i];
342
+ if (!r || r.failed) failedSlices.push(s.key);
343
+ });
344
+ const succeededCount = slices.length - failedSlices.length;
345
+ const coverageComplete = failedSlices.length === 0;
346
+ const allFindings = sliceResults.filter(Boolean).filter((r) => !r.failed).flatMap((r) => (r.findings || []).map((f) => ({ ...f, slice: r.slice })));
347
+ if (!coverageComplete) {
348
+ log(`⚠ PARTIAL COVERAGE — ${failedSlices.length}/${slices.length} slices failed after retry and produced NO findings: ${failedSlices.join(", ")}. The register will be flagged INCOMPLETE. Resume the run to re-scan only the failed slices.`);
349
+ }
350
+ log(`deep scan complete: ${allFindings.length} verified findings across ${succeededCount}/${slices.length} slices${coverageComplete ? " (full coverage)" : " (PARTIAL)"}`);
310
351
 
311
352
  // Synthesis — an agent does the ARCHIVE + REGISTER WRITE + GIT entirely via its
312
353
  // own Bash/Write tools. The orchestrator does NOT touch fs. The agent is given the
@@ -320,13 +361,19 @@ const synthesis = await agent(
320
361
  ``,
321
362
  `STEP 1 — ARCHIVE (do this FIRST, deterministically, via Bash): if \`${projectDir}/.gsd-t/techdebt.md\` exists, rename it to \`${projectDir}/.gsd-t/techdebt_YYYY-MM-DD.md\` using its header date (fallback: today's date from \`date +%F\`); on same-day collision append \`_2\`, \`_3\`. Use \`git mv\` if it's a git repo, else \`mv\`. Capture the archive path. ${pre.priorRegisterExists ? "(A prior register EXISTS — you MUST archive it.)" : "(No prior register — skip archiving.)"}`,
322
363
  ``,
323
- `STEP 2 WRITE the fresh \`${projectDir}/.gsd-t/techdebt.md\` (Write tool). Start TD numbering at TD-${tdStart} (computed deterministically do NOT renumber or restart). Structure: a Summary table (CRITICAL/HIGH/MEDIUM/LOW counts), then sections Critical→High→Medium→Low, each finding as \`### TD-NNN — {title}\` with Area / Severity / Status: OPEN / Location (file:line) / Description / Impact / Remediation / Milestone candidate. Re-rank globally by true severity; de-duplicate findings multiple slices surfaced into one item listing all locations. GSD-T effort units only (domain/wave/spawn/token) — never human-hours.`,
364
+ // M72: MANDATORY coverage bannerenforced here, not left to the agent's notice.
365
+ coverageComplete
366
+ ? `COVERAGE: all ${slices.length} slices succeeded — full coverage. Note this in the header.`
367
+ : `⚠ COVERAGE IS INCOMPLETE — ${failedSlices.length} of ${slices.length} slices FAILED to return findings and were NOT scanned: ${failedSlices.join(", ")}. You MUST put a prominent "> ⚠ PARTIAL COVERAGE — N of M codebase areas were not scanned this pass (listed below); findings UNDER-COUNT the real debt. Re-run for full coverage." banner at the TOP of the register, AND list the un-scanned slice names. Do NOT present this as a complete picture.`,
368
+ ``,
369
+ `STEP 2 — WRITE the fresh \`${projectDir}/.gsd-t/techdebt.md\` (Write tool). Start TD numbering at TD-${tdStart} (computed deterministically — do NOT renumber or restart). Structure: the coverage banner (above), a Summary table (CRITICAL/HIGH/MEDIUM/LOW counts), then sections Critical→High→Medium→Low, each finding as \`### TD-NNN — {title}\` with Area / Severity / Status: OPEN / Location (file:line) / Description / Impact / Remediation / Milestone candidate. Re-rank globally by true severity; de-duplicate findings multiple slices surfaced into one item listing all locations. GSD-T effort units only (domain/wave/spawn/token) — never human-hours.`,
370
+ `If the findings set is large, WRITE INCREMENTALLY — create the file with the header+summary first (Write), then APPEND each severity section with Edit. Do NOT attempt to emit the entire multi-hundred-item register in a single Write call (it can stall); build it up section by section so progress is durable.`,
324
371
  ``,
325
372
  `STEP 3 — COMMIT via Bash if it's a git repo (\`git add .gsd-t/techdebt.md\` + the archive; commit). Do NOT push.`,
326
373
  ``,
327
- `Verified findings:`,
374
+ `Verified findings (${allFindings.length} total):`,
328
375
  "```json",
329
- findingsJson.length > 200000 ? findingsJson.slice(0, 200000) + "\n…(truncated)" : findingsJson,
376
+ findingsJson.length > 500000 ? findingsJson.slice(0, 500000) + "\n…(TRUNCATED — too many findings to inline; the above is the first portion. Note in the register that some lower-severity items may be omitted due to volume.)" : findingsJson,
330
377
  "```",
331
378
  ``,
332
379
  `Return JSON per the schema: status "written" once the register file is on disk, the counts, the archivePath (or ""), and tdRange (e.g. "TD-${tdStart}..TD-NNN").`,
@@ -429,8 +476,12 @@ log(`docs commit: ${commitAgent && commitAgent.status}`);
429
476
  // projectDir. The fragile, wrong-tree HTML report is not worth that risk; dropped.
430
477
 
431
478
  return {
432
- status: "complete",
433
- slices: slices.length,
479
+ // M72: status reflects coverage — a partial scan is NOT "complete".
480
+ status: coverageComplete ? "complete" : "complete-partial-coverage",
481
+ coverageComplete,
482
+ slicesTotal: slices.length,
483
+ slicesSucceeded: succeededCount,
484
+ slicesFailed: failedSlices, // names of un-scanned areas (empty if full coverage)
434
485
  findings: allFindings.length,
435
486
  counts: synthesis.counts,
436
487
  tdRange: synthesis.tdRange,