do-better 1.0.0
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/LICENSE +21 -0
- package/README.md +265 -0
- package/bin/cli.js +262 -0
- package/do-better/SKILL.md +417 -0
- package/do-better/references/refute-charter.md +170 -0
- package/do-better/references/scoring.md +98 -0
- package/do-better/references/taxonomy.md +136 -0
- package/do-better/references/templates/charter-template.md +71 -0
- package/do-better/references/templates/finding-template.md +56 -0
- package/do-better/references/templates/rail-template.md +74 -0
- package/do-better/references/templates/roadmap-template.md +67 -0
- package/do-better/references/templates/ticket-template.md +78 -0
- package/do-better/references/verification.md +129 -0
- package/package.json +20 -0
- package/src/adlc.js +331 -0
- package/src/artifacts.js +580 -0
- package/src/charter.js +657 -0
- package/src/comprehend.js +553 -0
- package/src/identify.js +1119 -0
- package/src/llm.js +530 -0
- package/src/rail.js +533 -0
- package/src/refresh.js +256 -0
- package/src/roadmap.js +630 -0
- package/src/scan.js +313 -0
- package/src/state.js +260 -0
- package/src/utils.js +449 -0
package/src/identify.js
ADDED
|
@@ -0,0 +1,1119 @@
|
|
|
1
|
+
// src/identify.js — D2 Identify: refute-chartered finders, loop-until-dry (K=2),
|
|
2
|
+
// adversarial reproduce-or-kill verification. One file per VERIFIED finding.
|
|
3
|
+
// Contract: blueprint §2.6 + §7 D2. Exports: run(ctx), PHASE_ID, dedupeKey(candidate).
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import fs from "node:fs";
|
|
6
|
+
import {
|
|
7
|
+
OpError, BudgetError, gateError, sha256Hex, isSafeRelPath, truncate,
|
|
8
|
+
readPackageFile, gitHeadSha, TAXONOMY, mapLimit, warnIfDirtyTree,
|
|
9
|
+
} from "./utils.js";
|
|
10
|
+
import { recordPhase, addSpend, setGate, pinSha, nextFindingId } from "./state.js";
|
|
11
|
+
import { LAYOUT, readArtifact, readFindings, runReproCheck, verifyCitations, writeArtifact, writeFinding } from "./artifacts.js";
|
|
12
|
+
import { withFallback, cleanJsonResponse } from "./llm.js";
|
|
13
|
+
|
|
14
|
+
export const PHASE_ID = "identify";
|
|
15
|
+
export const K_DRY = 2;
|
|
16
|
+
export const MAX_PASSES = 8;
|
|
17
|
+
|
|
18
|
+
const SEVERITIES = new Set(["critical", "high", "medium", "low"]);
|
|
19
|
+
export const PACKET_BYTES = 30_000;
|
|
20
|
+
const SLICE_CHARS = 24_000;
|
|
21
|
+
const REPRO_TIMEOUT_MS = 30_000;
|
|
22
|
+
const FALLBACK_REFUTE =
|
|
23
|
+
"You are chartered to REFUTE the claim that this codebase is acceptable on the given dimension. " +
|
|
24
|
+
"Find concrete evidence that it is not. Every claim requires a file and line citation into the code you were shown. " +
|
|
25
|
+
"Report everything, including low-confidence findings — verification happens downstream and will kill anything that does not reproduce.";
|
|
26
|
+
const VERDICT_SYSTEM =
|
|
27
|
+
"You are a blind adversarial verifier. You see ONLY a bare claim and the cited code slice — no proposer reasoning. " +
|
|
28
|
+
'Decide whether the code shown actually supports the claim. Respond with JSON {"verdict":"CONFIRM"|"KILL"|"UNCERTAIN","reason":"..."}. ' +
|
|
29
|
+
"When in doubt, do NOT confirm.";
|
|
30
|
+
const REPRO_SYSTEM =
|
|
31
|
+
"You propose mechanical reproduction checks for code findings. Allowed shapes ONLY: " +
|
|
32
|
+
'`node --test <relative test file>`, `node -e "<snippet ≤500 chars>"` (snippet must exit 0 iff the issue is present), ' +
|
|
33
|
+
'or a native grep as {"grep":{"pattern":"<regex>","file":"<relative path>"}}. ' +
|
|
34
|
+
'If no deterministic check exists, return {"reproCmd":null}. Respond with JSON only.';
|
|
35
|
+
// T3 semantic dedupe: a cheap-tier equivalence judge layered OVER the free hash
|
|
36
|
+
// filter. It sees a numbered list of prior findings (same dimension+file) and
|
|
37
|
+
// one new finding, and decides whether the new one merely restates a prior in
|
|
38
|
+
// different words. Bias toward "new" (null) — false-new is cheap, false-dup is
|
|
39
|
+
// unrecoverable (see the fail-open doctrine at the call site).
|
|
40
|
+
const DEDUPE_SYSTEM =
|
|
41
|
+
"You judge whether a NEW code finding is a SEMANTIC DUPLICATE of one already recorded — the same " +
|
|
42
|
+
"underlying issue restated in different words, not merely a finding in the same file or area. " +
|
|
43
|
+
'You are given a numbered list of prior findings and one new finding. Respond with JSON ' +
|
|
44
|
+
'{"duplicateOf": <the index of the prior finding it duplicates, or null if it is genuinely new>}. ' +
|
|
45
|
+
"Return an index ONLY when you are confident it is the same issue; when in any doubt, return null.";
|
|
46
|
+
|
|
47
|
+
// The five finder lenses (T2 pooled parallax). IDs are fixed doctrine; the
|
|
48
|
+
// paragraph text lives in refute-charter.md's "## Lenses" section and is loaded
|
|
49
|
+
// via parseLenses(). The hardcoded fallback below preserves diversity when that
|
|
50
|
+
// section is absent or unparseable — degraded loudly, never silently.
|
|
51
|
+
const LENS_IDS = Object.freeze([
|
|
52
|
+
"exploit-author", "oncall-3am", "new-hire-reader", "performance-profiler", "staff-skeptic",
|
|
53
|
+
]);
|
|
54
|
+
const FALLBACK_LENSES = Object.freeze([
|
|
55
|
+
{ id: "exploit-author", text: "Read as an attacker: trace every path from an untrusted boundary to a dangerous sink (shell, query, template, deserializer, file path, outbound request). Cite the line where attacker-controlled data reaches the call and the input that turns it hostile." },
|
|
56
|
+
{ id: "oncall-3am", text: "Read as the engineer the pager just woke: hunt for what fails silently or un-diagnosably — swallowed errors, missing timeouts, retries that mask the cause, states with no breadcrumb. Cite the line where a real failure produces no signal or the wrong one." },
|
|
57
|
+
{ id: "new-hire-reader", text: "Read as someone here on day one, trusting names and comments: hunt for anything that builds a wrong mental model — misleading names, comments that contradict the code, magic constants, implicit coupling. Cite the line a careful reader would misunderstand and the mistake it invites." },
|
|
58
|
+
{ id: "performance-profiler", text: "Read with a flamegraph in mind: hunt for cost hidden in innocent code — a query inside a loop, a quadratic scan, blocking I/O on a hot path, unbounded growth on untrusted input. Cite the line whose cost is super-linear in something a caller controls and the scale that stalls it." },
|
|
59
|
+
{ id: "staff-skeptic", text: "Read as a staff engineer in design review: look past the line to the decision it encodes and how it ages — eroding layer boundaries, unenforced invariants, two sources of truth, load-bearing 'temporary' shapes. Cite where intent and implementation diverge and the change that would break it." },
|
|
60
|
+
]);
|
|
61
|
+
|
|
62
|
+
// ---------------------------------------------------------------------------
|
|
63
|
+
// Pure helpers
|
|
64
|
+
// ---------------------------------------------------------------------------
|
|
65
|
+
export function dedupeKey(candidate) {
|
|
66
|
+
const claim = String(candidate?.claim ?? candidate?.title ?? "")
|
|
67
|
+
.toLowerCase().replace(/\s+/g, " ").trim();
|
|
68
|
+
return sha256Hex(`${candidate?.dimension ?? ""}|${candidate?.file ?? ""}|${claim}`);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Split the refute-charter doc into the finder base prompt (everything ABOVE
|
|
72
|
+
// "## Lenses" — this is what every finder sees, replacing the pre-T2 whole-file
|
|
73
|
+
// load) and the parsed lens catalog. Fallback (section absent or not the
|
|
74
|
+
// expected 5 lenses): base = the whole doc (pre-T2 behavior preserved) and the
|
|
75
|
+
// hardcoded catalog is used — a declared degradation, logged loudly via
|
|
76
|
+
// log.warn so a malformed reference never silently collapses lens diversity.
|
|
77
|
+
export function parseLenses(refuteDoc, log) {
|
|
78
|
+
const doc = String(refuteDoc ?? "");
|
|
79
|
+
const headingRx = /^##\s+Lenses\s*$/m;
|
|
80
|
+
const m = headingRx.exec(doc);
|
|
81
|
+
if (m) {
|
|
82
|
+
const base = doc.slice(0, m.index).replace(/\s+$/, "");
|
|
83
|
+
const section = doc.slice(m.index + m[0].length);
|
|
84
|
+
const lenses = [];
|
|
85
|
+
const lensRx = /^###\s+(.+?)\s*$/gm;
|
|
86
|
+
let lm;
|
|
87
|
+
const marks = [];
|
|
88
|
+
while ((lm = lensRx.exec(section)) !== null) marks.push({ id: lm[1].trim(), start: lensRx.lastIndex });
|
|
89
|
+
for (let i = 0; i < marks.length; i++) {
|
|
90
|
+
const end = i + 1 < marks.length
|
|
91
|
+
? section.lastIndexOf("###", marks[i + 1].start)
|
|
92
|
+
: section.length;
|
|
93
|
+
const text = section.slice(marks[i].start, end === -1 ? section.length : end).trim();
|
|
94
|
+
if (marks[i].id && text) lenses.push({ id: marks[i].id, text });
|
|
95
|
+
}
|
|
96
|
+
const ids = lenses.map((l) => l.id);
|
|
97
|
+
const wellFormed = lenses.length === LENS_IDS.length && LENS_IDS.every((id) => ids.includes(id));
|
|
98
|
+
if (wellFormed) return { base, lenses };
|
|
99
|
+
}
|
|
100
|
+
log?.warn?.(
|
|
101
|
+
'refute-charter.md: "## Lenses" section absent or unparseable — falling back to the ' +
|
|
102
|
+
`${FALLBACK_LENSES.length} hardcoded finder lenses (declared degradation; lens diversity preserved).`,
|
|
103
|
+
);
|
|
104
|
+
return { base: doc.replace(/\s+$/, ""), lenses: FALLBACK_LENSES.map((l) => ({ ...l })) };
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// The MAXIMUM pool width from the --n flag. When --n is unset the ceiling is 1
|
|
108
|
+
// (pooling is opt-in): a no-flag `audit` keeps the pre-T2 single-finder call
|
|
109
|
+
// count per pass, which the frozen identify.test.js "MAX_PASSES cap" rail pins
|
|
110
|
+
// exactly (a weight-5 dimension at width>1 would go dry early and never hit the
|
|
111
|
+
// cap). Lens ROTATION across passes is still active at width 1; only the
|
|
112
|
+
// within-pass FAN-OUT needs --n>1. --n N raises the ceiling to N.
|
|
113
|
+
function charterPoolMax(n) {
|
|
114
|
+
return Number.isInteger(n) && n >= 1 ? n : 1;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Charter-weighted finder pool width (T2). `n` is the --n ceiling (the MAXIMUM
|
|
118
|
+
// pool width). A high-weight dimension gets the full width; mid-weight gets
|
|
119
|
+
// half (floored, min 1); weight 1 gets a single finder — no pooling, identical
|
|
120
|
+
// to the pre-T2 single-call behavior for that dimension.
|
|
121
|
+
export function charterPoolWidth(weight, n) {
|
|
122
|
+
const cap = Number.isInteger(n) && n >= 1 ? n : 1;
|
|
123
|
+
const w = Number(weight);
|
|
124
|
+
let width;
|
|
125
|
+
if (w >= 4) width = cap;
|
|
126
|
+
else if (w >= 2) width = Math.max(1, Math.floor(cap / 2));
|
|
127
|
+
else width = 1;
|
|
128
|
+
return Math.min(cap, Math.max(1, width));
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function patchPhase(state, phase, patch) {
|
|
132
|
+
return { ...state, phases: { ...state.phases, [phase]: { ...state.phases[phase], ...patch } } };
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function makeGateError(_message, gate, detail) {
|
|
136
|
+
return gateError(gate, detail); // H15 — shared, well-formed message
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function clamp01(n, dflt = 0.5) {
|
|
140
|
+
const v = Number(n);
|
|
141
|
+
if (!Number.isFinite(v)) return dflt;
|
|
142
|
+
return Math.min(1, Math.max(0, v));
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function validateCandidate(raw, dimensionId, log) {
|
|
146
|
+
if (!raw || typeof raw !== "object") return null;
|
|
147
|
+
const file = raw.file;
|
|
148
|
+
const line = Number(raw.line);
|
|
149
|
+
const severity = String(raw.severity ?? "").toLowerCase();
|
|
150
|
+
const ok =
|
|
151
|
+
typeof raw.title === "string" && raw.title.trim() !== "" &&
|
|
152
|
+
typeof file === "string" && isSafeRelPath(file) &&
|
|
153
|
+
Number.isInteger(line) && line >= 1 &&
|
|
154
|
+
SEVERITIES.has(severity);
|
|
155
|
+
if (!ok) {
|
|
156
|
+
log?.warn?.(`finder:${dimensionId}: dropped invalid candidate (fail closed): ${truncate(JSON.stringify(raw ?? null), 120)}`);
|
|
157
|
+
return null;
|
|
158
|
+
}
|
|
159
|
+
return {
|
|
160
|
+
dimension: dimensionId,
|
|
161
|
+
title: raw.title.trim(),
|
|
162
|
+
claim: typeof raw.claim === "string" && raw.claim.trim() !== "" ? raw.claim.trim() : raw.title.trim(),
|
|
163
|
+
file, line, severity,
|
|
164
|
+
confidence: clamp01(raw.confidence),
|
|
165
|
+
method: raw.method === "static" ? "static" : null,
|
|
166
|
+
check: raw.check ?? null,
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function parseExtraDimensions(value) {
|
|
171
|
+
if (!Array.isArray(value)) return [];
|
|
172
|
+
const out = [];
|
|
173
|
+
for (const e of value) {
|
|
174
|
+
let id = null, label = null, weight = 3;
|
|
175
|
+
if (e && typeof e === "object" && e.id) {
|
|
176
|
+
id = String(e.id); label = String(e.label ?? e.id);
|
|
177
|
+
const w = Number(e.weight); if (Number.isFinite(w) && w >= 1) weight = w;
|
|
178
|
+
} else if (typeof e === "string" && e.trim() !== "") {
|
|
179
|
+
const [rawId, rawW] = e.split(":");
|
|
180
|
+
id = rawId.trim(); label = id;
|
|
181
|
+
const w = Number(rawW); if (Number.isFinite(w) && w >= 1) weight = w;
|
|
182
|
+
}
|
|
183
|
+
if (id && /^[a-z0-9_-]+$/i.test(id)) out.push({ id, label, weight });
|
|
184
|
+
}
|
|
185
|
+
return out;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function orderedDimensions(charterMeta) {
|
|
189
|
+
const weights = charterMeta?.weights && typeof charterMeta.weights === "object" ? charterMeta.weights : {};
|
|
190
|
+
const dims = TAXONOMY.map((t, i) => {
|
|
191
|
+
const w = Number(weights[t.id]);
|
|
192
|
+
return { id: t.id, label: t.label, weight: Number.isFinite(w) && w >= 1 ? w : 1, order: i };
|
|
193
|
+
});
|
|
194
|
+
parseExtraDimensions(charterMeta?.extraDimensions).forEach((e, i) => {
|
|
195
|
+
if (!dims.some((d) => d.id === e.id)) dims.push({ ...e, order: 100 + i });
|
|
196
|
+
});
|
|
197
|
+
return dims.sort((a, b) => b.weight - a.weight || a.order - b.order);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function loadRef(name, fallback) {
|
|
201
|
+
try { return readPackageFile(`do-better/references/${name}`); } catch { return fallback; }
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function dimensionBrief(taxonomyDoc, dim) {
|
|
205
|
+
if (taxonomyDoc) {
|
|
206
|
+
const rx = new RegExp(`^##\\s+${dim.label.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\s*$`, "mi");
|
|
207
|
+
const m = rx.exec(taxonomyDoc);
|
|
208
|
+
if (m) {
|
|
209
|
+
const rest = taxonomyDoc.slice(m.index + m[0].length);
|
|
210
|
+
const next = rest.search(/^##\s+/m);
|
|
211
|
+
const section = (next === -1 ? rest : rest.slice(0, next)).trim();
|
|
212
|
+
if (section) return truncate(section, 2000);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
return `Find concrete, citable evidence of "${dim.label}" problems in this codebase: anything a skeptical senior engineer would flag on that dimension.`;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
async function jsonCall(llm, args, fallbackFn) {
|
|
219
|
+
// json:true routes withFallback to llm.callJson (object + re-ask retry);
|
|
220
|
+
// jsonMode:true covers implementations that thread it into llm.call instead.
|
|
221
|
+
// Either way a string result is parsed defensively below (fail closed).
|
|
222
|
+
const out = await withFallback(llm, { ...args, json: true, jsonMode: true }, fallbackFn);
|
|
223
|
+
if (typeof out !== "string") return out;
|
|
224
|
+
try {
|
|
225
|
+
return JSON.parse(cleanJsonResponse(out));
|
|
226
|
+
} catch {
|
|
227
|
+
throw new OpError(`[${args.label}] returned unparseable JSON`);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// ---------------------------------------------------------------------------
|
|
232
|
+
// Coverage slices (from D1's declared coverage manifest)
|
|
233
|
+
// ---------------------------------------------------------------------------
|
|
234
|
+
function deepReadFileList(dotdir, state) {
|
|
235
|
+
const manifest = readArtifact(dotdir, LAYOUT.comprehension.coverageManifest);
|
|
236
|
+
if (manifest?.body) {
|
|
237
|
+
const lines = manifest.body.split("\n");
|
|
238
|
+
const start = lines.findIndex((l) => l.trim() === "## Deep-read files");
|
|
239
|
+
if (start !== -1) {
|
|
240
|
+
const files = [];
|
|
241
|
+
for (let i = start + 1; i < lines.length; i++) {
|
|
242
|
+
const line = lines[i].trim();
|
|
243
|
+
if (line.startsWith("## ")) break;
|
|
244
|
+
const m = /^-\s+(.+)$/.exec(line);
|
|
245
|
+
if (m && m[1] !== "(none)" && isSafeRelPath(m[1])) files.push(m[1]);
|
|
246
|
+
}
|
|
247
|
+
if (files.length) return files;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
const largest = state?.phases?.scan?.facts?.largestFiles;
|
|
251
|
+
if (Array.isArray(largest)) return largest.map((f) => f?.file).filter((f) => typeof f === "string" && isSafeRelPath(f));
|
|
252
|
+
return [];
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function loadSlices(root, files) {
|
|
256
|
+
const slices = [];
|
|
257
|
+
for (const file of files) {
|
|
258
|
+
try {
|
|
259
|
+
const raw = fs.readFileSync(path.join(root, file), "utf8");
|
|
260
|
+
// Record read-time truncation (H7): content past SLICE_CHARS is dropped
|
|
261
|
+
// here and never reaches ANY finder cell. Without this flag the coverage
|
|
262
|
+
// manifest silently claims full coverage of a large file, because the
|
|
263
|
+
// downstream truncatedFiles check compares the ALREADY-capped raw against
|
|
264
|
+
// the larger PACKET_BYTES and so almost never fires — a breach of the
|
|
265
|
+
// declared-never-silent doctrine (D10).
|
|
266
|
+
slices.push({ file, raw: truncate(raw, SLICE_CHARS), truncated: raw.length > SLICE_CHARS });
|
|
267
|
+
} catch { /* unreadable — declared in coverage manifest already */ }
|
|
268
|
+
}
|
|
269
|
+
return slices;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// Content fingerprint over the ordered, readable deep-read set (adversarial
|
|
273
|
+
// review finding, round 4): the dry-cell resume cache below is keyed on
|
|
274
|
+
// headSha, but packet identity is POSITIONAL and content is read from the
|
|
275
|
+
// WORKING TREE, not the committed blob. Two things can change while headSha
|
|
276
|
+
// stays constant — file content (a budget-stop resume is precisely the flow
|
|
277
|
+
// where uncommitted edits are live) and the file SET/order (`audit` always
|
|
278
|
+
// re-runs D1 comprehend before D2, which can regenerate a different deep-read
|
|
279
|
+
// list). Either shift silently invalidates a positional index -> packet
|
|
280
|
+
// mapping: a recorded-dry packet 0 could be entirely different files/content
|
|
281
|
+
// on resume. This fingerprint changes on ANY such drift, so the completion
|
|
282
|
+
// guard can invalidate the WHOLE dry-cell cache (full re-examination) rather
|
|
283
|
+
// than trusting a positional index against content it never actually
|
|
284
|
+
// verified. Whole-set invalidation, not per-packet reconciliation: simpler
|
|
285
|
+
// and safe — the failure mode of over-invalidating is one extra pass, not a
|
|
286
|
+
// silently unaudited file.
|
|
287
|
+
function packetSetFingerprint(slices) {
|
|
288
|
+
const parts = slices.map((s) => `${s.file}:${sha256Hex(s.raw)}`);
|
|
289
|
+
return sha256Hex(parts.join("|"));
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// One slice → its numbered, file-delimited chunk (the unit of packetization).
|
|
293
|
+
function renderSlice(s) {
|
|
294
|
+
const numbered = s.raw.split("\n").map((l, i) => `${i + 1}: ${l}`).join("\n");
|
|
295
|
+
return `\n=== ${s.file} ===\n${numbered}\n`;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// Render a group of slices into a single finder packet. Skip-and-continue (NOT
|
|
299
|
+
// break): a chunk that would overflow maxBytes is skipped so later slices still
|
|
300
|
+
// get a chance; a SINGLE chunk larger than maxBytes is hard-truncated rather
|
|
301
|
+
// than dropped — given ≥1 readable slice the packet is never empty. Callers
|
|
302
|
+
// that need every slice covered must partition first (partitionSlices), which
|
|
303
|
+
// only ever hands this a group that already fits (or a lone oversized slice).
|
|
304
|
+
function buildFinderPacket(slices, maxBytes = PACKET_BYTES) {
|
|
305
|
+
let packet = "";
|
|
306
|
+
for (const s of slices) {
|
|
307
|
+
const chunk = renderSlice(s);
|
|
308
|
+
if (chunk.length > maxBytes) {
|
|
309
|
+
packet += truncate(chunk, maxBytes);
|
|
310
|
+
continue;
|
|
311
|
+
}
|
|
312
|
+
if (packet.length + chunk.length > maxBytes) continue;
|
|
313
|
+
packet += chunk;
|
|
314
|
+
}
|
|
315
|
+
return packet || "(no deep-read slices available)";
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// Partition the WHOLE readable deep-read set into finder packets: every slice
|
|
319
|
+
// lands in exactly one packet, in input order; a slice whose rendered chunk
|
|
320
|
+
// exceeds maxBytes becomes its own hard-truncated singleton packet; non-empty
|
|
321
|
+
// input never yields []. This replaces the old "rotate one shared window"
|
|
322
|
+
// scheme — the loop now examines the entire set, never just the head.
|
|
323
|
+
export function partitionSlices(slices, maxBytes = PACKET_BYTES) {
|
|
324
|
+
const packets = [];
|
|
325
|
+
let group = [];
|
|
326
|
+
let groupBytes = 0;
|
|
327
|
+
const flush = () => {
|
|
328
|
+
if (group.length === 0) return;
|
|
329
|
+
packets.push({ files: group.map((s) => s.file), packet: buildFinderPacket(group, maxBytes) });
|
|
330
|
+
group = [];
|
|
331
|
+
groupBytes = 0;
|
|
332
|
+
};
|
|
333
|
+
for (const s of slices) {
|
|
334
|
+
const chunk = renderSlice(s);
|
|
335
|
+
if (chunk.length > maxBytes) {
|
|
336
|
+
flush();
|
|
337
|
+
packets.push({ files: [s.file], packet: buildFinderPacket([s], maxBytes) });
|
|
338
|
+
continue;
|
|
339
|
+
}
|
|
340
|
+
if (groupBytes + chunk.length > maxBytes) flush();
|
|
341
|
+
group.push(s);
|
|
342
|
+
groupBytes += chunk.length;
|
|
343
|
+
}
|
|
344
|
+
flush();
|
|
345
|
+
return packets;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// ---------------------------------------------------------------------------
|
|
349
|
+
// Offline static finders (deterministic, one pass per dimension)
|
|
350
|
+
// ---------------------------------------------------------------------------
|
|
351
|
+
function regexCandidates(slices, dimension, rx, { title, claim, severity, perFile = true }) {
|
|
352
|
+
const out = [];
|
|
353
|
+
for (const s of slices) {
|
|
354
|
+
const lines = s.raw.split("\n");
|
|
355
|
+
for (let i = 0; i < lines.length; i++) {
|
|
356
|
+
if (rx.test(lines[i])) {
|
|
357
|
+
out.push({
|
|
358
|
+
dimension, title: `${title} in ${s.file}`, claim: `${claim} (line ${i + 1})`,
|
|
359
|
+
file: s.file, line: i + 1, severity, confidence: 0.9,
|
|
360
|
+
method: "static", check: { type: "regex", pattern: rx.source, flags: rx.flags.replace("g", "") },
|
|
361
|
+
});
|
|
362
|
+
if (perFile) break;
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
return out;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
function staticFinderPass(dimId, root, slices) {
|
|
370
|
+
switch (dimId) {
|
|
371
|
+
case "maintainability":
|
|
372
|
+
return regexCandidates(slices, dimId, /\b(TODO|FIXME|HACK)\b/, {
|
|
373
|
+
title: "Acknowledged debt marker", claim: "TODO/FIXME/HACK marker indicates acknowledged unaddressed debt", severity: "low",
|
|
374
|
+
});
|
|
375
|
+
case "security":
|
|
376
|
+
return [
|
|
377
|
+
...regexCandidates(slices, dimId, /\beval\s*\(/, {
|
|
378
|
+
title: "eval() usage", claim: "eval() executes dynamically constructed code", severity: "high",
|
|
379
|
+
}),
|
|
380
|
+
...regexCandidates(slices, dimId, /(api[_-]?key|secret|password)\s*[:=]\s*["'][^"']{8,}["']/i, {
|
|
381
|
+
title: "Possible hardcoded secret", claim: "string literal assigned to a secret-named binding", severity: "high",
|
|
382
|
+
}),
|
|
383
|
+
];
|
|
384
|
+
case "correctness":
|
|
385
|
+
return regexCandidates(slices, dimId, /catch\s*(\([^)]*\))?\s*\{\s*\}/, {
|
|
386
|
+
title: "Empty catch block", claim: "errors are silently swallowed by an empty catch block", severity: "medium",
|
|
387
|
+
});
|
|
388
|
+
case "test-quality": {
|
|
389
|
+
const hasTests = slices.some((s) => /(^|\/)(test|tests|__tests__)\//.test(s.file) || /\.(test|spec)\./.test(s.file));
|
|
390
|
+
if (!hasTests && fs.existsSync(path.join(root, "package.json"))) {
|
|
391
|
+
return [{
|
|
392
|
+
dimension: dimId, title: "No test files in analyzed set", claim: "no test files were found among analyzed files",
|
|
393
|
+
file: "package.json", line: 1, severity: "medium", confidence: 0.7,
|
|
394
|
+
method: "static", check: { type: "no-tests" },
|
|
395
|
+
}];
|
|
396
|
+
}
|
|
397
|
+
return [];
|
|
398
|
+
}
|
|
399
|
+
case "dependency-health": {
|
|
400
|
+
try {
|
|
401
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(root, "package.json"), "utf8"));
|
|
402
|
+
const hasDeps = Object.keys(pkg.dependencies ?? {}).length > 0;
|
|
403
|
+
const hasLock = ["package-lock.json", "pnpm-lock.yaml", "yarn.lock"].some((f) => fs.existsSync(path.join(root, f)));
|
|
404
|
+
if (hasDeps && !hasLock) {
|
|
405
|
+
return [{
|
|
406
|
+
dimension: dimId, title: "Dependencies without a lockfile", claim: "runtime dependencies declared but no lockfile is committed",
|
|
407
|
+
file: "package.json", line: 1, severity: "medium", confidence: 0.9,
|
|
408
|
+
method: "static", check: { type: "no-lockfile" },
|
|
409
|
+
}];
|
|
410
|
+
}
|
|
411
|
+
} catch { /* no parseable package.json */ }
|
|
412
|
+
return [];
|
|
413
|
+
}
|
|
414
|
+
case "dx": {
|
|
415
|
+
if (fs.existsSync(path.join(root, "package.json")) && !fs.existsSync(path.join(root, "README.md"))) {
|
|
416
|
+
return [{
|
|
417
|
+
dimension: dimId, title: "Missing README", claim: "repository has no README.md onboarding entry point",
|
|
418
|
+
file: "package.json", line: 1, severity: "low", confidence: 0.9,
|
|
419
|
+
method: "static", check: { type: "no-readme" },
|
|
420
|
+
}];
|
|
421
|
+
}
|
|
422
|
+
return [];
|
|
423
|
+
}
|
|
424
|
+
default:
|
|
425
|
+
return []; // performance / operability / extras: no safe deterministic heuristic offline
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
// Self-contained, re-runnable check spec for a static candidate: the cited file
|
|
430
|
+
// rides along so refresh/roadmap can re-execute it without the candidate object.
|
|
431
|
+
function staticCheckSpec(cand) {
|
|
432
|
+
return { ...(cand.check ?? {}), file: cand.check?.file ?? cand.file };
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
// ---------------------------------------------------------------------------
|
|
436
|
+
// Adversarial verification (reproduce-or-kill, F2/F4)
|
|
437
|
+
// ---------------------------------------------------------------------------
|
|
438
|
+
function sanitizeRepro(obj, root) {
|
|
439
|
+
if (obj && typeof obj === "object" && obj.grep && typeof obj.grep === "object") {
|
|
440
|
+
const { pattern, file } = obj.grep;
|
|
441
|
+
if (typeof pattern === "string" && pattern.length <= 200 &&
|
|
442
|
+
typeof file === "string" && isSafeRelPath(file)) {
|
|
443
|
+
return { kind: "grep", pattern, file };
|
|
444
|
+
}
|
|
445
|
+
return null;
|
|
446
|
+
}
|
|
447
|
+
const cmd = obj?.reproCmd;
|
|
448
|
+
if (typeof cmd !== "string") return null;
|
|
449
|
+
let m = /^node --test\s+(\S+)$/.exec(cmd.trim());
|
|
450
|
+
if (m && isSafeRelPath(m[1]) && fs.existsSync(path.join(root, m[1]))) return { kind: "test", file: m[1] };
|
|
451
|
+
m = /^node -e\s+([\s\S]+)$/.exec(cmd.trim());
|
|
452
|
+
if (m) {
|
|
453
|
+
let snippet = m[1].trim();
|
|
454
|
+
if ((snippet.startsWith('"') && snippet.endsWith('"')) || (snippet.startsWith("'") && snippet.endsWith("'"))) {
|
|
455
|
+
snippet = snippet.slice(1, -1);
|
|
456
|
+
}
|
|
457
|
+
if (snippet.length > 0 && snippet.length <= 500) return { kind: "eval", snippet };
|
|
458
|
+
}
|
|
459
|
+
return null;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
// A repro is "demonstrative" — trustworthy enough to ALONE flip a candidate to
|
|
463
|
+
// verified — only when it genuinely ties to the claim (H9). A repo-authored
|
|
464
|
+
// test (`node --test`) and a grep with a SPECIFIC pattern qualify. A
|
|
465
|
+
// model-proposed `node -e` snippet and a catch-all grep (matches virtually any
|
|
466
|
+
// non-empty file) do NOT: they can pass without demonstrating anything, so a
|
|
467
|
+
// candidate backed only by one is not verified on the command alone — it must
|
|
468
|
+
// still earn a CONFIRM from the blind frontier verifier. As a bonus, a
|
|
469
|
+
// non-demonstrative `node -e` is never executed here, removing that
|
|
470
|
+
// prompt-injection surface (a hostile brownfield repo can no longer coax the
|
|
471
|
+
// mid-tier proposer into running arbitrary code).
|
|
472
|
+
const CATCHALL_GREP = new Set([".", ".*", ".+", ".*.*", "^", "$", "", "[\\s\\S]*", "\\S*", "\\s*", "\\w*", "[^]*"]);
|
|
473
|
+
function isDemonstrativeRepro(repro) {
|
|
474
|
+
if (!repro) return false;
|
|
475
|
+
if (repro.kind === "test") return true;
|
|
476
|
+
if (repro.kind === "grep") {
|
|
477
|
+
const p = String(repro.pattern ?? "").trim();
|
|
478
|
+
return p.length > 0 && !CATCHALL_GREP.has(p);
|
|
479
|
+
}
|
|
480
|
+
return false; // eval (node -e): never demonstrative, and never run
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
function runRepro(root, repro, exec) {
|
|
484
|
+
if (repro.kind === "grep") {
|
|
485
|
+
const check = { type: "grep", pattern: repro.pattern, file: repro.file };
|
|
486
|
+
const res = runReproCheck(root, check);
|
|
487
|
+
return { exitCode: res.ok === true ? 0 : 1, record: res.record, check };
|
|
488
|
+
}
|
|
489
|
+
const args = repro.kind === "test" ? ["--test", repro.file] : ["-e", repro.snippet];
|
|
490
|
+
const r = exec("node", args, { cwd: root, timeout: REPRO_TIMEOUT_MS });
|
|
491
|
+
const status = typeof r.status === "number" ? r.status : 1;
|
|
492
|
+
return {
|
|
493
|
+
exitCode: status,
|
|
494
|
+
cmd: ["node", ...args],
|
|
495
|
+
record: truncate(`$ node ${repro.kind === "test" ? `--test ${repro.file}` : "-e <snippet>"}\nexit ${status}\n${r.stdout ?? ""}\n${r.stderr ?? ""}`.trim(), 2000),
|
|
496
|
+
};
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
function codeSlice(root, file, line, span) {
|
|
500
|
+
try {
|
|
501
|
+
const lines = fs.readFileSync(path.join(root, file), "utf8").split("\n");
|
|
502
|
+
const start = Math.max(0, line - 1 - span);
|
|
503
|
+
const end = Math.min(lines.length, line + span);
|
|
504
|
+
return lines.slice(start, end).map((l, i) => `${start + i + 1}: ${l}`).join("\n");
|
|
505
|
+
} catch {
|
|
506
|
+
return "(cited file unreadable)";
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
async function verifyCandidate(ctx, cand, head7) {
|
|
511
|
+
const { root, llm, exec, log } = ctx;
|
|
512
|
+
const citation = { file: cand.file, line: cand.line, sha: head7 };
|
|
513
|
+
const { verified } = verifyCitations(root, [citation], exec);
|
|
514
|
+
if (verified.length === 0) return { verified: false, reason: "citation does not verify against the worktree" };
|
|
515
|
+
const evidence = [{ file: cand.file, line: cand.line, sha: head7 }];
|
|
516
|
+
|
|
517
|
+
if (cand.method === "static") {
|
|
518
|
+
const checkSpec = staticCheckSpec(cand);
|
|
519
|
+
const check = runReproCheck(root, checkSpec);
|
|
520
|
+
return check.ok === true
|
|
521
|
+
? { verified: true, evidence, reproduction: { method: "static", record: check.record, exitCode: null, check: checkSpec } }
|
|
522
|
+
: { verified: false, reason: `static check did not reproduce: ${check.record}` };
|
|
523
|
+
}
|
|
524
|
+
if (llm.offline) return { verified: false, reason: "offline: no deterministic reproduction available" };
|
|
525
|
+
|
|
526
|
+
// a) deterministic reproduction first
|
|
527
|
+
let repro = null;
|
|
528
|
+
try {
|
|
529
|
+
const obj = await jsonCall(llm, {
|
|
530
|
+
prompt: `Claim: ${cand.claim}\nCited location: ${cand.file}:${cand.line}\nPropose a mechanical reproduction check, or {"reproCmd":null}.`,
|
|
531
|
+
system: REPRO_SYSTEM, tier: "mid", label: "repro-cmd",
|
|
532
|
+
}, () => ({ reproCmd: null }));
|
|
533
|
+
repro = sanitizeRepro(obj, root);
|
|
534
|
+
} catch (e) {
|
|
535
|
+
if (e instanceof BudgetError) throw e;
|
|
536
|
+
repro = null; // unparseable proposal → fall through to blind reread
|
|
537
|
+
}
|
|
538
|
+
// Only a DEMONSTRATIVE repro (H9) may alone verify — a non-demonstrative one
|
|
539
|
+
// (model `node -e`, catch-all grep) is ignored here and the candidate falls
|
|
540
|
+
// through to the blind reread, which must independently CONFIRM the claim.
|
|
541
|
+
if (repro && isDemonstrativeRepro(repro)) {
|
|
542
|
+
const r = runRepro(root, repro, exec);
|
|
543
|
+
if (r.exitCode === 0) {
|
|
544
|
+
return {
|
|
545
|
+
verified: true,
|
|
546
|
+
evidence,
|
|
547
|
+
reproduction: {
|
|
548
|
+
method: "command", record: r.record, exitCode: 0,
|
|
549
|
+
...(r.cmd ? { cmd: r.cmd } : {}),
|
|
550
|
+
...(r.check ? { check: r.check } : {}),
|
|
551
|
+
},
|
|
552
|
+
};
|
|
553
|
+
}
|
|
554
|
+
return { verified: false, reason: `reproduction command failed (exit ${r.exitCode})` };
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
// b) blind frontier reread — proposer reasoning withheld (F2). Reached when no
|
|
558
|
+
// repro was proposed OR the proposal was non-demonstrative (H9): a
|
|
559
|
+
// trivially-passing check cannot alone verify a finding.
|
|
560
|
+
try {
|
|
561
|
+
const slice = codeSlice(root, cand.file, cand.line, 40);
|
|
562
|
+
const obj = await jsonCall(llm, {
|
|
563
|
+
prompt: `Bare claim: ${cand.claim}\n\nCited code slice (${cand.file}, ±40 lines around line ${cand.line}):\n${slice}`,
|
|
564
|
+
system: VERDICT_SYSTEM, tier: "frontier", label: "verdict",
|
|
565
|
+
}, () => ({ verdict: "UNCERTAIN" }));
|
|
566
|
+
const v = String(obj?.verdict ?? "").toUpperCase();
|
|
567
|
+
if (v === "CONFIRM") {
|
|
568
|
+
return {
|
|
569
|
+
verified: true, evidence,
|
|
570
|
+
reproduction: { method: "reread", record: truncate(`CONFIRM (blind reread): ${String(obj?.reason ?? "")}`.trim(), 1000), exitCode: null },
|
|
571
|
+
};
|
|
572
|
+
}
|
|
573
|
+
log?.substep?.(`killed (verdict ${v || "missing"}): ${cand.title}`);
|
|
574
|
+
return { verified: false, reason: `verdict ${v || "missing"}` };
|
|
575
|
+
} catch (e) {
|
|
576
|
+
if (e instanceof BudgetError) throw e;
|
|
577
|
+
return { verified: false, reason: "unparseable verdict (fail closed)" };
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
// ---------------------------------------------------------------------------
|
|
582
|
+
// T3 semantic dedupe — the SECOND admission filter, layered over the free hash
|
|
583
|
+
// filter (dedupeKey). Only hash-survivors reach here, and only online (the
|
|
584
|
+
// offline path never calls finderCell). One cheap-tier call per survivor,
|
|
585
|
+
// comparing it against `priorSameCell` — prior ADMITTED entries (this run's
|
|
586
|
+
// pool + prior VERIFIED findings) that share the candidate's dimension AND
|
|
587
|
+
// file. Returns true iff the model judges the candidate a paraphrase of one of
|
|
588
|
+
// them.
|
|
589
|
+
//
|
|
590
|
+
// FAILS OPEN — deliberately, and this is the ONE sanctioned fail-open path in
|
|
591
|
+
// D2; every other failure here fails CLOSED (drop the candidate, kill the
|
|
592
|
+
// finding). An unparseable response, an out-of-range index, or a thrown error
|
|
593
|
+
// (network exhaustion, bad JSON) is treated as "not a duplicate" → the
|
|
594
|
+
// candidate is ADMITTED. Rationale for the asymmetry: a false NEW costs one
|
|
595
|
+
// wasted downstream verification call — and verification kills genuine junk
|
|
596
|
+
// anyway — whereas a false DUPLICATE permanently suppresses a real finding that
|
|
597
|
+
// nothing downstream can resurrect. When the cheap judge is uncertain or
|
|
598
|
+
// broken, admitting is the recoverable error. (BudgetError is NOT a
|
|
599
|
+
// semantic-check failure — it is the hard spend ceiling, so it still rethrows
|
|
600
|
+
// stop-the-world, matching every other call site in this file.)
|
|
601
|
+
async function isSemanticDuplicate(ctx, cand, priorSameCell) {
|
|
602
|
+
const { llm, log } = ctx;
|
|
603
|
+
if (priorSameCell.length === 0) return false; // nothing to be a duplicate of — skip the call entirely
|
|
604
|
+
// Location is part of the duplicate judgment (H6): two genuinely distinct
|
|
605
|
+
// defects of the same class in one file produce near-identical title/claim
|
|
606
|
+
// text, so without file:line the cheap judge has no signal to separate
|
|
607
|
+
// "paraphrase of the same issue" from "same class at a different location" —
|
|
608
|
+
// and a false duplicate is permanently unrecoverable (see doctrine above).
|
|
609
|
+
const list = priorSameCell
|
|
610
|
+
.map((p, i) => `${i}. location: ${p.file}:${p.line ?? "?"}\n title: ${p.title}\n claim: ${p.claim}`)
|
|
611
|
+
.join("\n");
|
|
612
|
+
const prompt =
|
|
613
|
+
`Prior findings for dimension "${cand.dimension}" in ${cand.file}:\n${list}\n\n` +
|
|
614
|
+
`New finding:\n location: ${cand.file}:${cand.line}\n title: ${cand.title}\n claim: ${cand.claim}\n\n` +
|
|
615
|
+
'Is the new finding a semantic duplicate of one listed above? A different code location ' +
|
|
616
|
+
'(line) usually means a DISTINCT instance, even when the wording matches. ' +
|
|
617
|
+
'Respond {"duplicateOf": <index>|null}.';
|
|
618
|
+
try {
|
|
619
|
+
const obj = await jsonCall(llm, {
|
|
620
|
+
prompt, system: DEDUPE_SYSTEM, tier: "cheap", label: `dedupe:${cand.dimension}`,
|
|
621
|
+
}, () => ({ duplicateOf: null }));
|
|
622
|
+
const idx = obj?.duplicateOf;
|
|
623
|
+
if (idx === null || idx === undefined) return false; // model says genuinely new
|
|
624
|
+
const n = Number(idx);
|
|
625
|
+
if (Number.isInteger(n) && n >= 0 && n < priorSameCell.length) return true;
|
|
626
|
+
// Out-of-range / non-integer index: fail OPEN — admit. (See doctrine above.)
|
|
627
|
+
log?.warn?.(
|
|
628
|
+
`dedupe:${cand.dimension}: semantic check returned an out-of-range index ${JSON.stringify(idx)} ` +
|
|
629
|
+
`(list length ${priorSameCell.length}) — fail open, admitting the candidate.`,
|
|
630
|
+
);
|
|
631
|
+
return false;
|
|
632
|
+
} catch (e) {
|
|
633
|
+
if (e instanceof BudgetError) throw e; // budget ceiling stops the world, never fails open
|
|
634
|
+
// Network exhaustion / unparseable JSON after the LLM layer's own retries:
|
|
635
|
+
// fail OPEN — admit. (See doctrine above.)
|
|
636
|
+
log?.warn?.(
|
|
637
|
+
`dedupe:${cand.dimension}: semantic check failed (${e.message}) — fail open, admitting the candidate.`,
|
|
638
|
+
);
|
|
639
|
+
return false;
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
// ---------------------------------------------------------------------------
|
|
644
|
+
// D2 finder coverage manifest (§7.6) — an auditable record of exactly what the
|
|
645
|
+
// packetized finder loop examined, written idempotently into D1's
|
|
646
|
+
// coverage-manifest.md.
|
|
647
|
+
// ---------------------------------------------------------------------------
|
|
648
|
+
const D2_COVERAGE_HEADING = "## D2 finder coverage";
|
|
649
|
+
|
|
650
|
+
function d2CoverageSection({ dims, offline, passesByDimension, packetsByDimension, suppressedByDimension, examinedFiles, truncatedFiles, unreadableFiles }) {
|
|
651
|
+
const examinedTxt = examinedFiles.length ? examinedFiles.join(", ") : "(none)";
|
|
652
|
+
const truncatedTxt = truncatedFiles.length ? truncatedFiles.join(", ") : "(none)";
|
|
653
|
+
const lines = [D2_COVERAGE_HEADING, ""];
|
|
654
|
+
for (const dim of dims) {
|
|
655
|
+
const packets = packetsByDimension[dim.id] ?? 0;
|
|
656
|
+
const passes = passesByDimension[dim.id] ?? 0;
|
|
657
|
+
const suppressed = suppressedByDimension?.[dim.id] ?? 0;
|
|
658
|
+
lines.push(`### ${dim.id}`);
|
|
659
|
+
lines.push(`- Files examined: ${examinedTxt}`);
|
|
660
|
+
lines.push(`- Packets: ${packets}${offline ? " (offline — no packetization)" : ""}`);
|
|
661
|
+
lines.push(`- Total passes: ${passes}`);
|
|
662
|
+
// T3 semantic suppressions — declared, never silent (adversarial review
|
|
663
|
+
// finding: a false-duplicate permanently loses a real finding, and that
|
|
664
|
+
// outcome was previously invisible anywhere in the run's output).
|
|
665
|
+
lines.push(`- Semantic suppressions: ${suppressed}`);
|
|
666
|
+
lines.push(`- Truncated slices: ${truncatedTxt}`);
|
|
667
|
+
lines.push("");
|
|
668
|
+
}
|
|
669
|
+
lines.push("### Unexamined");
|
|
670
|
+
if (unreadableFiles.length === 0) lines.push("- (none)");
|
|
671
|
+
else for (const f of unreadableFiles) lines.push(`- ${f} (unreadable)`);
|
|
672
|
+
return lines.join("\n");
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
// Replace the existing "## D2 finder coverage" section (if present) or append a
|
|
676
|
+
// fresh one — idempotent, so re-runs update rather than duplicate.
|
|
677
|
+
function replaceOrAppendD2Section(body, section) {
|
|
678
|
+
const lines = String(body ?? "").split("\n");
|
|
679
|
+
const start = lines.findIndex((l) => l.trim() === D2_COVERAGE_HEADING);
|
|
680
|
+
let head, tail;
|
|
681
|
+
if (start === -1) {
|
|
682
|
+
head = lines;
|
|
683
|
+
tail = [];
|
|
684
|
+
} else {
|
|
685
|
+
let end = lines.length;
|
|
686
|
+
for (let i = start + 1; i < lines.length; i++) {
|
|
687
|
+
if (lines[i].startsWith("## ")) { end = i; break; }
|
|
688
|
+
}
|
|
689
|
+
head = lines.slice(0, start);
|
|
690
|
+
tail = lines.slice(end);
|
|
691
|
+
}
|
|
692
|
+
const headTxt = head.join("\n").replace(/\s+$/, "");
|
|
693
|
+
const tailTxt = tail.join("\n").replace(/^\s+/, "").replace(/\s+$/, "");
|
|
694
|
+
const parts = headTxt ? [headTxt, "", section] : [section];
|
|
695
|
+
if (tailTxt) parts.push("", tailTxt);
|
|
696
|
+
return parts.join("\n").replace(/\n{3,}/g, "\n\n").replace(/\s+$/, "") + "\n";
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
function writeD2Coverage(dotdir, data) {
|
|
700
|
+
const rel = LAYOUT.comprehension.coverageManifest;
|
|
701
|
+
const existing = readArtifact(dotdir, rel);
|
|
702
|
+
const meta = existing?.meta ?? {};
|
|
703
|
+
const body = existing?.body ?? "# Coverage Manifest\n";
|
|
704
|
+
writeArtifact(dotdir, rel, { meta, body: replaceOrAppendD2Section(body, d2CoverageSection(data)) });
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
// One (dimension × packet) cell: loop-until-dry (K_DRY consecutive zero-new
|
|
708
|
+
// passes, MAX_PASSES cap). priorConclusions is pool-wide per dimension — a
|
|
709
|
+
// candidate already found via another packet is never re-proposed here.
|
|
710
|
+
async function finderCell(ctx, { dim, packetText, pool, priorFixed, seen, base, lenses, poolWidth, taxonomyDoc }) {
|
|
711
|
+
const { llm, log } = ctx;
|
|
712
|
+
const newCands = [];
|
|
713
|
+
let suppressed = 0;
|
|
714
|
+
let dryStreak = 0;
|
|
715
|
+
let pass = 0;
|
|
716
|
+
while (pass < MAX_PASSES && dryStreak < K_DRY) {
|
|
717
|
+
const passIndex = pass;
|
|
718
|
+
pass += 1;
|
|
719
|
+
// One shared context per pass (prior conclusions + packet); the N pooled
|
|
720
|
+
// calls differ ONLY by their assigned lens. Candidates from all N are
|
|
721
|
+
// validated and deduped together — the pass's newCount is the POOLED total,
|
|
722
|
+
// and the dry streak advances only when the whole pool yields zero new.
|
|
723
|
+
const priorList = priorFixed.concat(pool.map((c) => ({ title: c.title, file: c.file, line: c.line })));
|
|
724
|
+
const priorTxt = priorList.length
|
|
725
|
+
? priorList.map((c) => `- ${c.title} (${c.file}${c.line != null ? `:${c.line}` : ""})`).join("\n")
|
|
726
|
+
: "(none)";
|
|
727
|
+
const prompt =
|
|
728
|
+
`Dimension under refutation: ${dim.label} (charter weight ${dim.weight}).\n\n` +
|
|
729
|
+
`${dimensionBrief(taxonomyDoc, dim)}\n\n` +
|
|
730
|
+
`Conclusions of prior passes — do NOT repeat these; find NEW evidence or return an empty list:\n${priorTxt}\n\n` +
|
|
731
|
+
`Code under review:\n${packetText}\n\n` +
|
|
732
|
+
'Return JSON {"candidates":[{"title":"...","claim":"...","file":"src/x.js","line":12,"severity":"critical|high|medium|low","confidence":0.8}]} — empty array if nothing new.';
|
|
733
|
+
let newCount = 0;
|
|
734
|
+
// The poolWidth finder calls in a pass differ ONLY by their lens and share
|
|
735
|
+
// one prompt/prior-conclusions context — they are independent, so run them
|
|
736
|
+
// concurrently (H8) with bounded concurrency = poolWidth. A BudgetError from
|
|
737
|
+
// any pooled call still rethrows stop-the-world (after in-flight siblings
|
|
738
|
+
// settle); any other error, after the LLM layer's own retries, propagates
|
|
739
|
+
// and fails the phase (fail closed — a silently absent pool member would be
|
|
740
|
+
// undeclared coverage loss). Admission below stays SEQUENTIAL in lens-index
|
|
741
|
+
// order, so seen/pool/semantic-dedupe ordering is identical to the previous
|
|
742
|
+
// one-at-a-time loop — the concurrency is wall-clock only, not behavioral.
|
|
743
|
+
const rawByLens = await mapLimit(
|
|
744
|
+
Array.from({ length: poolWidth }, (_, i) => i),
|
|
745
|
+
poolWidth,
|
|
746
|
+
async (i) => {
|
|
747
|
+
const lens = lenses[(passIndex + i) % lenses.length];
|
|
748
|
+
const system = `${base}\n\nLens: ${lens.text}`;
|
|
749
|
+
const obj = await jsonCall(llm, { prompt, system, tier: "mid", label: `finder:${dim.id}` }, () => ({ candidates: [] }));
|
|
750
|
+
return Array.isArray(obj) ? obj : Array.isArray(obj?.candidates) ? obj.candidates : [];
|
|
751
|
+
},
|
|
752
|
+
);
|
|
753
|
+
for (const raw of rawByLens) {
|
|
754
|
+
for (const r of raw) {
|
|
755
|
+
const cand = validateCandidate(r, dim.id, log);
|
|
756
|
+
if (!cand) continue;
|
|
757
|
+
const key = dedupeKey(cand);
|
|
758
|
+
// (a) Hash filter — free, unchanged, FIRST. Exact/normalized-wording
|
|
759
|
+
// repeats never reach the semantic tier.
|
|
760
|
+
if (seen.has(key)) continue;
|
|
761
|
+
// (b) Semantic filter (T3) — hash-survivors only, online only. Compare
|
|
762
|
+
// against prior ADMITTED entries (prior verified findings + this run's
|
|
763
|
+
// pool) sharing this candidate's dimension AND file. A judged paraphrase
|
|
764
|
+
// is suppressed: it does NOT join the pool or become a finding, and it
|
|
765
|
+
// is NOT counted toward newCount (so it neither resets nor blocks the
|
|
766
|
+
// dry streak). Its hash key IS recorded, so the identical paraphrase is
|
|
767
|
+
// not re-litigated by a later pass.
|
|
768
|
+
if (!llm.offline) {
|
|
769
|
+
const priorSameCell = priorFixed
|
|
770
|
+
.concat(pool)
|
|
771
|
+
.filter((p) => p.dimension === cand.dimension && p.file === cand.file);
|
|
772
|
+
if (await isSemanticDuplicate(ctx, cand, priorSameCell)) {
|
|
773
|
+
seen.add(key);
|
|
774
|
+
// Declared, never silent (adversarial review finding): a
|
|
775
|
+
// semantic suppression is the one outcome the fail-open doctrine
|
|
776
|
+
// calls "permanently loses a finding nothing downstream can
|
|
777
|
+
// resurrect" — it must be as visible as a killed candidate.
|
|
778
|
+
suppressed += 1;
|
|
779
|
+
log?.substep?.(`suppressed [${dim.id}] "${cand.title}" — judged a semantic duplicate in ${cand.file}`);
|
|
780
|
+
continue;
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
seen.add(key);
|
|
784
|
+
pool.push(cand);
|
|
785
|
+
newCands.push(cand);
|
|
786
|
+
newCount += 1;
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
dryStreak = newCount === 0 ? dryStreak + 1 : 0;
|
|
790
|
+
}
|
|
791
|
+
return { passes: pass, dry: dryStreak >= K_DRY, newCands, suppressed };
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
// ---------------------------------------------------------------------------
|
|
795
|
+
// Phase entry point
|
|
796
|
+
// ---------------------------------------------------------------------------
|
|
797
|
+
export async function run(ctx) {
|
|
798
|
+
const { root, dotdir, llm, log, exec } = ctx;
|
|
799
|
+
const flags = ctx.flags ?? {};
|
|
800
|
+
const now = ctx.now;
|
|
801
|
+
let state = ctx.state;
|
|
802
|
+
try {
|
|
803
|
+
if (!state?.gates?.comprehend?.passed) {
|
|
804
|
+
throw new OpError("Comprehension gate not passed — `do-better audit` runs D1 before D2; resolve the comprehend gate first.");
|
|
805
|
+
}
|
|
806
|
+
const charterArt = readArtifact(dotdir, LAYOUT.charter);
|
|
807
|
+
if (!charterArt) throw new OpError("Missing .dobetter/charter.md — run `do-better charter` first.");
|
|
808
|
+
const headSha = gitHeadSha(root, exec);
|
|
809
|
+
const head7 = headSha.slice(0, 7);
|
|
810
|
+
warnIfDirtyTree(root, exec, log, "D2 identify"); // H10 — declared, never silent
|
|
811
|
+
const offline = llm.offline === true;
|
|
812
|
+
// Progress (H16): D2 is one of the two heaviest phases and previously ran
|
|
813
|
+
// in silence for tens of minutes. Announce the phase; per-cell steps below.
|
|
814
|
+
log?.phase?.("D2", `Identify${offline ? " (offline static pass)" : ""}`);
|
|
815
|
+
|
|
816
|
+
const dims = orderedDimensions(charterArt.meta ?? {});
|
|
817
|
+
const { base: refuteBase, lenses } = parseLenses(loadRef("refute-charter.md", FALLBACK_REFUTE), log);
|
|
818
|
+
const poolMax = charterPoolMax(flags.n);
|
|
819
|
+
const taxonomyDoc = loadRef("taxonomy.md", "");
|
|
820
|
+
// Each dimension's OWN effective search width — charterPoolWidth depends on
|
|
821
|
+
// BOTH poolMax (the shared --n ceiling) AND dim.weight (independently
|
|
822
|
+
// editable per-dimension via charter.md, without touching poolMax, source
|
|
823
|
+
// content, or headSha). Computed upfront since it only depends on
|
|
824
|
+
// dims/lenses/poolMax, all already known here.
|
|
825
|
+
const currentWidthByDimension = Object.fromEntries(
|
|
826
|
+
dims.map((d) => [d.id, Math.min(charterPoolWidth(d.weight, poolMax), lenses.length)]),
|
|
827
|
+
);
|
|
828
|
+
const declaredFiles = deepReadFileList(dotdir, state);
|
|
829
|
+
const slices = loadSlices(root, declaredFiles);
|
|
830
|
+
const examinedFiles = slices.map((s) => s.file);
|
|
831
|
+
const unreadableFiles = declaredFiles.filter((f) => !examinedFiles.includes(f));
|
|
832
|
+
// A file is truncated (H7) if it was capped at read time (content past
|
|
833
|
+
// SLICE_CHARS dropped) OR if its numbered render still overflows a packet
|
|
834
|
+
// (hard-truncated by buildFinderPacket as a singleton). Both are undeclared
|
|
835
|
+
// coverage loss unless surfaced here.
|
|
836
|
+
const truncatedFiles = slices
|
|
837
|
+
.filter((s) => s.truncated || renderSlice(s).length > PACKET_BYTES)
|
|
838
|
+
.map((s) => s.file);
|
|
839
|
+
const dryCellsFingerprint = packetSetFingerprint(slices);
|
|
840
|
+
|
|
841
|
+
// Partition the whole readable set into packets (online only); offline runs
|
|
842
|
+
// a single deterministic static pass and does not consume packets.
|
|
843
|
+
const packets = offline ? [] : partitionSlices(slices);
|
|
844
|
+
|
|
845
|
+
// Starvation is a gate failure, never a silent zero-finding pass: an online
|
|
846
|
+
// run with no readable deep-read files has nothing to examine (F4).
|
|
847
|
+
if (!offline && packets.length === 0) {
|
|
848
|
+
state = addSpend(state, PHASE_ID, llm.drainSpend());
|
|
849
|
+
state = setGate(state, "identify", { passed: false, dryPassesByDimension: {}, packetsByDimension: {}, unverified: 0 });
|
|
850
|
+
state = recordPhase(state, PHASE_ID, { status: "failed", sha: headSha, now: now() });
|
|
851
|
+
const detail =
|
|
852
|
+
`deep-read set is empty or unreadable — no packets to examine online ` +
|
|
853
|
+
`(${declaredFiles.length} declared, ${examinedFiles.length} readable); ` +
|
|
854
|
+
"re-run D1 comprehend so D2 has code to refute, or use --offline for a static-only pass.";
|
|
855
|
+
const err = makeGateError(`Gate failed: identify — ${detail}`, "identify", detail);
|
|
856
|
+
err.state = state;
|
|
857
|
+
throw err;
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
// Persisted per-cell dry state: on a re-run against the SAME headSha
|
|
861
|
+
// (resuming after a BudgetError before the next phase pins a new sha),
|
|
862
|
+
// packets already recorded dry are skipped — zero finder calls reissued. A
|
|
863
|
+
// sha change discards this entirely (full re-examination).
|
|
864
|
+
//
|
|
865
|
+
// Completion guard, keyed on a DEDICATED marker, not phase `status`
|
|
866
|
+
// (adversarial review finding, round 2 — a `status`-keyed guard has its
|
|
867
|
+
// own bug): dry-cell state is only honored when the prior run's
|
|
868
|
+
// `dryCellsComplete` flag is not true — i.e. this is a genuine resume
|
|
869
|
+
// after a BudgetError or a not-dry gate failure, never a run that
|
|
870
|
+
// actually finished. `status` is unsuitable for this because a
|
|
871
|
+
// BudgetError never calls recordPhase — it propagates straight through
|
|
872
|
+
// the outer catch, which only attaches spend, so `status` keeps
|
|
873
|
+
// whatever value it had BEFORE this run started. A guard keyed on
|
|
874
|
+
// `status === "done"` therefore breaks exactly the case it exists to
|
|
875
|
+
// protect: complete a run (status "done") → deliberately re-audit
|
|
876
|
+
// (correctly discards stale dry state) → that re-audit hits a
|
|
877
|
+
// BudgetError mid-loop → `status` is STILL "done" (never touched by the
|
|
878
|
+
// interruption) → the next resume wrongly discards the dry-cell
|
|
879
|
+
// progress THIS interrupted run just persisted, restarting from zero
|
|
880
|
+
// forever under a tight budget. `dryCellsComplete` avoids this: it is
|
|
881
|
+
// explicitly reset to false at the top of every run (below) and is set
|
|
882
|
+
// true ONLY at the actual successful-completion point, so an
|
|
883
|
+
// interrupted run's incremental patches always persist it as false.
|
|
884
|
+
const priorIdentify = state?.phases?.identify ?? {};
|
|
885
|
+
const priorComplete = priorIdentify.dryCellsComplete === true;
|
|
886
|
+
// Honored only when the commit sha AND the content fingerprint match
|
|
887
|
+
// (round-4 finding): these are legitimately whole-cache concerns — any
|
|
888
|
+
// drift invalidates everything, see packetSetFingerprint's comment.
|
|
889
|
+
const priorDryRaw = !priorComplete
|
|
890
|
+
&& priorIdentify.dryCellsSha === headSha
|
|
891
|
+
&& priorIdentify.dryCellsFingerprint === dryCellsFingerprint
|
|
892
|
+
? (priorIdentify.dryCellsByDimension ?? {})
|
|
893
|
+
: {};
|
|
894
|
+
// Per-dimension search-width safety (round-6 finding, extending round 5):
|
|
895
|
+
// round 5 gated on a single global poolMax, reasoning that comparing it
|
|
896
|
+
// once was "equivalent to comparing every dimension's own effective
|
|
897
|
+
// width" — true only when no dimension's WEIGHT changes. charterPoolWidth
|
|
898
|
+
// depends on BOTH poolMax and dim.weight, and weight is independently
|
|
899
|
+
// editable per-dimension via charter.md (a human-gated artifact) without
|
|
900
|
+
// touching poolMax, source content, or headSha. A dimension recorded dry
|
|
901
|
+
// at a narrower effective width (because its weight was lower then) must
|
|
902
|
+
// NOT be trusted once its weight rises — that dimension's OWN recorded
|
|
903
|
+
// dry cells are discarded; unaffected dimensions remain correctly
|
|
904
|
+
// resumed. Per-dimension, not whole-cache, because weight changes are
|
|
905
|
+
// inherently per-dimension and whole-cache invalidation here would
|
|
906
|
+
// needlessly re-examine dimensions nothing changed for.
|
|
907
|
+
const priorWidthByDimension = priorIdentify.dryCellsWidthByDimension ?? {};
|
|
908
|
+
const priorDry = Object.fromEntries(
|
|
909
|
+
dims.map((d) => [
|
|
910
|
+
d.id,
|
|
911
|
+
(priorWidthByDimension[d.id] ?? 1) >= currentWidthByDimension[d.id] ? (priorDryRaw[d.id] ?? []) : [],
|
|
912
|
+
]),
|
|
913
|
+
);
|
|
914
|
+
// Reset for THIS run — and this must clear the PERSISTED dry-cell data
|
|
915
|
+
// too, not just the in-memory `priorDry` computed above (round-3
|
|
916
|
+
// adversarial review finding: clearing only `dryCellsComplete` left a
|
|
917
|
+
// window between here and the first per-dimension patchPhase call,
|
|
918
|
+
// below, where `state.phases.identify.dryCellsByDimension` still held
|
|
919
|
+
// the PRIOR completed run's stale set. A BudgetError firing inside that
|
|
920
|
+
// window — realistic: it can fire on the very first finder call, before
|
|
921
|
+
// any dimension's loop iteration completes — persisted exactly that
|
|
922
|
+
// stale set alongside dryCellsComplete:false, and the next resume
|
|
923
|
+
// honored it as if it were THIS run's own progress, silently skipping
|
|
924
|
+
// cells the re-audit existed to re-examine). Clearing
|
|
925
|
+
// dryCellsByDimension/dryCellsSha/dryCellsFingerprint/
|
|
926
|
+
// dryCellsWidthByDimension here, in the SAME patch, means an early
|
|
927
|
+
// interruption's persisted state truthfully shows "nothing recorded yet
|
|
928
|
+
// for this attempt" rather than resurrecting stale data. When
|
|
929
|
+
// priorComplete is false (a genuine resume) and sha/fingerprint/width all
|
|
930
|
+
// match, this is a no-op in effect — the existing values are either what
|
|
931
|
+
// priorDry already captured (resume case) or already stale (discarded
|
|
932
|
+
// either way by the read above).
|
|
933
|
+
//
|
|
934
|
+
// dryCellsWidthByDimension is seeded from the prior map (not started
|
|
935
|
+
// empty) for the SAME reason dryCellsByDimension is seeded below: a
|
|
936
|
+
// dimension not yet reached by this run's loop must not vanish from the
|
|
937
|
+
// persisted snapshot just because an earlier dimension's own persist
|
|
938
|
+
// fires first.
|
|
939
|
+
const dryCellsWidthByDimension = { ...priorWidthByDimension };
|
|
940
|
+
state = patchPhase(state, PHASE_ID, {
|
|
941
|
+
dryCellsComplete: false, dryCellsByDimension: priorDry, dryCellsSha: headSha, dryCellsFingerprint,
|
|
942
|
+
dryCellsWidthByDimension,
|
|
943
|
+
});
|
|
944
|
+
|
|
945
|
+
const passesByDimension = {};
|
|
946
|
+
const packetsByDimension = {};
|
|
947
|
+
// Seeded with priorDry (not started empty): each per-dimension patchPhase
|
|
948
|
+
// call below persists this WHOLE object, overwriting the persisted key
|
|
949
|
+
// entirely — a dimension not yet reached by the loop would otherwise
|
|
950
|
+
// vanish from the persisted snapshot the moment an EARLIER dimension's
|
|
951
|
+
// own persist fires, even though its resumed data is still valid and
|
|
952
|
+
// sitting correctly in priorDry. Seeding preserves it until (and unless)
|
|
953
|
+
// that dimension's own turn genuinely overwrites its entry.
|
|
954
|
+
const dryCellsByDimension = { ...priorDry };
|
|
955
|
+
const notDry = []; // [{ dim, packet }] — starvation detail names dimension AND packet
|
|
956
|
+
const counts = {};
|
|
957
|
+
// D6 idempotency: seed the dedupe set from already-verified findings so
|
|
958
|
+
// re-runs reconcile against .dobetter/findings/ instead of duplicating
|
|
959
|
+
// finding files and burning new IDs for identical claims. The same findings
|
|
960
|
+
// seed each dimension's prior-conclusions, so a resumed cell picks up from
|
|
961
|
+
// its recorded pool instead of a blank slate.
|
|
962
|
+
const seen = new Set();
|
|
963
|
+
const priorByDim = {};
|
|
964
|
+
for (const prior of readFindings(dotdir)) {
|
|
965
|
+
const file = prior.evidence?.[0]?.file ?? "";
|
|
966
|
+
seen.add(dedupeKey({ dimension: prior.dimension, file, claim: prior.claim ?? prior.title }));
|
|
967
|
+
seen.add(dedupeKey({ dimension: prior.dimension, file, claim: prior.title }));
|
|
968
|
+
// dimension + claim ride along (beyond title/file) so the T3 semantic
|
|
969
|
+
// filter can offer prior verified findings as same-cell comparison
|
|
970
|
+
// entries — a paraphrase of a finding verified in a PREVIOUS run is caught
|
|
971
|
+
// too, not just repeats within this run.
|
|
972
|
+
(priorByDim[prior.dimension] ??= []).push({
|
|
973
|
+
dimension: prior.dimension, title: prior.title, claim: prior.claim ?? prior.title, file,
|
|
974
|
+
// line rides along so the semantic-dedupe judge can tell two distinct
|
|
975
|
+
// same-class defects in one file apart (H6). evidence[0].line is the
|
|
976
|
+
// pinned citation line; null when a prior finding lacks one.
|
|
977
|
+
line: prior.evidence?.[0]?.line ?? null,
|
|
978
|
+
});
|
|
979
|
+
}
|
|
980
|
+
let killed = 0;
|
|
981
|
+
let verifiedCount = 0;
|
|
982
|
+
|
|
983
|
+
// reproduce-or-kill — unverified findings never reach output (D8/F4).
|
|
984
|
+
const verifyAndRecord = async (dimId, candidates) => {
|
|
985
|
+
for (const cand of candidates) {
|
|
986
|
+
const verdict = await verifyCandidate(ctx, cand, head7);
|
|
987
|
+
if (verdict.verified) {
|
|
988
|
+
const next = nextFindingId(state, dimId);
|
|
989
|
+
state = next.state;
|
|
990
|
+
writeFinding(dotdir, {
|
|
991
|
+
id: next.id, dimension: dimId, title: cand.title, claim: cand.claim, severity: cand.severity,
|
|
992
|
+
confidence: cand.confidence, evidence: verdict.evidence, reproduction: verdict.reproduction,
|
|
993
|
+
status: "verified", foundAt: now(), headSha, stale: false,
|
|
994
|
+
});
|
|
995
|
+
verifiedCount += 1;
|
|
996
|
+
counts[dimId].verified += 1;
|
|
997
|
+
} else {
|
|
998
|
+
killed += 1;
|
|
999
|
+
counts[dimId].killed += 1;
|
|
1000
|
+
log?.substep?.(`killed [${dimId}] ${cand.title} — ${verdict.reason}`);
|
|
1001
|
+
}
|
|
1002
|
+
}
|
|
1003
|
+
};
|
|
1004
|
+
|
|
1005
|
+
for (const dim of dims) {
|
|
1006
|
+
counts[dim.id] = { verified: 0, killed: 0, suppressed: 0 };
|
|
1007
|
+
|
|
1008
|
+
if (offline) {
|
|
1009
|
+
const pool = [];
|
|
1010
|
+
for (const raw of staticFinderPass(dim.id, root, slices)) {
|
|
1011
|
+
const cand = validateCandidate(raw, dim.id, log);
|
|
1012
|
+
if (!cand) continue;
|
|
1013
|
+
cand.method = "static";
|
|
1014
|
+
cand.check = raw.check;
|
|
1015
|
+
const key = dedupeKey(cand);
|
|
1016
|
+
if (!seen.has(key)) { seen.add(key); pool.push(cand); }
|
|
1017
|
+
}
|
|
1018
|
+
passesByDimension[dim.id] = 1; // single deterministic pass, dry by construction (declared degradation)
|
|
1019
|
+
packetsByDimension[dim.id] = 0; // offline: no packetization
|
|
1020
|
+
await verifyAndRecord(dim.id, pool);
|
|
1021
|
+
continue;
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
const pool = [];
|
|
1025
|
+
const priorFixed = priorByDim[dim.id] ?? [];
|
|
1026
|
+
const priorDryForDim = priorDry[dim.id] ?? [];
|
|
1027
|
+
// Clamp to the lens catalog size (adversarial review finding): beyond
|
|
1028
|
+
// lenses.length, pool member i and i+lenses.length receive the SAME
|
|
1029
|
+
// lens (rotation wraps) and thus an IDENTICAL prompt — a fully
|
|
1030
|
+
// redundant call that cannot add coverage but still spends budget.
|
|
1031
|
+
// (Same value as currentWidthByDimension[dim.id], computed upfront for
|
|
1032
|
+
// the resume-validity check above — reused here as the single source
|
|
1033
|
+
// of truth rather than recomputed.)
|
|
1034
|
+
const poolWidth = currentWidthByDimension[dim.id];
|
|
1035
|
+
const dryCells = [];
|
|
1036
|
+
let totalPasses = 0;
|
|
1037
|
+
for (let pi = 0; pi < packets.length; pi++) {
|
|
1038
|
+
if (priorDryForDim.includes(pi)) {
|
|
1039
|
+
dryCells.push(pi); // recorded dry on a prior same-sha, same-or-wider-width run — skip, no finder calls
|
|
1040
|
+
continue;
|
|
1041
|
+
}
|
|
1042
|
+
const cell = await finderCell(ctx, {
|
|
1043
|
+
dim, packetText: packets[pi].packet, pool, priorFixed, seen, base: refuteBase, lenses, poolWidth, taxonomyDoc,
|
|
1044
|
+
});
|
|
1045
|
+
totalPasses += cell.passes;
|
|
1046
|
+
counts[dim.id].suppressed += cell.suppressed;
|
|
1047
|
+
if (cell.dry) dryCells.push(pi);
|
|
1048
|
+
else notDry.push({ dim: dim.id, packet: pi });
|
|
1049
|
+
await verifyAndRecord(dim.id, cell.newCands);
|
|
1050
|
+
// Per-cell progress (H16): dimension, packet index, pass count, running
|
|
1051
|
+
// verified/killed, and cumulative spend — all already in scope.
|
|
1052
|
+
log?.step?.(
|
|
1053
|
+
`[${dim.id}] packet ${pi + 1}/${packets.length} · ${cell.passes} pass${cell.passes === 1 ? "" : "es"}` +
|
|
1054
|
+
` · ${counts[dim.id].verified} verified/${counts[dim.id].killed} killed · $${llm.spentSoFar().toFixed(2)} spent`,
|
|
1055
|
+
);
|
|
1056
|
+
// Persist incrementally so a BudgetError mid-loop leaves the dry-cell
|
|
1057
|
+
// state (and verified findings, already on disk) recoverable on re-run.
|
|
1058
|
+
passesByDimension[dim.id] = totalPasses;
|
|
1059
|
+
packetsByDimension[dim.id] = packets.length;
|
|
1060
|
+
dryCellsByDimension[dim.id] = dryCells.slice();
|
|
1061
|
+
dryCellsWidthByDimension[dim.id] = poolWidth;
|
|
1062
|
+
state = patchPhase(state, PHASE_ID, { passesByDimension, packetsByDimension, dryCellsByDimension, dryCellsSha: headSha, dryCellsFingerprint, dryCellsWidthByDimension });
|
|
1063
|
+
}
|
|
1064
|
+
passesByDimension[dim.id] = totalPasses;
|
|
1065
|
+
packetsByDimension[dim.id] = packets.length;
|
|
1066
|
+
dryCellsByDimension[dim.id] = dryCells;
|
|
1067
|
+
dryCellsWidthByDimension[dim.id] = poolWidth;
|
|
1068
|
+
state = patchPhase(state, PHASE_ID, { passesByDimension, packetsByDimension, dryCellsByDimension, dryCellsSha: headSha, dryCellsFingerprint, dryCellsWidthByDimension });
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
const suppressedByDimension = {};
|
|
1072
|
+
for (const dim of dims) suppressedByDimension[dim.id] = counts[dim.id]?.suppressed ?? 0;
|
|
1073
|
+
writeD2Coverage(dotdir, {
|
|
1074
|
+
dims, offline, passesByDimension, packetsByDimension, suppressedByDimension, examinedFiles, truncatedFiles, unreadableFiles,
|
|
1075
|
+
});
|
|
1076
|
+
|
|
1077
|
+
state = patchPhase(state, PHASE_ID, {
|
|
1078
|
+
passesByDimension, packetsByDimension, dryCellsByDimension, dryCellsSha: headSha, dryCellsFingerprint, dryCellsWidthByDimension,
|
|
1079
|
+
killed, verified: verifiedCount,
|
|
1080
|
+
});
|
|
1081
|
+
state = addSpend(state, PHASE_ID, llm.drainSpend());
|
|
1082
|
+
|
|
1083
|
+
if (notDry.length > 0) {
|
|
1084
|
+
state = setGate(state, "identify", { passed: false, dryPassesByDimension: passesByDimension, packetsByDimension, unverified: 0 });
|
|
1085
|
+
state = recordPhase(state, PHASE_ID, { status: "failed", sha: headSha, now: now() });
|
|
1086
|
+
const cellList = notDry.map(({ dim, packet }) => `${dim}[packet ${packet}]`).join(", ");
|
|
1087
|
+
const detail = `cell(s) not dry after ${MAX_PASSES} passes: ${cellList} — finders kept producing new candidates; re-run audit (verified findings so far are preserved in .dobetter/findings/)`;
|
|
1088
|
+
const err = makeGateError(`Gate failed: identify — ${detail}`, "identify", detail);
|
|
1089
|
+
err.state = state;
|
|
1090
|
+
throw err;
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
state = setGate(state, "identify", { passed: true, dryPassesByDimension: passesByDimension, packetsByDimension, unverified: 0 });
|
|
1094
|
+
state = recordPhase(state, PHASE_ID, { status: "done", sha: headSha, now: now() });
|
|
1095
|
+
state = pinSha(state, PHASE_ID, headSha);
|
|
1096
|
+
// Mark dry-cell state complete ONLY here, at genuine successful
|
|
1097
|
+
// completion — this is what the completion guard above checks, not
|
|
1098
|
+
// `status` (see the comment there for why `status` alone is unsafe).
|
|
1099
|
+
state = patchPhase(state, PHASE_ID, { dryCellsComplete: true });
|
|
1100
|
+
|
|
1101
|
+
const perDim = dims
|
|
1102
|
+
.map((d) => `${d.id}: ${counts[d.id].verified} verified / ${counts[d.id].killed} killed / ${counts[d.id].suppressed} suppressed (${passesByDimension[d.id]} pass${passesByDimension[d.id] === 1 ? "" : "es"})`)
|
|
1103
|
+
.join("; ");
|
|
1104
|
+
const summary =
|
|
1105
|
+
`Identify complete @ ${head7}: every (dimension × packet) cell ran dry (K=${K_DRY}); ${verifiedCount} finding(s) verified, ${killed} killed, 0 unverified written. ` +
|
|
1106
|
+
(offline ? "DEGRADED: offline static-analysis pass only — re-run online for LLM finders. " : "") +
|
|
1107
|
+
`Per dimension — ${perDim}. (Finding counts are not a success metric; ticket quality is.)`;
|
|
1108
|
+
return {
|
|
1109
|
+
state,
|
|
1110
|
+
gate: { name: "identify", passed: true, human: false, detail: `all dimensions dry, ${verifiedCount} verified, 0 unverified` },
|
|
1111
|
+
summary,
|
|
1112
|
+
};
|
|
1113
|
+
} catch (err) {
|
|
1114
|
+
if (!err.state) {
|
|
1115
|
+
try { err.state = addSpend(state, PHASE_ID, llm.drainSpend()); } catch { err.state = state; }
|
|
1116
|
+
}
|
|
1117
|
+
throw err;
|
|
1118
|
+
}
|
|
1119
|
+
}
|