@tekyzinc/gsd-t 5.11.30 → 5.11.31
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 +65 -0
- package/README.md +1 -1
- package/bin/gsd-t-slice-budget.cjs +38 -2
- package/package.json +1 -1
- package/templates/workflows/gsd-t-scan.workflow.js +394 -46
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,71 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to GSD-T are documented here. Updated with each release.
|
|
4
4
|
|
|
5
|
+
## [5.11.31] - 2026-08-11
|
|
6
|
+
|
|
7
|
+
### Added — the scanner now ranks its own findings before it numbers them
|
|
8
|
+
|
|
9
|
+
A 492-finding scan of hilo-figma-atos was grouped by severity and ordered by
|
|
10
|
+
discovery inside each group, so "fix the criticals in order" gave you a dead
|
|
11
|
+
marketing page before the missing database access rules on 193 tenant tables.
|
|
12
|
+
Run by hand over that register, an architect pass changed the answer enough to
|
|
13
|
+
become a permanent stage:
|
|
14
|
+
|
|
15
|
+
- The worst defect in the codebase was filed HIGH. Account credits that cover a
|
|
16
|
+
whole invoice are never marked used, so the same credit is given away again
|
|
17
|
+
every month, forever. It sat at position 127.
|
|
18
|
+
- A typo in one text box silently routes every real card payment to the practice
|
|
19
|
+
gateway, where charges report success and no money moves. Filed MEDIUM.
|
|
20
|
+
- Every school's signed legal agreements are downloadable by any other school.
|
|
21
|
+
Filed LOW.
|
|
22
|
+
- A fabricated thunderstorm advisory for a named airport is shown to pilots on
|
|
23
|
+
every page. Filed MEDIUM.
|
|
24
|
+
- 22 findings were confirmed unreachable code, carried as risk.
|
|
25
|
+
|
|
26
|
+
And the work changed shape: 492 findings collapsed into 28 root causes, with 144
|
|
27
|
+
of the 328 medium/low findings attaching to a root that already existed. The
|
|
28
|
+
codebase does not have 492 problems; it has about 28, most repeated dozens of
|
|
29
|
+
times. Scheduled individually, that produces dozens of half-fixes of one defect.
|
|
30
|
+
|
|
31
|
+
**Four changes, all measured on that run:**
|
|
32
|
+
|
|
33
|
+
**Architect stage** (`phase("Architect")`, after Synthesis, before ordering) —
|
|
34
|
+
re-tiers by consequence, groups by root cause, ranks roots within tier and
|
|
35
|
+
members within root, and marks confirmed dead code so it sinks below live
|
|
36
|
+
findings. TD numbers are assigned AFTER it, so TD-1 is the most urgent thing in
|
|
37
|
+
the codebase. Adds an EXTREME tier: leaves wrong data behind, crosses a tenant
|
|
38
|
+
boundary, moves money wrongly, or touches safety — as against CRITICAL, where
|
|
39
|
+
the feature merely does not work. Carries the dynamic-import warning that nearly
|
|
40
|
+
cost a live credit-card form its rating, and halts if the ordering loses a
|
|
41
|
+
finding.
|
|
42
|
+
|
|
43
|
+
**Verification is batched, 10 findings per agent.** Measured head to head on the
|
|
44
|
+
same 20 findings spanning all four severities: batched caught 1 false positive
|
|
45
|
+
and 2 severity corrections against the individual arm's 0 and 1, for 71 fewer
|
|
46
|
+
characters of evidence and no changed verdict. Severity is comparative — an agent
|
|
47
|
+
seeing findings together can rank them, one seeing a single finding confirms
|
|
48
|
+
whatever it was handed. 492 findings went from ~570 verifier agents to ~50.
|
|
49
|
+
|
|
50
|
+
**Design-export snapshots are left out of slicing**, and named in the plan.
|
|
51
|
+
`.figma-make-exports/` held 1,532 source files and 968,597 lines on that project
|
|
52
|
+
— six near-identical copies of a design prototype, read by finders as if they
|
|
53
|
+
were the product, for two findings that were both about the folder itself. That
|
|
54
|
+
is roughly 19 slices of finder-and-verifier work.
|
|
55
|
+
|
|
56
|
+
**One severity phrase per tier.** The plain-English companion labelled all 61
|
|
57
|
+
criticals "fix before launch" for a system already serving customers, and had
|
|
58
|
+
drifted to twelve phrasings for four tiers. An unmapped tier is now announced
|
|
59
|
+
rather than silently labelled "review".
|
|
60
|
+
|
|
61
|
+
- `templates/workflows/gsd-t-scan.workflow.js`: architect stage + schema, batched
|
|
62
|
+
verify, tier-derived labels, EXTREME throughout.
|
|
63
|
+
- `bin/gsd-t-slice-budget.cjs`: snapshot-directory exclusion, reported by name.
|
|
64
|
+
- `test/m112-architect-stage.test.js`, `test/m112-severity-labels.test.js`: 15 new
|
|
65
|
+
regressions; slice-budget and schema-tolerance suites extended.
|
|
66
|
+
|
|
67
|
+
Two existing tests were pinned to variable names rather than behaviour and failed
|
|
68
|
+
on a change that preserved what they protect; both now assert the property.
|
|
69
|
+
|
|
5
70
|
## [5.11.30] - 2026-08-11
|
|
6
71
|
|
|
7
72
|
### Fixed — the scan crashed before any finder ran: `slices is not defined`
|
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.31** - 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.
|
|
@@ -59,6 +59,32 @@ const SKIP_DIRS = new Set([
|
|
|
59
59
|
'coverage', 'out', '.turbo', '.venv', 'venv', 'Pods', 'vendor', '.gradle',
|
|
60
60
|
]);
|
|
61
61
|
|
|
62
|
+
// [RULE] slice-budget-skips-design-export-snapshots
|
|
63
|
+
//
|
|
64
|
+
// hilo-figma-atos, 2026-08-11: `.figma-make-exports/` held 1,879 tracked files
|
|
65
|
+
// and 57 MB of design-tool exports — six dated snapshots of a prototype, each a
|
|
66
|
+
// near-copy of the last. They were measured, sliced, and read by finders as if
|
|
67
|
+
// they were the application. Out of the whole scan, exactly TWO findings came
|
|
68
|
+
// from them, and both were about the directory itself (it has no type-check
|
|
69
|
+
// exclusion; the same key is committed six times) rather than about defects in
|
|
70
|
+
// the product.
|
|
71
|
+
//
|
|
72
|
+
// These are recognised by NAME rather than by a project's own ignore rules,
|
|
73
|
+
// because the project had no such rule — that absence was itself one of the two
|
|
74
|
+
// findings. Matched as a path SEGMENT so a real source folder that merely
|
|
75
|
+
// contains the word (say `src/exports/`) is untouched.
|
|
76
|
+
const SNAPSHOT_DIR_PATTERNS = [
|
|
77
|
+
/^\.?figma-make-exports$/i,
|
|
78
|
+
/^\.?figma-exports$/i,
|
|
79
|
+
/^design-exports?$/i,
|
|
80
|
+
/^ui-snapshots?$/i,
|
|
81
|
+
/^__snapshots-export__$/i,
|
|
82
|
+
];
|
|
83
|
+
|
|
84
|
+
function isSnapshotExportDir(name) {
|
|
85
|
+
return SNAPSHOT_DIR_PATTERNS.some((re) => re.test(name));
|
|
86
|
+
}
|
|
87
|
+
|
|
62
88
|
/** Lines in one file. A file that cannot be read is reported, never counted as 0. */
|
|
63
89
|
function countLines(file) {
|
|
64
90
|
const text = fs.readFileSync(file, 'utf8');
|
|
@@ -72,7 +98,7 @@ function countLines(file) {
|
|
|
72
98
|
}
|
|
73
99
|
|
|
74
100
|
/** Every source file under a path, with its line count. */
|
|
75
|
-
function measurePath(projectDir, rel, problems) {
|
|
101
|
+
function measurePath(projectDir, rel, problems, skippedSnapshots) {
|
|
76
102
|
const abs = path.resolve(projectDir, rel);
|
|
77
103
|
const out = [];
|
|
78
104
|
|
|
@@ -107,6 +133,12 @@ function measurePath(projectDir, rel, problems) {
|
|
|
107
133
|
for (const ent of entries) {
|
|
108
134
|
if (ent.isDirectory()) {
|
|
109
135
|
if (SKIP_DIRS.has(ent.name)) continue;
|
|
136
|
+
if (isSnapshotExportDir(ent.name)) {
|
|
137
|
+
// Reported, never silent: a directory this large vanishing from the
|
|
138
|
+
// measurement without a word is how a coverage hole hides.
|
|
139
|
+
skippedSnapshots.push(path.relative(projectDir, path.join(dir, ent.name)));
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
110
142
|
walk(path.join(dir, ent.name));
|
|
111
143
|
continue;
|
|
112
144
|
}
|
|
@@ -178,6 +210,7 @@ function splitSlice(slice, files, min, max, oversizedFiles) {
|
|
|
178
210
|
|
|
179
211
|
function plan(projectDir, slices, min, max) {
|
|
180
212
|
const problems = [];
|
|
213
|
+
const skippedSnapshots = [];
|
|
181
214
|
const oversizedFiles = [];
|
|
182
215
|
const out = [];
|
|
183
216
|
let measuredLines = 0;
|
|
@@ -188,7 +221,7 @@ function plan(projectDir, slices, min, max) {
|
|
|
188
221
|
const files = [];
|
|
189
222
|
const seen = new Set();
|
|
190
223
|
for (const p of paths) {
|
|
191
|
-
for (const f of measurePath(projectDir, p, problems)) {
|
|
224
|
+
for (const f of measurePath(projectDir, p, problems, skippedSnapshots)) {
|
|
192
225
|
// A file listed under two paths of one slice is one file, counted once.
|
|
193
226
|
if (seen.has(f.file)) continue;
|
|
194
227
|
seen.add(f.file);
|
|
@@ -232,6 +265,9 @@ function plan(projectDir, slices, min, max) {
|
|
|
232
265
|
.sort((a, b) => b.lines - a.lines)
|
|
233
266
|
.map((f) => ({ file: f.file, lines: f.lines })),
|
|
234
267
|
problems,
|
|
268
|
+
// Design-tool export snapshots left out of the measurement, named so the
|
|
269
|
+
// omission is visible rather than inferred from a smaller total.
|
|
270
|
+
skippedSnapshots: Array.from(new Set(skippedSnapshots)).sort(),
|
|
235
271
|
slices: out,
|
|
236
272
|
};
|
|
237
273
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tekyzinc/gsd-t",
|
|
3
|
-
"version": "5.11.
|
|
3
|
+
"version": "5.11.31",
|
|
4
4
|
"description": "GSD-T: Contract-Driven Development for Claude Code \u2014 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",
|
|
@@ -183,6 +183,55 @@ const VERIFY_SCHEMA = {
|
|
|
183
183
|
},
|
|
184
184
|
};
|
|
185
185
|
|
|
186
|
+
// One verifier reads a BATCH of findings and returns a verdict for each.
|
|
187
|
+
//
|
|
188
|
+
// [RULE] verify-batched-not-one-agent-per-finding
|
|
189
|
+
//
|
|
190
|
+
// Measured on hilo-figma-atos, 2026-08-11 — the same 20 findings, spanning all
|
|
191
|
+
// four severities, verified both ways against real code:
|
|
192
|
+
//
|
|
193
|
+
// one-agent-per-finding batched (20 in one)
|
|
194
|
+
// false positives caught 0 1
|
|
195
|
+
// severity corrections 1 2
|
|
196
|
+
// mean evidence length 426 chars 355 chars
|
|
197
|
+
//
|
|
198
|
+
// Batching did not merely cost less — it judged BETTER, and both disagreements
|
|
199
|
+
// were checked by hand and went the batch's way. An agent holding many findings
|
|
200
|
+
// sees them in relation to each other, and severity is a comparative judgment:
|
|
201
|
+
// "help articles leaking" is HIGH *next to* the cross-tenant student-record
|
|
202
|
+
// leaks beside it. An agent handed one finding alone has no yardstick, so it
|
|
203
|
+
// tends to confirm whatever it was given — which is what 20-of-20 confirmed,
|
|
204
|
+
// zero false positives, looked like on the unbatched side.
|
|
205
|
+
//
|
|
206
|
+
// The cost is ~70 characters less evidence per finding, which changed no verdict.
|
|
207
|
+
// The saving is the run: 492 findings went from ~570 verifier agents to ~50.
|
|
208
|
+
const VERIFY_BATCH_SCHEMA = {
|
|
209
|
+
type: "object",
|
|
210
|
+
required: ["verdicts"],
|
|
211
|
+
additionalProperties: true,
|
|
212
|
+
properties: {
|
|
213
|
+
verdicts: {
|
|
214
|
+
type: "array",
|
|
215
|
+
minItems: 1,
|
|
216
|
+
items: {
|
|
217
|
+
type: "object",
|
|
218
|
+
required: ["index", "verdict"],
|
|
219
|
+
additionalProperties: true,
|
|
220
|
+
properties: {
|
|
221
|
+
// The finding's position in the batch it was given. Position rather
|
|
222
|
+
// than title: a title can be paraphrased back, an index cannot.
|
|
223
|
+
index: { type: "integer" },
|
|
224
|
+
verdict: { type: "string", enum: ["confirmed", "CONFIRMED", "Confirmed", "false-positive", "FALSE-POSITIVE", "False-positive", "needs-detail", "NEEDS-DETAIL", "Needs-detail"] },
|
|
225
|
+
confirmed: { type: "boolean" },
|
|
226
|
+
note: { type: "string" },
|
|
227
|
+
evidence: { type: "string" },
|
|
228
|
+
correctedSeverity: { type: "string", enum: ["CRITICAL", "critical", "Critical", "HIGH", "high", "High", "MEDIUM", "medium", "Medium", "LOW", "low", "Low"] },
|
|
229
|
+
},
|
|
230
|
+
},
|
|
231
|
+
},
|
|
232
|
+
},
|
|
233
|
+
};
|
|
234
|
+
|
|
186
235
|
// M75: synthesis no longer writes the register via one agent (the Hilo Scan #14
|
|
187
236
|
// synthesis stalled after 9 of 322 items typing a 466KB file). Instead: a bounded
|
|
188
237
|
// dedup agent (inline DEDUP_SCHEMA, small input) decides merge groups; the
|
|
@@ -988,44 +1037,81 @@ async function scanSlice(slice) {
|
|
|
988
1037
|
if (verifyMode === "none" || finderResult.findings.length === 0) {
|
|
989
1038
|
return { slice: sliceKey, findings: finderResult.findings || [], failed: false };
|
|
990
1039
|
}
|
|
991
|
-
//
|
|
992
|
-
//
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
})
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1040
|
+
// Verify in BATCHES — see [RULE] verify-batched-not-one-agent-per-finding at
|
|
1041
|
+
// VERIFY_BATCH_SCHEMA for the measurement that chose this over one agent per
|
|
1042
|
+
// finding. Ten is small enough that every finding still gets its files opened,
|
|
1043
|
+
// and large enough that the verifier can weigh them against each other, which
|
|
1044
|
+
// is what severity judgment actually requires.
|
|
1045
|
+
const VERIFY_BATCH_SIZE = 10;
|
|
1046
|
+
const batches = [];
|
|
1047
|
+
for (let i = 0; i < finderResult.findings.length; i += VERIFY_BATCH_SIZE) {
|
|
1048
|
+
batches.push(finderResult.findings.slice(i, i + VERIFY_BATCH_SIZE));
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
const verifiedBatches = await parallel(
|
|
1052
|
+
batches.map((batch, batchNo) => async () => {
|
|
1053
|
+
const verifyPrompt = [
|
|
1054
|
+
`You are a VERIFIER for ${batch.length} tech-debt finding(s) in \`${projectDir}\`. Confirm EACH against the ACTUAL code (open the referenced files with Read) — do not trust the finder.`,
|
|
1055
|
+
``,
|
|
1056
|
+
`Findings, as a numbered list. Return one verdict per finding, using its \`index\`:`,
|
|
1057
|
+
...batch.map((f, i) => `[${i}] ${JSON.stringify(f)}`),
|
|
1058
|
+
``,
|
|
1059
|
+
`For EACH finding: verdict="confirmed" only if the defect genuinely exists. If the finder misread the code → verdict="false-positive". If real but underspecified → verdict="needs-detail" (kept).`,
|
|
1060
|
+
`Set correctedSeverity when the severity is wrong. You are seeing these findings TOGETHER — use that: severity is comparative, and a finding that looks alarming alone is often plainly lesser beside the others in this batch. Judge each one's consequence relative to the rest.`,
|
|
1061
|
+
`Put the file and line you actually checked in \`evidence\`. A verdict with no evidence from the code is the failure this step exists to prevent.`,
|
|
1062
|
+
`Return ONE object per finding, ${batch.length} in total, each carrying its \`index\`. Missing verdicts are treated as unverified.`,
|
|
1063
|
+
SHAPE_RULE,
|
|
1064
|
+
].join("\n");
|
|
1065
|
+
|
|
1066
|
+
// Attempt 1 — sonnet. Attempt 2 names the likely mistake and escalates to
|
|
1067
|
+
// the model that has never made it. Same shape as the finder escalation.
|
|
1068
|
+
const attempt1 = await gatedAgent(verifyPrompt, {
|
|
1069
|
+
label: `verify:${sliceKey}#${batchNo + 1}`, phase: "Deep Scan",
|
|
1070
|
+
schema: VERIFY_BATCH_SCHEMA, model: "sonnet",
|
|
1071
|
+
});
|
|
1072
|
+
const v = (attempt1 && Array.isArray(attempt1.verdicts)) ? attempt1 : await gatedAgent(
|
|
1073
|
+
verifyPrompt + UNWRAP_HINT,
|
|
1074
|
+
{
|
|
1075
|
+
label: `verify:${sliceKey}#${batchNo + 1} (retry on opus)`, phase: "Deep Scan",
|
|
1076
|
+
schema: VERIFY_BATCH_SCHEMA, model: "opus",
|
|
1014
1077
|
}
|
|
1015
|
-
|
|
1016
|
-
|
|
1078
|
+
);
|
|
1079
|
+
|
|
1080
|
+
const byIndex = new Map();
|
|
1081
|
+
for (const r of ((v && Array.isArray(v.verdicts) && v.verdicts) || [])) {
|
|
1082
|
+
if (Number.isInteger(r.index)) byIndex.set(r.index, r);
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
return batch.map((f, i) => {
|
|
1086
|
+
const r = byIndex.get(i);
|
|
1087
|
+
// No verdict came back for this finding. It is NOT dropped and NOT
|
|
1088
|
+
// silently passed as verified — it is kept and MARKED, so a batch that
|
|
1089
|
+
// answered for eight of ten cannot quietly delete the other two, and
|
|
1090
|
+
// the register can show which findings nobody checked.
|
|
1091
|
+
if (!r) return { ...f, _verify: "unverified" };
|
|
1092
|
+
|
|
1093
|
+
// Compared case-INSENSITIVELY: the schema accepts "false-positive" in
|
|
1094
|
+
// any casing, so an exact match would silently KEEP a finding the
|
|
1017
1095
|
// verifier had rejected.
|
|
1018
|
-
const verdict = String(
|
|
1019
|
-
if (
|
|
1020
|
-
|
|
1096
|
+
const verdict = String(r.verdict || "").toLowerCase();
|
|
1097
|
+
if (verdict === "false-positive" || r.confirmed === false) return null;
|
|
1098
|
+
|
|
1099
|
+
// Severity normalised to the shouted form here, once, so the report
|
|
1021
1100
|
// reads consistently no matter how a finder typed it.
|
|
1022
|
-
const sev = String(
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
return
|
|
1026
|
-
}
|
|
1101
|
+
const sev = String(r.correctedSeverity || f.severity || "").toUpperCase();
|
|
1102
|
+
const out = { ...f, severity: sev, _verify: verdict || "confirmed" };
|
|
1103
|
+
if (r.evidence) out._evidence = r.evidence;
|
|
1104
|
+
return out;
|
|
1105
|
+
});
|
|
1027
1106
|
})
|
|
1028
1107
|
);
|
|
1108
|
+
|
|
1109
|
+
// A batch whose agent died resolves to null; its findings are kept and marked
|
|
1110
|
+
// rather than lost — losing a real defect is worse than carrying an unchecked
|
|
1111
|
+
// one, and the mark is what stops it reading as verified.
|
|
1112
|
+
const verified = verifiedBatches.flatMap((res, batchNo) =>
|
|
1113
|
+
res === null ? batches[batchNo].map((f) => ({ ...f, _verify: "verify-errored" })) : res
|
|
1114
|
+
);
|
|
1029
1115
|
return { slice: sliceKey, findings: verified.filter(Boolean), failed: false };
|
|
1030
1116
|
}
|
|
1031
1117
|
|
|
@@ -1181,7 +1267,9 @@ if (allFindings.length > 1) {
|
|
|
1181
1267
|
}
|
|
1182
1268
|
|
|
1183
1269
|
// (a)+(c) Deterministically merge dups, sort by severity, assign TD numbers, format.
|
|
1184
|
-
|
|
1270
|
+
// EXTREME is the architect's tier for anything that leaves wrong data behind,
|
|
1271
|
+
// breaches a tenant boundary, moves money wrongly, or touches safety.
|
|
1272
|
+
const SEV_ORDER = { EXTREME: 0, CRITICAL: 1, HIGH: 2, MEDIUM: 3, LOW: 4 };
|
|
1185
1273
|
const dropped = new Set();
|
|
1186
1274
|
const merged = [];
|
|
1187
1275
|
for (const group of mergeGroups) {
|
|
@@ -1208,6 +1296,151 @@ for (const f of finalFindings) {
|
|
|
1208
1296
|
counts.total = finalFindings.length;
|
|
1209
1297
|
|
|
1210
1298
|
|
|
1299
|
+
const ARCHITECT_SCHEMA = {
|
|
1300
|
+
type: "object",
|
|
1301
|
+
required: ["placements"],
|
|
1302
|
+
additionalProperties: true,
|
|
1303
|
+
properties: {
|
|
1304
|
+
roots: {
|
|
1305
|
+
type: "array",
|
|
1306
|
+
items: {
|
|
1307
|
+
type: "object",
|
|
1308
|
+
required: ["key", "name"],
|
|
1309
|
+
additionalProperties: true,
|
|
1310
|
+
properties: {
|
|
1311
|
+
key: { type: "string", description: "short id, e.g. R1" },
|
|
1312
|
+
name: { type: "string", description: "plain-English name of the shared cause" },
|
|
1313
|
+
why: { type: "string", description: "why these are one cause, with code evidence" },
|
|
1314
|
+
fix: { type: "string", description: "the single fix that closes them" },
|
|
1315
|
+
tier: { type: "string", enum: ["EXTREME", "CRITICAL", "HIGH", "MEDIUM", "LOW", "extreme", "critical", "high", "medium", "low"] },
|
|
1316
|
+
rank: { type: "integer", description: "rank among roots of the same tier, 1 = worst" },
|
|
1317
|
+
rankReason:{ type: "string" },
|
|
1318
|
+
},
|
|
1319
|
+
},
|
|
1320
|
+
},
|
|
1321
|
+
placements: {
|
|
1322
|
+
type: "array",
|
|
1323
|
+
minItems: 1,
|
|
1324
|
+
items: {
|
|
1325
|
+
type: "object",
|
|
1326
|
+
required: ["index", "tier"],
|
|
1327
|
+
additionalProperties: true,
|
|
1328
|
+
properties: {
|
|
1329
|
+
index: { type: "integer", description: "the finding's index in the list given" },
|
|
1330
|
+
tier: { type: "string", enum: ["EXTREME", "CRITICAL", "HIGH", "MEDIUM", "LOW", "extreme", "critical", "high", "medium", "low"] },
|
|
1331
|
+
rootKey: { type: "string", description: "the root this belongs to, or omitted if standalone" },
|
|
1332
|
+
rankInRoot: { type: "integer", description: "rank among that root's members, 1 = worst" },
|
|
1333
|
+
alsoRoots: { type: "array", items: { type: "string" }, description: "other roots that also cause this" },
|
|
1334
|
+
deadCode: { type: "boolean", description: "true ONLY when confirmed unreachable — say which checks in `note`" },
|
|
1335
|
+
notADefect: { type: "boolean", description: "true when this is not a real defect" },
|
|
1336
|
+
reason: { type: "string", description: "one line, grounded in consequence" },
|
|
1337
|
+
note: { type: "string" },
|
|
1338
|
+
},
|
|
1339
|
+
},
|
|
1340
|
+
},
|
|
1341
|
+
},
|
|
1342
|
+
};
|
|
1343
|
+
|
|
1344
|
+
// ─── Architect — re-tier by consequence, group by root cause, rank ──────────
|
|
1345
|
+
//
|
|
1346
|
+
// [RULE] architect-ranks-before-numbering
|
|
1347
|
+
//
|
|
1348
|
+
// Run by hand over the hilo-figma-atos register on 2026-08-11, and it changed
|
|
1349
|
+
// the answer enough to become a permanent stage:
|
|
1350
|
+
//
|
|
1351
|
+
// · The worst finding in the codebase was filed HIGH. Account credits that
|
|
1352
|
+
// cover a whole invoice are never marked used, so the same credit is given
|
|
1353
|
+
// away again every month, forever. It sat at position 127.
|
|
1354
|
+
// · A typo in one text box silently routes every real card payment to the
|
|
1355
|
+
// practice gateway, where charges report success and no money moves. Filed
|
|
1356
|
+
// MEDIUM.
|
|
1357
|
+
// · Every school's signed legal agreements are downloadable by any other
|
|
1358
|
+
// school. Filed LOW.
|
|
1359
|
+
// · A fabricated thunderstorm advisory for a named airport is shown to pilots
|
|
1360
|
+
// on every page. Filed MEDIUM.
|
|
1361
|
+
// · 22 findings were genuinely unreachable code — carried as risk when they
|
|
1362
|
+
// cannot execute.
|
|
1363
|
+
//
|
|
1364
|
+
// And the shape of the work changed: 492 findings collapsed into 28 root causes.
|
|
1365
|
+
// 144 of the 328 medium/low findings attached to a root that already existed,
|
|
1366
|
+
// and only 7 new causes were needed. The codebase does not have 492 problems; it
|
|
1367
|
+
// has about 28, most of them repeated dozens of times. Scheduling the findings
|
|
1368
|
+
// individually produces dozens of half-fixes of one defect.
|
|
1369
|
+
//
|
|
1370
|
+
// Two things make this stage worth its cost, and both were measured, not assumed:
|
|
1371
|
+
// 1. Severity assigned per-finding is unreliable, because severity is
|
|
1372
|
+
// COMPARATIVE. An agent seeing findings together ranks them; an agent
|
|
1373
|
+
// seeing one finding alone confirms whatever it was handed.
|
|
1374
|
+
// 2. TD numbers must be assigned AFTER this, or the register's numbering
|
|
1375
|
+
// encodes the order slices happened to finish.
|
|
1376
|
+
async function architectPass(findings) {
|
|
1377
|
+
if (!findings.length) return null;
|
|
1378
|
+
|
|
1379
|
+
// One agent per ~150 findings — the size that held its judgment in the manual
|
|
1380
|
+
// run. More than that and the later findings get thinner treatment; fewer and
|
|
1381
|
+
// the agent loses the comparison that makes ranking possible.
|
|
1382
|
+
const ARCH_BATCH = 150;
|
|
1383
|
+
const chunks = [];
|
|
1384
|
+
for (let i = 0; i < findings.length; i += ARCH_BATCH) chunks.push(findings.slice(i, i + ARCH_BATCH));
|
|
1385
|
+
|
|
1386
|
+
log(`architect: ${findings.length} findings across ${chunks.length} pass(es) — re-tier by consequence, group by root cause, rank`);
|
|
1387
|
+
|
|
1388
|
+
const TIER_RULES = [
|
|
1389
|
+
`TIERS ARE ABOUT CONSEQUENCE, NOT THE KIND OF BUG. The dividing question for every finding: DOES IT LEAVE WRONG DATA BEHIND, OR DOES IT JUST FAIL TO DO ANYTHING?`,
|
|
1390
|
+
` EXTREME — breach, data loss, money wrong, or safety. Irreversible or legally reportable. Cross-tenant read OR write, missing database-level access rules, leaked credentials, remote code execution, privilege escalation — AND ALSO anything that CORRUPTS DATA or REPORTS SUCCESS WHILE WRITING NOTHING (money shown as moved but not moved; a record referenced elsewhere that was never created; a kill switch that reports "off" while the thing runs).`,
|
|
1391
|
+
` CRITICAL — the feature does not work; recoverable once fixed; NO bad data left behind. A page that never loads. An endpoint that fails and shows an empty list.`,
|
|
1392
|
+
` HIGH — real, should be fixed, neither of the above.`,
|
|
1393
|
+
` MEDIUM / LOW — confirm as filed.`,
|
|
1394
|
+
` DEAD CODE is NOT a risk tier. Genuinely unreachable code cannot cause a problem — mark it dead and it moves to a cleanup list.`,
|
|
1395
|
+
``,
|
|
1396
|
+
`⚠ AN EMPTY IMPORT LIST DOES NOT PROVE UNREACHABILITY. A page reached by dynamic import shows zero importers in the code graph and is still live and routed — this nearly cost a live credit-card form its EXTREME rating. Before calling anything dead: check the graph, search the name across the source tree, AND search for dynamic loading and the router file. Say which checks you ran.`,
|
|
1397
|
+
].join("\n");
|
|
1398
|
+
|
|
1399
|
+
const results = await parallel(chunks.map((chunk, ci) => async () => {
|
|
1400
|
+
const listing = chunk.map((f, i) =>
|
|
1401
|
+
`[${i}] severity=${f.severity} area=${ascii(f.area) || "?"} | ${ascii(f.title)} | at ${(f.files && f.files.join(", ")) || "?"} | ${ascii(f.description || "").slice(0, 400)}`
|
|
1402
|
+
).join("\n");
|
|
1403
|
+
|
|
1404
|
+
const prompt = [
|
|
1405
|
+
`⛔ Work ONLY inside \`${projectDir}\`. Read real code with Read/Grep to settle any question; the code graph answers structural questions (\`gsd-t graph who-imports <file>\`, \`who-calls\`, \`blast-radius\`) and must be preferred over grep for those.`,
|
|
1406
|
+
``,
|
|
1407
|
+
`You are the ARCHITECT for a completed tech-debt scan. ${chunk.length} findings, already verified against the code by an earlier pass. DO NOT re-verify them all. Your job is judgment about TIER, ROOT CAUSE and ORDER.`,
|
|
1408
|
+
``,
|
|
1409
|
+
TIER_RULES,
|
|
1410
|
+
``,
|
|
1411
|
+
`YOUR HIGHEST-VALUE OUTPUT IS FINDING THE MIS-FILED ONES. Severity as assigned is not reliable: in the run that created this stage, the single worst defect in the codebase (account credits re-spent every month, forever) was filed HIGH, and a payment misrouting that makes charges succeed while no money moves was filed MEDIUM. Hunt specifically for money, cross-tenant access, safety, and silent data corruption hiding at a low severity.`,
|
|
1412
|
+
``,
|
|
1413
|
+
`FINDINGS:`,
|
|
1414
|
+
listing,
|
|
1415
|
+
``,
|
|
1416
|
+
`GROUP BY ROOT CAUSE. A root is one underlying cause where ONE fix closes several findings — e.g. many routes missing the same tenant check. For each root give a plain-English name, why they are one cause (with code evidence), the single fix, and its members by index. A root with ONE member is not a group; leave it standalone. A root's tier is the tier of its WORST member.`,
|
|
1417
|
+
`Rank roots within their tier by worst consequence, and members within a root by consequence. RISK order, never the order the findings arrived.`,
|
|
1418
|
+
``,
|
|
1419
|
+
`Return JSON per the schema. Every one of the ${chunk.length} findings must appear exactly once in \`placements\` — a finding you drop is a defect nobody will see again.`,
|
|
1420
|
+
SHAPE_RULE,
|
|
1421
|
+
].join("\n");
|
|
1422
|
+
|
|
1423
|
+
const r = await gatedAgent(prompt, {
|
|
1424
|
+
label: `architect ${ci + 1}/${chunks.length}`, phase: "Architect",
|
|
1425
|
+
schema: ARCHITECT_SCHEMA, model: "opus",
|
|
1426
|
+
});
|
|
1427
|
+
return (r && Array.isArray(r.placements)) ? r : null;
|
|
1428
|
+
}));
|
|
1429
|
+
|
|
1430
|
+
const ok = results.filter(Boolean);
|
|
1431
|
+
if (!ok.length) {
|
|
1432
|
+
// Nothing to rank with. The register is still written, in its filed order —
|
|
1433
|
+
// said out loud, because a register that looks ranked and is not is worse
|
|
1434
|
+
// than one that never claimed to be.
|
|
1435
|
+
log(`⚠ ARCHITECT PRODUCED NOTHING — the register keeps its filed severities and discovery order. It is NOT prioritised; treat its ordering as arbitrary.`);
|
|
1436
|
+
return null;
|
|
1437
|
+
}
|
|
1438
|
+
if (ok.length < chunks.length) {
|
|
1439
|
+
log(`⚠ architect: ${ok.length} of ${chunks.length} passes returned — findings in the missing pass(es) keep their filed severity and sit unranked at the end.`);
|
|
1440
|
+
}
|
|
1441
|
+
return { chunks, results: ok.length === chunks.length ? results : results, partial: ok.length < chunks.length };
|
|
1442
|
+
}
|
|
1443
|
+
|
|
1211
1444
|
// M75 chunked formatter: returns an ARRAY of markdown chunks, each ≤ ~30KB, so each
|
|
1212
1445
|
// can be written through one bounded agent prompt WITHOUT truncation (a single write
|
|
1213
1446
|
// of a 466KB register truncates at ~165KB — verified). Chunk 0 is the header+summary
|
|
@@ -1244,20 +1477,99 @@ function typeOf(f) {
|
|
|
1244
1477
|
return "Other";
|
|
1245
1478
|
}
|
|
1246
1479
|
const TYPE_ORDER = ["Security / Vulnerability", "Dead Code", "Duplication", "Data Integrity / Concurrency", "Performance", "Contract Drift", "Testing", "Other"];
|
|
1247
|
-
const SEV_ORDER2 = {
|
|
1248
|
-
|
|
1480
|
+
const SEV_ORDER2 = { EXTREME: 0, CRITICAL: 1, HIGH: 2, MEDIUM: 3, LOW: 4 };
|
|
1481
|
+
|
|
1482
|
+
// The architect runs HERE — before anything is numbered. Its output re-tiers the
|
|
1483
|
+
// findings and decides their order; TD numbers then follow that order, so TD-1 is
|
|
1484
|
+
// the most urgent thing in the codebase rather than whatever a slice finished
|
|
1485
|
+
// first. See [RULE] architect-ranks-before-numbering above.
|
|
1486
|
+
phase("Architect");
|
|
1487
|
+
const architect = await architectPass(finalFindings);
|
|
1488
|
+
|
|
1489
|
+
// Apply the architect's tiers and grouping back onto the findings.
|
|
1490
|
+
if (architect) {
|
|
1491
|
+
let retiered = 0, deadFound = 0, notDefect = 0;
|
|
1492
|
+
architect.chunks.forEach((chunk, ci) => {
|
|
1493
|
+
const res = architect.results[ci];
|
|
1494
|
+
if (!res) return; // that pass returned nothing; its findings keep their filed severity
|
|
1495
|
+
const roots = new Map();
|
|
1496
|
+
for (const r of (res.roots || [])) if (r && r.key) roots.set(r.key, r);
|
|
1497
|
+
for (const p of res.placements) {
|
|
1498
|
+
const f = chunk[p.index];
|
|
1499
|
+
if (!f) continue; // an index outside the batch is not a finding to place
|
|
1500
|
+
const tier = String(p.tier || "").toUpperCase();
|
|
1501
|
+
if (tier && tier !== f.severity) { f.severity = tier; retiered++; }
|
|
1502
|
+
if (p.deadCode) { f._deadCode = true; deadFound++; }
|
|
1503
|
+
if (p.notADefect) { f._notADefect = true; notDefect++; }
|
|
1504
|
+
if (p.rootKey) {
|
|
1505
|
+
const root = roots.get(p.rootKey);
|
|
1506
|
+
f._rootKey = p.rootKey;
|
|
1507
|
+
f._rootName = (root && root.name) || p.rootKey;
|
|
1508
|
+
f._rootFix = root && root.fix;
|
|
1509
|
+
f._rootRank = (root && Number.isInteger(root.rank)) ? root.rank : 99;
|
|
1510
|
+
f._rankInRoot = Number.isInteger(p.rankInRoot) ? p.rankInRoot : 99;
|
|
1511
|
+
}
|
|
1512
|
+
if (Array.isArray(p.alsoRoots) && p.alsoRoots.length) f._alsoRoots = p.alsoRoots;
|
|
1513
|
+
if (p.reason) f._archReason = p.reason;
|
|
1514
|
+
}
|
|
1515
|
+
});
|
|
1516
|
+
log(`architect: ${retiered} finding(s) re-tiered, ${deadFound} confirmed dead code, ${notDefect} judged not a defect`);
|
|
1517
|
+
|
|
1518
|
+
// Recount — the tiers just changed, so the header's numbers must follow.
|
|
1519
|
+
counts.critical = 0; counts.high = 0; counts.medium = 0; counts.low = 0; counts.extreme = 0;
|
|
1520
|
+
for (const f of finalFindings) {
|
|
1521
|
+
const s = String(f.severity || "").toUpperCase();
|
|
1522
|
+
if (s === "EXTREME") counts.extreme++;
|
|
1523
|
+
else if (s === "CRITICAL") counts.critical++;
|
|
1524
|
+
else if (s === "HIGH") counts.high++;
|
|
1525
|
+
else if (s === "MEDIUM") counts.medium++;
|
|
1526
|
+
else if (s === "LOW") counts.low++;
|
|
1527
|
+
}
|
|
1528
|
+
}
|
|
1529
|
+
|
|
1530
|
+
// Ordering — the single source of truth for both the register's TD numbering and
|
|
1531
|
+
// the consolidation stage's references.
|
|
1532
|
+
//
|
|
1533
|
+
// With an architect result: tier, then the root's rank within that tier, then the
|
|
1534
|
+
// finding's rank within its root — the order a person should work through them.
|
|
1535
|
+
// Confirmed dead code sinks below everything: it cannot cause a problem, so it
|
|
1536
|
+
// must not sit above things that can.
|
|
1537
|
+
//
|
|
1538
|
+
// Without one: the old severity-then-type-then-arrival order, which is arbitrary
|
|
1539
|
+
// inside a severity and was announced as such by architectPass().
|
|
1249
1540
|
const orderedFindings = finalFindings
|
|
1250
1541
|
.map((f, i) => ({ f, i, t: typeOf(f) }))
|
|
1251
1542
|
.sort((a, b) => {
|
|
1252
|
-
const
|
|
1543
|
+
const deadA = a.f._deadCode ? 1 : 0, deadB = b.f._deadCode ? 1 : 0;
|
|
1544
|
+
if (deadA !== deadB) return deadA - deadB;
|
|
1545
|
+
const sv = (SEV_ORDER2[String(a.f.severity || "").toUpperCase()] ?? 9)
|
|
1546
|
+
- (SEV_ORDER2[String(b.f.severity || "").toUpperCase()] ?? 9);
|
|
1253
1547
|
if (sv !== 0) return sv;
|
|
1254
|
-
|
|
1255
|
-
|
|
1548
|
+
if (architect) {
|
|
1549
|
+
// A standalone finding ranks beside the roots, not after them: it is one
|
|
1550
|
+
// item the architect chose not to group, not a lesser item.
|
|
1551
|
+
const rr = (a.f._rootRank ?? 50) - (b.f._rootRank ?? 50);
|
|
1552
|
+
if (rr !== 0) return rr;
|
|
1553
|
+
const ri = (a.f._rankInRoot ?? 50) - (b.f._rankInRoot ?? 50);
|
|
1554
|
+
if (ri !== 0) return ri;
|
|
1555
|
+
} else {
|
|
1556
|
+
const tv = TYPE_ORDER.indexOf(a.t) - TYPE_ORDER.indexOf(b.t);
|
|
1557
|
+
if (tv !== 0) return tv;
|
|
1558
|
+
}
|
|
1256
1559
|
return a.i - b.i;
|
|
1257
1560
|
});
|
|
1258
1561
|
|
|
1562
|
+
// Every finding must survive the ordering. A sort cannot lose one, but the
|
|
1563
|
+
// architect's placement loop can only be trusted if this is checked rather than
|
|
1564
|
+
// assumed — a merge in the same family reported full coverage while having
|
|
1565
|
+
// dropped two.
|
|
1566
|
+
if (orderedFindings.length !== finalFindings.length) {
|
|
1567
|
+
log(`⚠ ORDERING LOST FINDINGS: ${finalFindings.length} in, ${orderedFindings.length} out — the register would under-report. Halting.`);
|
|
1568
|
+
return { status: "failed", reason: "ordering-lost-findings", expected: finalFindings.length, got: orderedFindings.length };
|
|
1569
|
+
}
|
|
1570
|
+
|
|
1259
1571
|
function fmtChunks(today) {
|
|
1260
|
-
const sevHead = {
|
|
1572
|
+
const sevHead = { EXTREME: "🔴 Extreme", CRITICAL: "🟠 Critical", HIGH: "🟡 High", MEDIUM: "🔵 Medium", LOW: "🟢 Low" };
|
|
1261
1573
|
const head = [];
|
|
1262
1574
|
head.push(`# Tech Debt Register - ${projectDir.split("/").pop()}`, "");
|
|
1263
1575
|
if (scanNumber) head.push(`**Scan #${scanNumber}** - Deep codebase scan (runtime-native, ${coverageComplete ? "full coverage" : "PARTIAL coverage"})`);
|
|
@@ -1272,10 +1584,17 @@ function fmtChunks(today) {
|
|
|
1272
1584
|
head.push(`> Effort estimates use GSD-T-native units (domain / wave / spawn / token-spend). Never human-hours.`);
|
|
1273
1585
|
head.push(`> TD numbering continues from the prior register (if any, archived). This scan begins at **TD-${tdStart}**.`, "");
|
|
1274
1586
|
if (!coverageComplete) head.push(`> ⚠️ **PARTIAL COVERAGE - ${failedSlices.length} of ${slices.length} codebase areas were NOT scanned this pass** (failed to return findings): ${ascii(failedSlices.join(", "))}. Findings UNDER-COUNT the real debt. Re-run (resume) for full coverage.`, "");
|
|
1275
|
-
head.push(`## Summary`, "", `| Severity | Count |`,
|
|
1276
|
-
|
|
1277
|
-
|
|
1587
|
+
head.push(`## Summary`, "", `| Severity | Count |`, `|----------|-------|`);
|
|
1588
|
+
// EXTREME only appears when the architect produced it. A row of zero on every
|
|
1589
|
+
// register of a healthy project is noise, and an absent row hides no count.
|
|
1590
|
+
if (counts.extreme) head.push(`| 🔴 EXTREME | ${counts.extreme} |`);
|
|
1591
|
+
head.push(
|
|
1592
|
+
`| 🟠 CRITICAL | ${counts.critical} |`, `| 🟡 HIGH | ${counts.high} |`,
|
|
1593
|
+
`| 🔵 MEDIUM | ${counts.medium} |`, `| 🟢 LOW | ${counts.low} |`,
|
|
1278
1594
|
`| **Total** | **${counts.total}** |`, "", "---", "");
|
|
1595
|
+
if (counts.extreme) {
|
|
1596
|
+
head.push(`> **EXTREME** means it leaves wrong data behind, crosses a tenant boundary, moves money wrongly, or touches safety. CRITICAL means the feature simply does not work, with nothing bad left behind.`, "");
|
|
1597
|
+
}
|
|
1279
1598
|
|
|
1280
1599
|
function itemMd(f, td) {
|
|
1281
1600
|
const L = [`### TD-${td} - ${ascii(f.title) || "(untitled)"}`,
|
|
@@ -1557,7 +1876,25 @@ log(`document phase: ${docsOk.length}/${docTargets.length} written/merged${docsF
|
|
|
1557
1876
|
// then ASSEMBLE deterministically with severity section headers, and chunk-write.
|
|
1558
1877
|
phase("Plain-English");
|
|
1559
1878
|
const peTarget = `${projectDir}/.gsd-t/techdebt_in_plain_english.md`; // internal fixed name (shared copy suffixed in share/)
|
|
1560
|
-
|
|
1879
|
+
// [RULE] severity-label-never-assumes-unlaunched
|
|
1880
|
+
//
|
|
1881
|
+
// "fix before launch" was wrong on every scan of a system already serving
|
|
1882
|
+
// customers — which is most of them. A register handed to the owner of a live
|
|
1883
|
+
// product that dates its own advice to before go-live reads as boilerplate, and
|
|
1884
|
+
// boilerplate is skipped.
|
|
1885
|
+
//
|
|
1886
|
+
// One phrase per tier, defined here only. The plain-English companion took its
|
|
1887
|
+
// labels from this map and then drifted: the hilo-figma-atos file carried twelve
|
|
1888
|
+
// different phrasings for four tiers ("Worth scheduling" / "Worth scheduling
|
|
1889
|
+
// soon" / "Should be scheduled soon" / "Can be scheduled at normal priority"),
|
|
1890
|
+
// plus casing variants, and only 36 of 61 criticals were labelled at all.
|
|
1891
|
+
const sevLabel = {
|
|
1892
|
+
EXTREME: "immediate priority",
|
|
1893
|
+
CRITICAL: "fix soon",
|
|
1894
|
+
HIGH: "schedule this cycle",
|
|
1895
|
+
MEDIUM: "clean up eventually",
|
|
1896
|
+
LOW: "clean up eventually",
|
|
1897
|
+
};
|
|
1561
1898
|
// Attach the deterministic TD number (matches the register: severity-sorted, tdStart+).
|
|
1562
1899
|
const peItems = finalFindings.map((f, i) => ({
|
|
1563
1900
|
td: tdStart + i, severity: f.severity, title: ascii(f.title),
|
|
@@ -1577,12 +1914,23 @@ const peResults = await parallel(peBatches.map((batch, bi) => async () => {
|
|
|
1577
1914
|
`**What it is.** <1-2 sentences, no jargon; define any unavoidable term in parentheses>`,
|
|
1578
1915
|
`**Why it matters.** <business/user consequence>`,
|
|
1579
1916
|
`**Real-world analogy.** <a concrete everyday comparison that genuinely maps to THIS issue>`,
|
|
1580
|
-
`**Severity.** <the
|
|
1917
|
+
`**Severity.** <the item's \`severityPhrase\`, copied EXACTLY, capitalised, ending with a full stop — e.g. "Immediate priority.">`,
|
|
1918
|
+
`Use that phrase VERBATIM. Do not reword it, do not add "soon"/"eventually"/"worth", do not invent a variant. Four phrases exist and no others: "Immediate priority.", "Fix soon.", "Schedule this cycle.", "Clean up eventually." A reader scanning for what to do next is reading the phrase, not the sentence around it, so a rewording makes two identical priorities look different.`,
|
|
1919
|
+
`NEVER write "fix before launch" or any wording implying the system has not launched — most scanned systems are already live and serving customers.`,
|
|
1581
1920
|
`Keep the td number EXACTLY. ASCII punctuation only (hyphens, straight quotes — NO em-dashes/smart-quotes/ellipsis). No preamble.`,
|
|
1582
1921
|
``,
|
|
1583
1922
|
`Findings (batch ${bi + 1}/${peBatches.length}):`,
|
|
1584
1923
|
"```json",
|
|
1585
|
-
|
|
1924
|
+
// A severity with no phrase is a bug in the map, not a finding to label
|
|
1925
|
+
// "review" — the old default quietly turned an unrecognised tier into a word
|
|
1926
|
+
// that says nothing, and read as deliberate. Normalised for case first,
|
|
1927
|
+
// because a finder that types "Critical" must not fall through.
|
|
1928
|
+
JSON.stringify(batch.map((it) => {
|
|
1929
|
+
const tier = String(it.severity || "").toUpperCase();
|
|
1930
|
+
const phrase = sevLabel[tier];
|
|
1931
|
+
if (!phrase) log(`⚠ severity "${it.severity}" has no label in sevLabel — ${it.title || "a finding"} will be labelled by its tier name`);
|
|
1932
|
+
return { ...it, severityPhrase: phrase || tier.toLowerCase() || "unrated" };
|
|
1933
|
+
})),
|
|
1586
1934
|
"```",
|
|
1587
1935
|
].join("\n");
|
|
1588
1936
|
try {
|