@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 +17 -0
- package/package.json +1 -1
- package/templates/workflows/gsd-t-scan.workflow.js +75 -24
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.
|
|
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) =>
|
|
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
|
-
|
|
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
|
-
|
|
309
|
-
|
|
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
|
-
|
|
364
|
+
// M72: MANDATORY coverage banner — enforced 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 >
|
|
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
|
-
|
|
433
|
-
|
|
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,
|