@tekyzinc/gsd-t 4.0.25 → 4.0.27
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,27 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to GSD-T are documented here. Updated with each release.
|
|
4
4
|
|
|
5
|
+
## [4.0.27] - 2026-06-04 (M79 Diagram Quality - patch)
|
|
6
|
+
|
|
7
|
+
### Fixed - scan-report diagrams were generic boilerplate, clashed with the dark theme, and one was actively misleading
|
|
8
|
+
|
|
9
|
+
The HTML scan report's six diagrams had three problems. (1) Four of them rendered a hardcoded "Tasks/Projects/Auth" template because `collectScanData` never populated the `services`/`layers`/`endpoints`/`states` inputs the generators read. (2) The database-schema diagram picked the wrong schema file on large repos (a 5-table video shim instead of the 417-table `src/lib/schema/index.ts`) and emitted `unknown` column types — a misleading diagram is worse than none. (3) All diagrams rendered on a white background with sharp corners and shrink-wrapped labels, clashing with the report's dark theme. The sequence diagram also failed to render because `validate & sanitize` broke the Mermaid sequence parser.
|
|
10
|
+
|
|
11
|
+
- `bin/scan-data-collector.js`: parse real `services`/`layers`/`endpoints` from `docs/architecture.md` and `states` from `docs/workflows.md` (strict transition-chain detection — prose noise yields `[]` so generators keep their good defaults).
|
|
12
|
+
- `bin/scan-diagrams-generators.js`: `genSystemArchitecture` draws up to 12 real feature domains with rounded `classDef`s; `genSequence` uses `validate and sanitize` (no `&` entity).
|
|
13
|
+
- `bin/scan-diagrams.js`: database-schema diagram suppressed by default via `SUPPRESSED_TYPES`; re-enable per call with `options.includeSchemaDiagram` once the schema extractor is fixed.
|
|
14
|
+
- `bin/scan-renderer.js`: shared `MERMAID_CONFIG` (dark `base` theme, rounded corners, node padding/spacing) applied via `mmdc -c`, plus `-b transparent` so diagrams blend into the dark panel.
|
|
15
|
+
- `test/m79-diagram-quality.test.js`: regression test (services extracted, schema suppressed + opt-in, sequence has no `&`, config injected).
|
|
16
|
+
- `test/scan.test.js`, `test/verify-gates.js`: updated to the 5-diagrams-by-default contract (schema via opt-in).
|
|
17
|
+
|
|
18
|
+
## [4.0.26] - 2026-06-03 (M78 Plain-English Grouped + Batched - patch)
|
|
19
|
+
|
|
20
|
+
### Fixed - plain-english companion was a flat ungrouped list + would stall on large registers
|
|
21
|
+
|
|
22
|
+
The non-technical companion (techdebt_in_plain_english.md) was generated by a SINGLE doc agent: it produced a flat list with no severity grouping, and would stall writing 300+ entries (the M75 register bug, unfixed for this doc). M78 gives it the register treatment: a dedicated Plain-English phase batches the (severity-sorted) findings, fans out bounded generator agents (shared concurrency gate), then assembles DETERMINISTICALLY with severity section headers (## Critical / High / Medium / Low) and chunk-writes. Removed from docTargets (no longer a single agent). +test/m78-plain-english-grouping.test.js (3 tests: grouped+complete+ordered, no mid-item chunk split, empty-severity omission). Also dropped the stale Render phase from meta.phases (removed back in M71). One-off: regrouped the existing Hilo plain-english doc by authoritative severity.
|
|
23
|
+
|
|
24
|
+
Suite: 1318 pass / 0 fail / 4 skip.
|
|
25
|
+
|
|
5
26
|
## [4.0.25] - 2026-06-03 (M77 HTML Report Reads Deep-Scan Table Format - patch)
|
|
6
27
|
|
|
7
28
|
### Fixed - scan report showed 0 critical/0 high on a 322-finding register
|
|
@@ -165,6 +165,55 @@ function parseQualityFindings(text) {
|
|
|
165
165
|
return findings.slice(0, 3);
|
|
166
166
|
}
|
|
167
167
|
|
|
168
|
+
// M79: derive the diagram inputs (services/layers/endpoints/states) from the DEEP
|
|
169
|
+
// living docs (docs/architecture.md, docs/workflows.md) so the architecture/data-flow
|
|
170
|
+
// diagrams reflect the project's REAL feature domains instead of falling back to the
|
|
171
|
+
// generic "Tasks/Projects" templates. Falls back to scan/architecture.md, then [].
|
|
172
|
+
function parseServices(docArch) {
|
|
173
|
+
if (!docArch) return [];
|
|
174
|
+
// Top-level numbered domain sections: "## 5. Multi-Tenant School and Location Model"
|
|
175
|
+
const svc = [];
|
|
176
|
+
for (const m of docArch.matchAll(/^##\s+\d+\.\s+(.+?)\s*$/gm)) {
|
|
177
|
+
const name = m[1].trim();
|
|
178
|
+
// skip meta sections that aren't feature domains
|
|
179
|
+
if (/^(system overview|technology stack|application structure|table of contents)/i.test(name)) continue;
|
|
180
|
+
svc.push(name);
|
|
181
|
+
}
|
|
182
|
+
return svc;
|
|
183
|
+
}
|
|
184
|
+
function parseLayers(docArch) {
|
|
185
|
+
if (!docArch) return [];
|
|
186
|
+
// "### Controller Layer", "## 4. Authentication and Session Layer", etc.
|
|
187
|
+
const layers = [];
|
|
188
|
+
for (const m of docArch.matchAll(/^#{2,3}\s+(?:\d+\.\s+)?(.+?\bLayer)\s*$/gmi)) {
|
|
189
|
+
const n = m[1].trim(); if (!layers.includes(n)) layers.push(n);
|
|
190
|
+
}
|
|
191
|
+
return layers;
|
|
192
|
+
}
|
|
193
|
+
function parseEndpoints(docArch) {
|
|
194
|
+
if (!docArch) return [];
|
|
195
|
+
// real route lines like `GET /feature-flags/resolve/:flagKey` or `POST /api/...`
|
|
196
|
+
const eps = [];
|
|
197
|
+
for (const m of docArch.matchAll(/\b(GET|POST|PUT|PATCH|DELETE)\s+(\/[A-Za-z0-9_\-\/:{}.]+)/g)) {
|
|
198
|
+
const e = m[1] + ' ' + m[2]; if (!eps.includes(e)) eps.push(e);
|
|
199
|
+
}
|
|
200
|
+
return eps;
|
|
201
|
+
}
|
|
202
|
+
function parseStates(docWorkflows) {
|
|
203
|
+
if (!docWorkflows) return [];
|
|
204
|
+
// Only accept REAL state transitions written as "Draft -> Open" / "Draft → Open"
|
|
205
|
+
// where BOTH sides look like status enum values (short, capitalized, no spaces).
|
|
206
|
+
// If we can't find a genuine transition chain, return [] so the generator keeps
|
|
207
|
+
// its (good) default state machine rather than rendering doc-prose noise. (M79:
|
|
208
|
+
// a loose match previously pulled "Domains/Source/Schedule" from prose headers.)
|
|
209
|
+
const isState = (w) => /^[A-Z][A-Za-z]{1,14}$/.test(w) && /(Draft|Open|Progress|Review|Done|Block|Cancel|Pending|Active|Closed|Approved|Rejected|Queued|Failed|Complete)/i.test(w);
|
|
210
|
+
const states = [];
|
|
211
|
+
for (const m of docWorkflows.matchAll(/\b([A-Z][A-Za-z]+)\s*(?:->|→)\s*([A-Z][A-Za-z]+)\b/g)) {
|
|
212
|
+
if (isState(m[1]) && isState(m[2])) for (const s of [m[1], m[2]]) if (!states.includes(s)) states.push(s);
|
|
213
|
+
}
|
|
214
|
+
return states.length >= 3 ? states : [];
|
|
215
|
+
}
|
|
216
|
+
|
|
168
217
|
function collectScanData(projectRoot) {
|
|
169
218
|
const scanDir = path.join(projectRoot, '.gsd-t', 'scan');
|
|
170
219
|
const rs = (f) => read(path.join(scanDir, f));
|
|
@@ -175,6 +224,9 @@ function collectScanData(projectRoot) {
|
|
|
175
224
|
const secText = rs('security.md');
|
|
176
225
|
const qualText = rs('quality.md');
|
|
177
226
|
const debtText = rr('.gsd-t/techdebt.md');
|
|
227
|
+
// Deep living docs — the real architecture knowledge (M79).
|
|
228
|
+
const docArch = rr('docs/architecture.md');
|
|
229
|
+
const docFlow = rr('docs/workflows.md');
|
|
178
230
|
|
|
179
231
|
let projectName = path.basename(projectRoot);
|
|
180
232
|
try { projectName = JSON.parse(rr('package.json')).name || projectName; } catch {}
|
|
@@ -188,8 +240,15 @@ function collectScanData(projectRoot) {
|
|
|
188
240
|
const qualFinds = parseQualityFindings(qualText);
|
|
189
241
|
const findings = secFinds.concat(qualFinds).slice(0, 10);
|
|
190
242
|
|
|
243
|
+
// Diagram inputs from the deep docs (M79).
|
|
244
|
+
const services = parseServices(docArch);
|
|
245
|
+
const layers = parseLayers(docArch);
|
|
246
|
+
const endpoints = parseEndpoints(docArch);
|
|
247
|
+
const states = parseStates(docFlow);
|
|
248
|
+
|
|
191
249
|
return { projectName, filesScanned, totalLoc, debtCritical, debtHigh, debtMedium,
|
|
192
|
-
testCoverage, domains, techDebt, findings
|
|
250
|
+
testCoverage, domains, techDebt, findings,
|
|
251
|
+
services, layers, endpoints, states };
|
|
193
252
|
}
|
|
194
253
|
|
|
195
254
|
module.exports = { collectScanData };
|
|
@@ -2,14 +2,22 @@
|
|
|
2
2
|
|
|
3
3
|
function genSystemArchitecture(analysisData) {
|
|
4
4
|
try {
|
|
5
|
-
|
|
5
|
+
// M79: when real feature domains were extracted from docs/architecture.md, draw
|
|
6
|
+
// them as the system's services (User -> Backend -> each domain). Show up to 12.
|
|
7
|
+
const services = (analysisData.services || []).slice(0, 12);
|
|
6
8
|
if (services.length >= 2) {
|
|
7
9
|
const lines = ['graph TB',
|
|
8
|
-
'
|
|
9
|
-
'
|
|
10
|
-
'
|
|
11
|
-
|
|
12
|
-
|
|
10
|
+
' User(["👤 User"]):::user',
|
|
11
|
+
' API["⚡ Backend / API"]:::core',
|
|
12
|
+
' User -->|"HTTPS"| API'];
|
|
13
|
+
services.forEach((s, i) => {
|
|
14
|
+
const label = String(s).replace(/"/g, "'");
|
|
15
|
+
lines.push(' S' + i + '["' + label + '"]:::svc');
|
|
16
|
+
lines.push(' API --> S' + i);
|
|
17
|
+
});
|
|
18
|
+
lines.push(' classDef user fill:#0d2035,stroke:#22d3ee,color:#a5f3fc,rx:8,ry:8');
|
|
19
|
+
lines.push(' classDef core fill:#1a0f3a,stroke:#a78bfa,color:#ede9fe,rx:8,ry:8');
|
|
20
|
+
lines.push(' classDef svc fill:#0f1d3a,stroke:#60a5fa,color:#bfdbfe,rx:8,ry:8');
|
|
13
21
|
return lines.join('\n');
|
|
14
22
|
}
|
|
15
23
|
return `graph TB
|
|
@@ -150,7 +158,7 @@ function genSequence(analysisData) {
|
|
|
150
158
|
participant Queue
|
|
151
159
|
User->>Client: Submit form
|
|
152
160
|
Client->>API: ${ep}
|
|
153
|
-
API->>API: validate
|
|
161
|
+
API->>API: validate and sanitize
|
|
154
162
|
alt invalid input
|
|
155
163
|
API-->>Client: 400 Bad Request
|
|
156
164
|
else valid
|
package/bin/scan-diagrams.js
CHANGED
|
@@ -42,11 +42,20 @@ function buildPlaceholder(def, mmd) {
|
|
|
42
42
|
};
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
+
// M79: the database-schema diagram is SUPPRESSED by default. The schema extractor
|
|
46
|
+
// picks the wrong file on large repos (grabbed a 5-table video shim instead of the
|
|
47
|
+
// 417-table src/lib/schema/index.ts) and emits `unknown` column types — a misleading
|
|
48
|
+
// diagram is worse than none. Re-enable per-call with options.includeSchemaDiagram
|
|
49
|
+
// once parseDrizzle/detectOrm are fixed to use the real schema.
|
|
50
|
+
const SUPPRESSED_TYPES = new Set(['database-schema']);
|
|
51
|
+
|
|
45
52
|
function generateDiagrams(analysisData, schemaData, options) {
|
|
46
53
|
try {
|
|
47
54
|
const { renderDiagram } = require('./scan-renderer');
|
|
55
|
+
const opts = options || {};
|
|
48
56
|
const results = [];
|
|
49
57
|
for (const def of DIAGRAM_DEFS) {
|
|
58
|
+
if (SUPPRESSED_TYPES.has(def.type) && !opts.includeSchemaDiagram) continue;
|
|
50
59
|
try {
|
|
51
60
|
const mmd = def.gen(analysisData, schemaData);
|
|
52
61
|
const isDbSchema = def.type === 'database-schema';
|
package/bin/scan-renderer.js
CHANGED
|
@@ -17,19 +17,50 @@ function makePlaceholder() {
|
|
|
17
17
|
return PLACEHOLDER_HTML;
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
+
// M79: shared Mermaid config — coherent dark palette that sits well on the report's
|
|
21
|
+
// dark page, ROUNDED node corners, and generous PADDING/spacing so labels (e.g.
|
|
22
|
+
// "assign", "resolve") aren't shrink-wrapped against the box edges. Applied to every
|
|
23
|
+
// diagram via mmdc's -c flag so all diagrams share one consistent look.
|
|
24
|
+
const MERMAID_CONFIG = {
|
|
25
|
+
theme: 'base',
|
|
26
|
+
themeVariables: {
|
|
27
|
+
background: '#0a0f1e',
|
|
28
|
+
primaryColor: '#172554', // node fill (deep blue, reads on dark)
|
|
29
|
+
primaryBorderColor: '#3b82f6',
|
|
30
|
+
primaryTextColor: '#e2e8f0',
|
|
31
|
+
lineColor: '#64748b',
|
|
32
|
+
secondaryColor: '#1e1b4b',
|
|
33
|
+
tertiaryColor: '#0a2318',
|
|
34
|
+
fontFamily: 'ui-sans-serif, system-ui, -apple-system, Segoe UI, sans-serif',
|
|
35
|
+
fontSize: '15px',
|
|
36
|
+
clusterBkg: '#0b1220',
|
|
37
|
+
clusterBorder: '#1e3a5f',
|
|
38
|
+
nodeBorder: '#3b82f6',
|
|
39
|
+
edgeLabelBackground: '#0b1220',
|
|
40
|
+
},
|
|
41
|
+
flowchart: { curve: 'basis', padding: 18, nodeSpacing: 55, rankSpacing: 70, htmlLabels: true, useMaxWidth: true },
|
|
42
|
+
state: { padding: 16, nodeSpacing: 55, rankSpacing: 70 },
|
|
43
|
+
sequence: { useMaxWidth: true, boxMargin: 12 },
|
|
44
|
+
};
|
|
45
|
+
|
|
20
46
|
function tryMmdc(mmdContent) {
|
|
21
47
|
const ts = Date.now();
|
|
22
48
|
const tmpIn = path.join(os.tmpdir(), 'gsd-scan-' + ts + '.mmd');
|
|
23
49
|
const tmpOut = path.join(os.tmpdir(), 'gsd-scan-' + ts + '.svg');
|
|
50
|
+
const tmpCfg = path.join(os.tmpdir(), 'gsd-scan-' + ts + '.config.json');
|
|
24
51
|
try {
|
|
25
52
|
fs.writeFileSync(tmpIn, mmdContent, 'utf8');
|
|
26
|
-
|
|
53
|
+
fs.writeFileSync(tmpCfg, JSON.stringify(MERMAID_CONFIG), 'utf8');
|
|
54
|
+
// -b transparent so the diagram blends into the report's dark panel instead of a
|
|
55
|
+
// white box; -c applies the shared theme/padding/rounded config.
|
|
56
|
+
execFileSync('mmdc', ['-i', tmpIn, '-o', tmpOut, '-c', tmpCfg, '-b', 'transparent', '--quiet'], { timeout: 30000, stdio: 'pipe' });
|
|
27
57
|
const svg = fs.readFileSync(tmpOut, 'utf8');
|
|
28
58
|
return { svgContent: stripSvgDimensions(svg), rendered: true, rendererUsed: 'mermaid-cli' };
|
|
29
59
|
} catch { return null; }
|
|
30
60
|
finally {
|
|
31
61
|
try { fs.unlinkSync(tmpIn); } catch {}
|
|
32
62
|
try { fs.unlinkSync(tmpOut); } catch {}
|
|
63
|
+
try { fs.unlinkSync(tmpCfg); } catch {}
|
|
33
64
|
}
|
|
34
65
|
}
|
|
35
66
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tekyzinc/gsd-t",
|
|
3
|
-
"version": "4.0.
|
|
3
|
+
"version": "4.0.27",
|
|
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",
|
|
@@ -31,8 +31,8 @@ export const meta = {
|
|
|
31
31
|
{ title: "Probe", detail: "volume probe → per-area slice list", model: "sonnet" },
|
|
32
32
|
{ title: "Deep Scan", detail: "pipeline: per-slice deep finder → single verify" },
|
|
33
33
|
{ title: "Synthesis", detail: "archive prior + write fresh register + git", model: "opus" },
|
|
34
|
-
{ title: "Document",
|
|
35
|
-
{ title: "
|
|
34
|
+
{ title: "Document", detail: "living docs + 5 dimension files (per-doc fan-out)" },
|
|
35
|
+
{ title: "Plain-English", detail: "non-technical companion: batched gen + severity-grouped chunked write" },
|
|
36
36
|
],
|
|
37
37
|
};
|
|
38
38
|
|
|
@@ -645,8 +645,10 @@ const docTargets = [
|
|
|
645
645
|
prompt: `Update or create \`${projectDir}/docs/requirements.md\`: functional requirements from routes/handlers/UI; technical from configs/package.json/runtime; non-functional from perf configs/rate limits/caching.` },
|
|
646
646
|
{ id: "readme", label: "README.md", merge: true,
|
|
647
647
|
prompt: `Update or create \`${projectDir}/README.md\`: project name+description; tech stack+versions; getting-started/setup; brief architecture overview; link to docs/. If it exists, MERGE — preserve the user's structure/custom content.` },
|
|
648
|
-
|
|
649
|
-
|
|
648
|
+
// NOTE (M78): the plain-english companion is NOT in docTargets — a single agent
|
|
649
|
+
// stalls writing 300+ entries (the M75 register bug). It has its own dedicated phase
|
|
650
|
+
// below: batched generation (bounded fan-out) + deterministic severity-grouped,
|
|
651
|
+
// chunked write.
|
|
650
652
|
];
|
|
651
653
|
|
|
652
654
|
const docResults = await parallel(
|
|
@@ -674,6 +676,85 @@ const docsOk = docResults.filter(Boolean).filter((r) => r.status === "written" |
|
|
|
674
676
|
const docsFailed = docResults.filter(Boolean).filter((r) => r.status === "failed");
|
|
675
677
|
log(`document phase: ${docsOk.length}/${docTargets.length} written/merged${docsFailed.length ? `; ${docsFailed.length} failed (non-fatal): ${docsFailed.map((d) => d.doc).join(", ")}` : ""}`);
|
|
676
678
|
|
|
679
|
+
// ─── Plain-English phase (M78) ───────────────────────────────────────────────
|
|
680
|
+
// The non-technical companion can be 300+ entries — a single agent stalls writing
|
|
681
|
+
// it (the M75 register bug). So: batch the (already severity-sorted) findings, fan
|
|
682
|
+
// out bounded generator agents (each writes its batch's entries via the shared gate),
|
|
683
|
+
// then ASSEMBLE deterministically with severity section headers, and chunk-write.
|
|
684
|
+
phase("Plain-English");
|
|
685
|
+
const peTarget = `${projectDir}/.gsd-t/techdebt_in_plain_english.md`;
|
|
686
|
+
const sevLabel = { CRITICAL: "fix before launch", HIGH: "fix soon", MEDIUM: "schedule", LOW: "clean up eventually" };
|
|
687
|
+
// Attach the deterministic TD number (matches the register: severity-sorted, tdStart+).
|
|
688
|
+
const peItems = finalFindings.map((f, i) => ({
|
|
689
|
+
td: tdStart + i, severity: f.severity, title: ascii(f.title),
|
|
690
|
+
area: ascii(f.area), detail: ascii(f.detail || f.impact || "").slice(0, 500),
|
|
691
|
+
}));
|
|
692
|
+
const PE_BATCH = 36;
|
|
693
|
+
const peBatches = [];
|
|
694
|
+
for (let i = 0; i < peItems.length; i += PE_BATCH) peBatches.push(peItems.slice(i, i + PE_BATCH));
|
|
695
|
+
const PE_SCHEMA = { type: "object", required: ["entries"], additionalProperties: false,
|
|
696
|
+
properties: { entries: { type: "array", items: { type: "object", required: ["td", "markdown"], properties: { td: { type: "integer" }, markdown: { type: "string" } } } } } };
|
|
697
|
+
|
|
698
|
+
const peResults = await parallel(peBatches.map((batch, bi) => async () => {
|
|
699
|
+
const prompt = [
|
|
700
|
+
`Write NON-TECHNICAL ("plain English") companion entries for a tech-debt register, for a non-engineer stakeholder (founder/PM).`,
|
|
701
|
+
`For EACH finding below, produce one entry. Return JSON {entries:[{td, markdown}]} where markdown is EXACTLY:`,
|
|
702
|
+
`### TD-<td> - <plain-English name, no jargon>`,
|
|
703
|
+
`**What it is.** <1-2 sentences, no jargon; define any unavoidable term in parentheses>`,
|
|
704
|
+
`**Why it matters.** <business/user consequence>`,
|
|
705
|
+
`**Real-world analogy.** <a concrete everyday comparison that genuinely maps to THIS issue>`,
|
|
706
|
+
`**Severity.** <the plain-urgency phrase given per item>`,
|
|
707
|
+
`Keep the td number EXACTLY. ASCII punctuation only (hyphens, straight quotes — NO em-dashes/smart-quotes/ellipsis). No preamble.`,
|
|
708
|
+
``,
|
|
709
|
+
`Findings (batch ${bi + 1}/${peBatches.length}):`,
|
|
710
|
+
"```json",
|
|
711
|
+
JSON.stringify(batch.map((it) => ({ ...it, severityPhrase: sevLabel[it.severity] || "review" }))),
|
|
712
|
+
"```",
|
|
713
|
+
].join("\n");
|
|
714
|
+
try {
|
|
715
|
+
const r = await gatedAgent(prompt, { label: `plain-english ${bi + 1}/${peBatches.length}`, phase: "Plain-English", schema: PE_SCHEMA, model: "sonnet" });
|
|
716
|
+
return { bi, entries: (r && Array.isArray(r.entries)) ? r.entries : [] };
|
|
717
|
+
} catch (e) { return { bi, entries: [], failed: true }; }
|
|
718
|
+
}));
|
|
719
|
+
// Map td -> entry markdown, then assemble grouped by severity (deterministic headers).
|
|
720
|
+
const peByTd = {};
|
|
721
|
+
for (const r of peResults) for (const e of (r.entries || [])) if (e && e.td != null) peByTd[e.td] = ascii(e.markdown || "");
|
|
722
|
+
const peFailed = peResults.filter((r) => r.failed).length;
|
|
723
|
+
const peGroups = { CRITICAL: [], HIGH: [], MEDIUM: [], LOW: [] };
|
|
724
|
+
for (const it of peItems) { const md = peByTd[it.td]; if (md && peGroups[it.severity]) peGroups[it.severity].push(md.trim()); }
|
|
725
|
+
const peSevHead = { CRITICAL: "## 🔴 Critical", HIGH: "## 🟠 High", MEDIUM: "## 🟡 Medium", LOW: "## 🟢 Low" };
|
|
726
|
+
const peHeader = [
|
|
727
|
+
`# Tech Debt - Plain English`, "",
|
|
728
|
+
`> Non-technical companion to .gsd-t/techdebt.md (Scan${scanNumber ? " #" + scanNumber : ""}, ${peItems.length} findings). One entry per item: what it is, why it matters, a real-world analogy, plain-urgency severity. Grouped by severity.`, "", "---",
|
|
729
|
+
].join("\n");
|
|
730
|
+
// Build chunks: header, then per-severity section (header + its entries), sub-split ≤30KB.
|
|
731
|
+
const peChunks = [peHeader];
|
|
732
|
+
for (const sev of ["CRITICAL", "HIGH", "MEDIUM", "LOW"]) {
|
|
733
|
+
const items = peGroups[sev];
|
|
734
|
+
if (!items.length) continue;
|
|
735
|
+
let buf = `\n${peSevHead[sev]} (${items.length})\n\n`;
|
|
736
|
+
for (const md of items) {
|
|
737
|
+
const piece = md + "\n\n";
|
|
738
|
+
if (buf.length + piece.length > 30000) { peChunks.push(buf); buf = ""; }
|
|
739
|
+
buf += piece;
|
|
740
|
+
}
|
|
741
|
+
if (buf.trim()) peChunks.push(buf);
|
|
742
|
+
}
|
|
743
|
+
let peWrote = 0;
|
|
744
|
+
for (let ci = 0; ci < peChunks.length; ci++) {
|
|
745
|
+
const first = ci === 0;
|
|
746
|
+
const res = await gatedAgent(
|
|
747
|
+
[
|
|
748
|
+
first ? `Create the file \`${peTarget}\` (overwrite) with EXACTLY the content between markers, using the Write tool. Verbatim.`
|
|
749
|
+
: `APPEND EXACTLY the content between markers to the END of \`${peTarget}\` (do not overwrite; append) via a Bash heredoc \`cat >> ${peTarget} <<'GSDTEOF'\` … \`GSDTEOF\`. Verbatim.`,
|
|
750
|
+
`Reply ONLY "OK".`, "", "<<<C>>>", peChunks[ci], "<<<END>>>",
|
|
751
|
+
].join("\n"),
|
|
752
|
+
{ label: `plain-english write ${ci + 1}/${peChunks.length}`, phase: "Plain-English", model: "haiku" }
|
|
753
|
+
).catch((e) => ({ _e: String(e && e.message) }));
|
|
754
|
+
if (typeof res === "string" && /ok/i.test(res)) peWrote++;
|
|
755
|
+
}
|
|
756
|
+
log(`plain-english: ${Object.values(peGroups).reduce((a, b) => a + b.length, 0)}/${peItems.length} entries, grouped by severity, ${peWrote}/${peChunks.length} chunks written${peFailed ? `; ${peFailed} gen batch(es) failed` : ""}`);
|
|
757
|
+
|
|
677
758
|
// Commit the docs + dimension files + plain-english via a small agent (Bash git).
|
|
678
759
|
const commitAgent = await agent(
|
|
679
760
|
[
|