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
|
@@ -0,0 +1,553 @@
|
|
|
1
|
+
// src/comprehend.js — D1 Comprehend: coverage plan, 7 artifacts, citations, parallax divergence gate.
|
|
2
|
+
// Contract: blueprint §2.6 + §7 D1. Exports: run(ctx), PHASE_ID, buildCoveragePlan(facts, charter).
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import fs from "node:fs";
|
|
5
|
+
import {
|
|
6
|
+
OpError, gateError, truncate, readPackageFile, gitHeadSha,
|
|
7
|
+
writeFileAtomic, readJsonSafe, isSafeRelPath, mapLimit, warnIfDirtyTree,
|
|
8
|
+
} from "./utils.js";
|
|
9
|
+
import { recordPhase, addSpend, setGate, pinSha } from "./state.js";
|
|
10
|
+
import {
|
|
11
|
+
LAYOUT, ensureLayout, writeArtifact, readArtifact,
|
|
12
|
+
formatCitation, parseCitations, verifyCitations,
|
|
13
|
+
} from "./artifacts.js";
|
|
14
|
+
import { withFallback, cleanJsonResponse } from "./llm.js";
|
|
15
|
+
import { runParallax, runSkillMining } from "./adlc.js";
|
|
16
|
+
|
|
17
|
+
export const PHASE_ID = "comprehend";
|
|
18
|
+
|
|
19
|
+
const DEEP_BYTES_CAP = 150_000;
|
|
20
|
+
const DEEP_FILES_CAP = 40;
|
|
21
|
+
const SCAN_FILES_CAP = 200;
|
|
22
|
+
const PACKET_BYTES = 30_000;
|
|
23
|
+
const SLICE_CHARS = 24_000;
|
|
24
|
+
const BEHAVIOR_KINDS = new Set(["route", "cli", "job", "event", "db-write"]);
|
|
25
|
+
const SECURITY_RX = /auth|crypto|secur|token|session|password|login|server|api|input|sanitiz|valid/i;
|
|
26
|
+
const TEST_RX = /(^|\/)(test|tests|__tests__|spec)(\/|\.|$)|\.(test|spec)\./i;
|
|
27
|
+
const LOWVALUE_RX = /package-lock|pnpm-lock|yarn\.lock|(^|\/)(dist|build|vendor|node_modules)\/|\.min\./;
|
|
28
|
+
const READER_SYSTEM =
|
|
29
|
+
"You are a careful code-comprehension reader. Report only what the provided code shows. " +
|
|
30
|
+
"Every claim MUST carry an inline citation of the form path:line@sha (e.g. src/app.js:42@abc1234). " +
|
|
31
|
+
"Claims without verifiable citations will be deleted downstream. Be concise.";
|
|
32
|
+
|
|
33
|
+
// ---------------------------------------------------------------------------
|
|
34
|
+
// Coverage plan (pure, deterministic)
|
|
35
|
+
// ---------------------------------------------------------------------------
|
|
36
|
+
function normalizeWeights(weights) {
|
|
37
|
+
const w = weights && typeof weights === "object" ? weights : {};
|
|
38
|
+
const out = {};
|
|
39
|
+
for (const [k, v] of Object.entries(w)) {
|
|
40
|
+
const n = Number(v);
|
|
41
|
+
out[k] = Number.isFinite(n) && n >= 1 ? n : 1;
|
|
42
|
+
}
|
|
43
|
+
return out;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function candidatePool(facts) {
|
|
47
|
+
if (Array.isArray(facts?.files) && facts.files.length > 0) {
|
|
48
|
+
return facts.files
|
|
49
|
+
.filter((f) => f && typeof f.file === "string")
|
|
50
|
+
.map((f) => ({ file: f.file, loc: Number(f.loc) || 0, bytes: Number(f.bytes) || (Number(f.loc) || 0) * 40 }));
|
|
51
|
+
}
|
|
52
|
+
if (Array.isArray(facts?.largestFiles)) {
|
|
53
|
+
return facts.largestFiles
|
|
54
|
+
.filter((f) => f && typeof f.file === "string")
|
|
55
|
+
.map((f) => ({ file: f.file, loc: Number(f.loc) || 0, bytes: (Number(f.loc) || 0) * 40 }));
|
|
56
|
+
}
|
|
57
|
+
return [];
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function rankScore(entry, weights) {
|
|
61
|
+
const loc = entry.loc || 0;
|
|
62
|
+
let boost = 1;
|
|
63
|
+
if (SECURITY_RX.test(entry.file)) boost += 0.5 * (weights.security ?? 1);
|
|
64
|
+
if (TEST_RX.test(entry.file)) boost += 0.4 * (weights["test-quality"] ?? 1);
|
|
65
|
+
if ((weights.maintainability ?? 1) >= 3 && loc > 400) boost += 0.3 * (weights.maintainability ?? 1);
|
|
66
|
+
const depth = entry.file.split("/").length;
|
|
67
|
+
const central = depth <= 2 ? 1.2 : 1;
|
|
68
|
+
let score = Math.log2(2 + loc) * boost * central;
|
|
69
|
+
if (LOWVALUE_RX.test(entry.file)) score *= 0.1;
|
|
70
|
+
return score;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function buildCoveragePlan(facts, charter) {
|
|
74
|
+
const weights = normalizeWeights(charter?.weights);
|
|
75
|
+
const pool = candidatePool(facts);
|
|
76
|
+
if (pool.length === 0) {
|
|
77
|
+
return { deepRead: [], scan: [], skipped: [], deepPct: 0, scanPct: 0, skipPct: 0, rationale: "No candidate files found in repo facts." };
|
|
78
|
+
}
|
|
79
|
+
const ranked = pool
|
|
80
|
+
.map((f) => ({ ...f, score: rankScore(f, weights) }))
|
|
81
|
+
.sort((a, b) => b.score - a.score || a.file.localeCompare(b.file));
|
|
82
|
+
const deepRead = [];
|
|
83
|
+
const scan = [];
|
|
84
|
+
const skipped = [];
|
|
85
|
+
let bytes = 0;
|
|
86
|
+
for (const f of ranked) {
|
|
87
|
+
const fb = f.bytes || f.loc * 40 || 400;
|
|
88
|
+
const fitsBytes = deepRead.length === 0 || bytes + fb <= DEEP_BYTES_CAP;
|
|
89
|
+
if (deepRead.length < DEEP_FILES_CAP && fitsBytes && bytes < DEEP_BYTES_CAP) {
|
|
90
|
+
deepRead.push(f.file);
|
|
91
|
+
bytes += fb;
|
|
92
|
+
} else if (scan.length < SCAN_FILES_CAP) {
|
|
93
|
+
scan.push(f.file);
|
|
94
|
+
} else {
|
|
95
|
+
skipped.push(f.file);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
const total = pool.length;
|
|
99
|
+
const pct = (n) => Math.round((n / total) * 100);
|
|
100
|
+
const topDims = Object.entries(weights).sort((a, b) => b[1] - a[1]).slice(0, 3).map(([k, v]) => `${k}=${v}`).join(", ");
|
|
101
|
+
return {
|
|
102
|
+
deepRead, scan, skipped,
|
|
103
|
+
deepPct: pct(deepRead.length), scanPct: pct(scan.length), skipPct: pct(skipped.length),
|
|
104
|
+
rationale:
|
|
105
|
+
`Ranked ${total} files by charter-weighted relevance (top weights: ${topDims || "defaults"}) × size × centrality. ` +
|
|
106
|
+
`Deep-read capped at ${DEEP_FILES_CAP} files / ${DEEP_BYTES_CAP} bytes; next ${SCAN_FILES_CAP} files scanned; remainder skipped (declared, never silent).`,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// ---------------------------------------------------------------------------
|
|
111
|
+
// Local helpers
|
|
112
|
+
// ---------------------------------------------------------------------------
|
|
113
|
+
function patchPhase(state, phase, patch) {
|
|
114
|
+
return { ...state, phases: { ...state.phases, [phase]: { ...state.phases[phase], ...patch } } };
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function makeGateError(_message, gate, detail) {
|
|
118
|
+
return gateError(gate, detail); // H15 — shared, well-formed message
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function enrichFacts(facts, root) {
|
|
122
|
+
if (Array.isArray(facts?.files) && facts.files.length > 0) return facts;
|
|
123
|
+
try {
|
|
124
|
+
const files = [];
|
|
125
|
+
const walk = (rel) => {
|
|
126
|
+
const abs = path.join(root, rel);
|
|
127
|
+
for (const name of fs.readdirSync(abs)) {
|
|
128
|
+
if (name === ".git" || name === "node_modules" || name === ".dobetter") continue;
|
|
129
|
+
const childRel = rel ? `${rel}/${name}` : name;
|
|
130
|
+
const st = fs.statSync(path.join(root, childRel));
|
|
131
|
+
if (st.isDirectory()) walk(childRel);
|
|
132
|
+
else if (st.isFile()) {
|
|
133
|
+
let loc = 0;
|
|
134
|
+
if (st.size <= 1_000_000) {
|
|
135
|
+
try { loc = fs.readFileSync(path.join(root, childRel), "utf8").split("\n").length; } catch { loc = 0; }
|
|
136
|
+
}
|
|
137
|
+
files.push({ file: childRel, loc, bytes: st.size });
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
};
|
|
141
|
+
walk("");
|
|
142
|
+
return { ...facts, files };
|
|
143
|
+
} catch {
|
|
144
|
+
return facts;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function loadSlices(root, files) {
|
|
149
|
+
const slices = [];
|
|
150
|
+
for (const file of files) {
|
|
151
|
+
if (!isSafeRelPath(file)) continue;
|
|
152
|
+
try {
|
|
153
|
+
const raw = fs.readFileSync(path.join(root, file), "utf8");
|
|
154
|
+
const numbered = raw.split("\n").map((l, i) => `${i + 1}: ${l}`).join("\n");
|
|
155
|
+
slices.push({ file, content: truncate(numbered, SLICE_CHARS), raw });
|
|
156
|
+
} catch { /* unreadable file — declared via skipped set */ }
|
|
157
|
+
}
|
|
158
|
+
return slices;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function buildPackets(header, slices) {
|
|
162
|
+
const packets = [];
|
|
163
|
+
let cur = header;
|
|
164
|
+
for (const s of slices) {
|
|
165
|
+
const chunk = `\n\n=== ${s.file} ===\n${s.content}`;
|
|
166
|
+
if (cur.length + chunk.length > PACKET_BYTES && cur !== header) {
|
|
167
|
+
packets.push(cur);
|
|
168
|
+
cur = header;
|
|
169
|
+
}
|
|
170
|
+
cur += chunk;
|
|
171
|
+
}
|
|
172
|
+
packets.push(cur);
|
|
173
|
+
return packets;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
async function jsonCall(llm, args, fallbackFn) {
|
|
177
|
+
// json:true routes withFallback to llm.callJson (object + re-ask retry);
|
|
178
|
+
// jsonMode:true covers implementations that thread it into llm.call instead.
|
|
179
|
+
// Either way a string result is parsed defensively below (fail closed).
|
|
180
|
+
const out = await withFallback(llm, { ...args, json: true, jsonMode: true }, fallbackFn);
|
|
181
|
+
if (typeof out !== "string") return out;
|
|
182
|
+
try {
|
|
183
|
+
return JSON.parse(cleanJsonResponse(out));
|
|
184
|
+
} catch {
|
|
185
|
+
throw new OpError(`[${args.label}] returned unparseable JSON`);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
async function readerText(llm, name, instruction, packets, fallbackFn) {
|
|
190
|
+
const parts = [];
|
|
191
|
+
for (let i = 0; i < packets.length; i++) {
|
|
192
|
+
const prompt =
|
|
193
|
+
`${instruction}\n\nCite every claim inline as path:line@sha. ` +
|
|
194
|
+
`Packet ${i + 1}/${packets.length}:\n\n${packets[i]}`;
|
|
195
|
+
const out = await withFallback(
|
|
196
|
+
llm,
|
|
197
|
+
{ prompt, system: READER_SYSTEM, tier: "mid", label: `reader:${name}` },
|
|
198
|
+
fallbackFn,
|
|
199
|
+
);
|
|
200
|
+
parts.push(typeof out === "string" ? out : String(out));
|
|
201
|
+
if (llm.offline) break; // fallback covers the whole artifact in one shot
|
|
202
|
+
}
|
|
203
|
+
return parts.join("\n\n");
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// ---------------------------------------------------------------------------
|
|
207
|
+
// Offline / deterministic builders
|
|
208
|
+
// ---------------------------------------------------------------------------
|
|
209
|
+
function structureCodemap(facts) {
|
|
210
|
+
const lines = ["# Codemap", "", "Structure-only codemap (offline degradation — purposes not inferred).", ""];
|
|
211
|
+
for (const d of facts.topDirs ?? []) lines.push(`- \`${d.dir}/\` — ${d.files} files, ${d.loc} LOC (structure-only)`);
|
|
212
|
+
for (const f of (facts.largestFiles ?? []).slice(0, 10)) lines.push(`- \`${f.file}\` — ${f.loc} LOC (structure-only)`);
|
|
213
|
+
return lines.join("\n");
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function grepBehaviors(root, files) {
|
|
217
|
+
const routeRx = /\b(?:app|router|server|api)\.(get|post|put|patch|delete|all)\s*\(\s*["'`]([^"'`]+)["'`]/;
|
|
218
|
+
const eventRx = /\.(?:addEventListener|on)\s*\(\s*["'`]([^"'`]+)["'`]/;
|
|
219
|
+
const found = [];
|
|
220
|
+
for (const file of files) {
|
|
221
|
+
if (!isSafeRelPath(file) || !/\.(c|m)?(j|t)sx?$/.test(file)) continue;
|
|
222
|
+
let raw;
|
|
223
|
+
try { raw = fs.readFileSync(path.join(root, file), "utf8"); } catch { continue; }
|
|
224
|
+
const lines = raw.split("\n");
|
|
225
|
+
let cliNoted = false;
|
|
226
|
+
for (let i = 0; i < lines.length && found.length < 200; i++) {
|
|
227
|
+
const route = routeRx.exec(lines[i]);
|
|
228
|
+
if (route) { found.push({ kind: "route", surface: `${route[1].toUpperCase()} ${route[2]}`, file, line: i + 1, summary: "route handler (structure-only)" }); continue; }
|
|
229
|
+
const ev = eventRx.exec(lines[i]);
|
|
230
|
+
if (ev) { found.push({ kind: "event", surface: ev[1], file, line: i + 1, summary: "event listener (structure-only)" }); continue; }
|
|
231
|
+
if (!cliNoted && /process\.argv/.test(lines[i])) { cliNoted = true; found.push({ kind: "cli", surface: path.basename(file), file, line: i + 1, summary: "CLI argv entry (structure-only)" }); }
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
try {
|
|
235
|
+
const raw = fs.readFileSync(path.join(root, "package.json"), "utf8");
|
|
236
|
+
const pkg = JSON.parse(raw);
|
|
237
|
+
if (pkg.bin) {
|
|
238
|
+
const line = raw.split("\n").findIndex((l) => l.includes('"bin"')) + 1 || 1;
|
|
239
|
+
const names = typeof pkg.bin === "string" ? [pkg.name ?? "cli"] : Object.keys(pkg.bin);
|
|
240
|
+
for (const name of names) found.push({ kind: "cli", surface: name, file: "package.json", line, summary: "declared bin entry (structure-only)" });
|
|
241
|
+
}
|
|
242
|
+
} catch { /* no package.json — fine */ }
|
|
243
|
+
return found;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function depsSection(root, facts) {
|
|
247
|
+
const lines = ["## Declared dependencies", ""];
|
|
248
|
+
let pkg = null;
|
|
249
|
+
try { pkg = readJsonSafe(path.join(root, "package.json")); } catch { pkg = null; }
|
|
250
|
+
if (pkg) {
|
|
251
|
+
for (const [name, ver] of Object.entries(pkg.dependencies ?? {})) lines.push(`- ${name}@${ver} (prod)`);
|
|
252
|
+
for (const [name, ver] of Object.entries(pkg.devDependencies ?? {})) lines.push(`- ${name}@${ver} (dev)`);
|
|
253
|
+
if (lines.length === 2) lines.push("- (none declared)");
|
|
254
|
+
} else {
|
|
255
|
+
lines.push("- (no parseable package.json)");
|
|
256
|
+
}
|
|
257
|
+
if (Array.isArray(facts.manifests) && facts.manifests.length) {
|
|
258
|
+
lines.push("", "## Manifests", "", ...facts.manifests.map((m) => `- ${m}`));
|
|
259
|
+
}
|
|
260
|
+
return lines.join("\n");
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function behaviorsToBody(entries, head7) {
|
|
264
|
+
const lines = [
|
|
265
|
+
"# Behavior Inventory",
|
|
266
|
+
"",
|
|
267
|
+
"KEYSTONE artifact — the denominator for \"retain existing functionality\".",
|
|
268
|
+
"One bullet per observable behavior: `- **B-NNN** [kind] surface — summary (file:line@sha)`.",
|
|
269
|
+
"",
|
|
270
|
+
];
|
|
271
|
+
entries.forEach((b, i) => {
|
|
272
|
+
const id = `B-${String(i + 1).padStart(3, "0")}`;
|
|
273
|
+
lines.push(`- **${id}** [${b.kind}] ${b.surface} — ${b.summary || "observed behavior"} (${formatCitation({ file: b.file, line: b.line, sha: head7 })})`);
|
|
274
|
+
});
|
|
275
|
+
if (entries.length === 0) lines.push("- (no observable behaviors identified)");
|
|
276
|
+
return lines.join("\n");
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function validateBehavior(b) {
|
|
280
|
+
if (!b || typeof b !== "object") return null;
|
|
281
|
+
const kind = String(b.kind ?? "").toLowerCase();
|
|
282
|
+
const line = Number(b.line);
|
|
283
|
+
if (!BEHAVIOR_KINDS.has(kind)) return null;
|
|
284
|
+
if (typeof b.file !== "string" || !isSafeRelPath(b.file)) return null;
|
|
285
|
+
if (!Number.isInteger(line) || line < 1) return null;
|
|
286
|
+
if (typeof b.surface !== "string" || b.surface.trim() === "") return null;
|
|
287
|
+
return { kind, surface: b.surface.trim(), file: b.file, line, summary: typeof b.summary === "string" ? b.summary.trim() : "" };
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// ---------------------------------------------------------------------------
|
|
291
|
+
// Citation gate (F4, deterministic)
|
|
292
|
+
// ---------------------------------------------------------------------------
|
|
293
|
+
function scrubCitations(root, body, exec, head7, log) {
|
|
294
|
+
const all = parseCitations(body);
|
|
295
|
+
if (all.length === 0) return { body, dropped: 0 };
|
|
296
|
+
const { verified } = verifyCitations(root, all, exec);
|
|
297
|
+
const okSet = new Set(verified.map((c) => `${c.file}:${c.line}`));
|
|
298
|
+
const kept = [];
|
|
299
|
+
let dropped = 0;
|
|
300
|
+
for (const line of body.split("\n")) {
|
|
301
|
+
const cits = parseCitations(line);
|
|
302
|
+
if (cits.length === 0) { kept.push(line); continue; }
|
|
303
|
+
const good = cits.filter((c) => okSet.has(`${c.file}:${c.line}`));
|
|
304
|
+
if (good.length === 0) {
|
|
305
|
+
dropped += 1;
|
|
306
|
+
log?.warn?.(`citation gate: dropped uncited claim: ${truncate(line.trim(), 120)}`);
|
|
307
|
+
continue;
|
|
308
|
+
}
|
|
309
|
+
let out = line;
|
|
310
|
+
for (const c of cits) {
|
|
311
|
+
const token = formatCitation(c);
|
|
312
|
+
const replacement = okSet.has(`${c.file}:${c.line}`) ? formatCitation({ ...c, sha: head7 }) : "(citation removed: unverifiable)";
|
|
313
|
+
out = out.split(token).join(replacement);
|
|
314
|
+
}
|
|
315
|
+
kept.push(out);
|
|
316
|
+
}
|
|
317
|
+
return { body: kept.join("\n"), dropped };
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function writeCoverageManifest(dotdir, { headSha, generatedAt, plan, degradations }) {
|
|
321
|
+
const body = [
|
|
322
|
+
"# Coverage Manifest", "",
|
|
323
|
+
"Declared sampling — never silent (D10).", "",
|
|
324
|
+
"## Coverage",
|
|
325
|
+
`- deep-read: ${plan.deepPct}% (${plan.deepRead.length} files)`,
|
|
326
|
+
`- scanned: ${plan.scanPct}% (${plan.scan.length} files)`,
|
|
327
|
+
`- skipped: ${plan.skipPct}% (${plan.skipped.length} files)`, "",
|
|
328
|
+
"## Rationale", plan.rationale, "",
|
|
329
|
+
"## Deep-read files", ...(plan.deepRead.length ? plan.deepRead.map((f) => `- ${f}`) : ["- (none)"]), "",
|
|
330
|
+
"## Scanned files", ...(plan.scan.length ? plan.scan.map((f) => `- ${f}`) : ["- (none)"]), "",
|
|
331
|
+
"## Skipped files", ...(plan.skipped.length ? plan.skipped.map((f) => `- ${f}`) : ["- (none)"]), "",
|
|
332
|
+
"## Degradations",
|
|
333
|
+
...(degradations.length ? degradations.map((d) => `- ${d}`) : ["- (none)"]),
|
|
334
|
+
].join("\n") + "\n";
|
|
335
|
+
writeArtifact(dotdir, LAYOUT.comprehension.coverageManifest, {
|
|
336
|
+
meta: { headSha, generatedAt, deepPct: plan.deepPct, scanPct: plan.scanPct, skipPct: plan.skipPct },
|
|
337
|
+
body,
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// ---------------------------------------------------------------------------
|
|
342
|
+
// Phase entry point
|
|
343
|
+
// ---------------------------------------------------------------------------
|
|
344
|
+
export async function run(ctx) {
|
|
345
|
+
const { root, dotdir, llm, adlc, flags, log, exec } = ctx;
|
|
346
|
+
const now = ctx.now;
|
|
347
|
+
let state = ctx.state;
|
|
348
|
+
try {
|
|
349
|
+
if (!state?.gates?.charter?.approved) {
|
|
350
|
+
throw new OpError("Charter not approved — run `do-better charter` and approve it (or `do-better charter --approve`) before `do-better audit`.");
|
|
351
|
+
}
|
|
352
|
+
const scanFacts = state?.phases?.scan?.facts;
|
|
353
|
+
if (!scanFacts) throw new OpError("No scan facts found — run `do-better scan` first.");
|
|
354
|
+
const charterArt = readArtifact(dotdir, LAYOUT.charter);
|
|
355
|
+
if (!charterArt) throw new OpError("Missing .dobetter/charter.md — run `do-better charter` first.");
|
|
356
|
+
const charter = {
|
|
357
|
+
weights: normalizeWeights(charterArt.meta?.weights),
|
|
358
|
+
intent: charterArt.meta?.intent ?? null,
|
|
359
|
+
body: charterArt.body ?? "",
|
|
360
|
+
};
|
|
361
|
+
|
|
362
|
+
const headSha = gitHeadSha(root, exec);
|
|
363
|
+
const head7 = headSha.slice(0, 7);
|
|
364
|
+
warnIfDirtyTree(root, exec, log, "D1 comprehend"); // H10 — declared, never silent
|
|
365
|
+
ensureLayout(dotdir);
|
|
366
|
+
|
|
367
|
+
const offline = llm.offline === true;
|
|
368
|
+
// Progress (H16): D1 is the other heavy phase that previously ran silent.
|
|
369
|
+
log?.phase?.("D1", `Comprehend${offline ? " (offline structure-only)" : ""}`);
|
|
370
|
+
const parallaxAvailable = !offline && adlc?.available?.parallax === true;
|
|
371
|
+
const readings = parallaxAvailable ? (flags.n ?? 3) : 1;
|
|
372
|
+
const threshold = flags.threshold ?? 0.25;
|
|
373
|
+
const degradations = [];
|
|
374
|
+
|
|
375
|
+
// 1) Coverage plan
|
|
376
|
+
const facts = enrichFacts(scanFacts, root);
|
|
377
|
+
const plan = buildCoveragePlan(facts, charter);
|
|
378
|
+
writeCoverageManifest(dotdir, { headSha, generatedAt: now(), plan, degradations });
|
|
379
|
+
const slices = loadSlices(root, plan.deepRead);
|
|
380
|
+
|
|
381
|
+
// 2) Packets seeded with charter + scan-time codemap draft
|
|
382
|
+
const draft = readArtifact(dotdir, LAYOUT.comprehension.codemap);
|
|
383
|
+
const header =
|
|
384
|
+
`Charter intent: ${charter.intent ?? "(unspecified)"}\n` +
|
|
385
|
+
`Dimension weights: ${JSON.stringify(charter.weights)}\n\n` +
|
|
386
|
+
`Charter excerpt:\n${truncate(charter.body, 2000)}\n\n` +
|
|
387
|
+
`Codemap draft:\n${truncate(draft?.body ?? "(no draft)", 4000)}`;
|
|
388
|
+
const packets = buildPackets(header, slices);
|
|
389
|
+
const baseMeta = { headSha, generatedAt: now(), readings };
|
|
390
|
+
|
|
391
|
+
// 3) Behaviors first (keystone) — JSON-validated entries
|
|
392
|
+
let behaviors = [];
|
|
393
|
+
if (offline) {
|
|
394
|
+
behaviors = grepBehaviors(root, plan.deepRead.concat(plan.scan));
|
|
395
|
+
} else {
|
|
396
|
+
// The per-packet behavior calls are independent (results merged and
|
|
397
|
+
// deduped afterward), so run them concurrently (H8). Validated entries are
|
|
398
|
+
// returned per packet and merged in PACKET ORDER below, so the inventory
|
|
399
|
+
// is byte-identical to the previous sequential loop — concurrency is
|
|
400
|
+
// wall-clock only.
|
|
401
|
+
const perPacket = await mapLimit(packets, packets.length, async (packet, i) => {
|
|
402
|
+
log?.step?.(`Behavior inventory · packet ${i + 1}/${packets.length} · $${llm.spentSoFar().toFixed(2)} spent`);
|
|
403
|
+
const obj = await jsonCall(
|
|
404
|
+
llm,
|
|
405
|
+
{
|
|
406
|
+
prompt:
|
|
407
|
+
"Enumerate every OBSERVABLE behavior in this code (routes, CLI entrypoints, jobs, events, db-writes). " +
|
|
408
|
+
'Return JSON {"behaviors":[{"kind":"route|cli|job|event|db-write","surface":"GET /x","file":"src/a.js","line":12,"summary":"..."}]}\n\n' + packet,
|
|
409
|
+
system: READER_SYSTEM, tier: "mid", label: "reader:behavior-inventory",
|
|
410
|
+
},
|
|
411
|
+
() => ({ behaviors: grepBehaviors(root, plan.deepRead) }),
|
|
412
|
+
);
|
|
413
|
+
const arr = Array.isArray(obj) ? obj : Array.isArray(obj?.behaviors) ? obj.behaviors : [];
|
|
414
|
+
const out = [];
|
|
415
|
+
for (const raw of arr) {
|
|
416
|
+
const b = validateBehavior(raw);
|
|
417
|
+
if (b) out.push(b);
|
|
418
|
+
else log?.warn?.("behavior-inventory: dropped invalid entry (fail closed)");
|
|
419
|
+
}
|
|
420
|
+
return out;
|
|
421
|
+
});
|
|
422
|
+
for (const arr of perPacket) behaviors.push(...arr);
|
|
423
|
+
}
|
|
424
|
+
const seen = new Set();
|
|
425
|
+
behaviors = behaviors.filter((b) => {
|
|
426
|
+
const k = `${b.kind}|${b.surface}|${b.file}:${b.line}`;
|
|
427
|
+
if (seen.has(k)) return false;
|
|
428
|
+
seen.add(k);
|
|
429
|
+
return true;
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
// 4) Remaining readers
|
|
433
|
+
log?.step?.(`Reader artifacts (codemap, architecture, dependencies, rails-map, glossary) · $${llm.spentSoFar().toFixed(2)} spent`);
|
|
434
|
+
const offlineNote = (title, extra = "") =>
|
|
435
|
+
`# ${title}\n\n(structure-only) Offline degradation — no LLM narrative.\n${extra}`;
|
|
436
|
+
// The five reader artifacts have no data dependency on each other (rails-map
|
|
437
|
+
// reads `behaviors`, already fully computed above), so run them concurrently
|
|
438
|
+
// (H8) with bounded concurrency. Each reader's result is assigned to its own
|
|
439
|
+
// named slot, so completion order is irrelevant — output is byte-identical
|
|
440
|
+
// to the previous sequential assignment.
|
|
441
|
+
const readerDefs = [
|
|
442
|
+
{ key: "codemap", run: () => readerText(llm, "codemap",
|
|
443
|
+
"Verify and extend this codemap draft: one line of purpose per top-level directory and major file.",
|
|
444
|
+
packets, () => structureCodemap(facts)) },
|
|
445
|
+
{ key: "architecture", run: () => readerText(llm, "architecture",
|
|
446
|
+
"Describe the intended design versus the actual implementation drift you can evidence in the code.",
|
|
447
|
+
packets, () => offlineNote("Architecture", (facts.topDirs ?? []).map((d) => `- \`${d.dir}/\``).join("\n"))) },
|
|
448
|
+
{ key: "dependencies", run: () => (offline
|
|
449
|
+
? Promise.resolve("## Risk flags\n\n(structure-only) EOL/CVE flags require online verification.")
|
|
450
|
+
: readerText(llm, "dependencies",
|
|
451
|
+
"Flag dependency risks (EOL, known-CVE-prone, coupling hotspots). Mark every external-knowledge flag as \"needs verification\".",
|
|
452
|
+
[packets[0]], () => "## Risk flags\n\n(structure-only)")) },
|
|
453
|
+
{ key: "railsMap", run: () => readerText(llm, "rails-map",
|
|
454
|
+
`Map these observable behaviors to existing tests; mark each covered or load-bearing-but-untested.\nBehaviors:\n${behaviorsToBody(behaviors, head7)}`,
|
|
455
|
+
[packets[0]], () => "# Rails Map\n\n(structure-only) Existing tests:\n" + (facts.testDirs ?? []).map((d) => `- \`${d}/\``).join("\n") + "\n\nBehavior coverage unknown offline.") },
|
|
456
|
+
{ key: "glossary", run: () => readerText(llm, "glossary",
|
|
457
|
+
"Build a glossary mapping business/domain terms to code terms, citing where each term is defined or used.",
|
|
458
|
+
[packets[0]], () => "# Glossary\n\n(structure-only) No glossary terms extracted offline.") },
|
|
459
|
+
];
|
|
460
|
+
const readerResults = await mapLimit(readerDefs, 4, (d) => d.run());
|
|
461
|
+
const bodies = {};
|
|
462
|
+
readerDefs.forEach((d, i) => { bodies[d.key] = readerResults[i]; });
|
|
463
|
+
bodies.dependencies = depsSection(root, facts) + "\n\n" + bodies.dependencies;
|
|
464
|
+
|
|
465
|
+
// 5) Citation gate + write artifacts
|
|
466
|
+
const writeScrubbed = (relPath, body, meta) => {
|
|
467
|
+
const { body: clean, dropped } = scrubCitations(root, body, exec, head7, log);
|
|
468
|
+
writeArtifact(dotdir, relPath, { meta, body: clean });
|
|
469
|
+
return dropped;
|
|
470
|
+
};
|
|
471
|
+
let droppedClaims = 0;
|
|
472
|
+
droppedClaims += writeScrubbed(LAYOUT.comprehension.codemap, bodies.codemap, { ...baseMeta, draft: false, generatedBy: "comprehend" });
|
|
473
|
+
const behaviorBody = behaviorsToBody(behaviors, head7);
|
|
474
|
+
const { body: behaviorClean, dropped: behaviorDropped } = scrubCitations(root, behaviorBody, exec, head7, log);
|
|
475
|
+
if (behaviorDropped > 0) log?.warn?.(`behavior-inventory: ${behaviorDropped} entries dropped (zero verified citations)`);
|
|
476
|
+
droppedClaims += behaviorDropped;
|
|
477
|
+
writeArtifact(dotdir, LAYOUT.comprehension.behaviorInventory, { meta: baseMeta, body: behaviorClean });
|
|
478
|
+
droppedClaims += writeScrubbed(LAYOUT.comprehension.dependencies, bodies.dependencies, baseMeta);
|
|
479
|
+
droppedClaims += writeScrubbed(LAYOUT.comprehension.railsMap, bodies.railsMap, baseMeta);
|
|
480
|
+
droppedClaims += writeScrubbed(LAYOUT.comprehension.glossary, bodies.glossary, baseMeta);
|
|
481
|
+
let architectureBody = scrubCitations(root, bodies.architecture, exec, head7, log).body;
|
|
482
|
+
writeArtifact(dotdir, LAYOUT.comprehension.architecture, { meta: baseMeta, body: architectureBody });
|
|
483
|
+
|
|
484
|
+
// 6) skill-mining sub-step (declared, never silent)
|
|
485
|
+
const sm = runSkillMining(adlc, { targetDir: root, offline, exec });
|
|
486
|
+
if (sm.skipped) degradations.push(`mined skills: skipped (${sm.reason ?? "skill-mining unavailable"})`);
|
|
487
|
+
else if (!sm.ok) degradations.push("mined skills: failed (skill-mining exited non-zero)");
|
|
488
|
+
|
|
489
|
+
// 7) Divergence gate (parallax)
|
|
490
|
+
let divergence = null;
|
|
491
|
+
let degraded = null;
|
|
492
|
+
let gateDetail = "";
|
|
493
|
+
if (!parallaxAvailable) {
|
|
494
|
+
degraded = "single-reading";
|
|
495
|
+
degradations.push(offline
|
|
496
|
+
? "parallax: skipped (--offline) — single-reading mode; human skim required before proceeding"
|
|
497
|
+
: "parallax: unavailable — single-reading mode; human skim required before proceeding");
|
|
498
|
+
gateDetail = "degraded single-reading mode — HUMAN SKIM REQUIRED before proceeding";
|
|
499
|
+
log?.warn?.("Parallax unavailable — single-reading mode. Human skim required before proceeding.");
|
|
500
|
+
} else {
|
|
501
|
+
const packetPath = path.join(dotdir, "tmp", "reading-packet.md");
|
|
502
|
+
writeFileAtomic(packetPath,
|
|
503
|
+
`# Reading packet\n\n## Charter\n${truncate(charter.body, 3000)}\n\n## Behavior inventory\n${behaviorClean}\n\n## Architecture\n${architectureBody}\n`);
|
|
504
|
+
const res = runParallax(adlc, { file: packetPath, n: readings, threshold, cwd: root, exec });
|
|
505
|
+
if (res.skipped) {
|
|
506
|
+
degraded = "single-reading";
|
|
507
|
+
degradations.push(`parallax: ${res.reason ?? "could not run"} — single-reading mode; human skim required before proceeding`);
|
|
508
|
+
gateDetail = "degraded single-reading mode — HUMAN SKIM REQUIRED before proceeding";
|
|
509
|
+
} else {
|
|
510
|
+
divergence = typeof res.score === "number" ? res.score : null;
|
|
511
|
+
const divs = Array.isArray(res.divergences) ? res.divergences : [];
|
|
512
|
+
if (divs.length > 0) {
|
|
513
|
+
architectureBody += "\n\n## Open questions\n" +
|
|
514
|
+
divs.map((d) => `- ${typeof d === "string" ? d : JSON.stringify(d)} (divergent reading — D2 confusion-finding seed)`).join("\n");
|
|
515
|
+
writeArtifact(dotdir, LAYOUT.comprehension.architecture, { meta: baseMeta, body: architectureBody });
|
|
516
|
+
}
|
|
517
|
+
if (!res.gate) {
|
|
518
|
+
writeCoverageManifest(dotdir, { headSha, generatedAt: now(), plan, degradations });
|
|
519
|
+
state = patchPhase(state, PHASE_ID, { divergence, readings });
|
|
520
|
+
state = setGate(state, "comprehend", { passed: false, divergence, threshold, degraded: null });
|
|
521
|
+
state = addSpend(state, PHASE_ID, llm.drainSpend());
|
|
522
|
+
state = recordPhase(state, PHASE_ID, { status: "failed", sha: headSha, now: now() });
|
|
523
|
+
const detail = `divergence ${divergence ?? "unknown"} ≥ ${threshold} — re-run audit after resolving the open questions appended to comprehension/architecture.md`;
|
|
524
|
+
const err = makeGateError(`Gate failed: comprehend — ${detail}`, "comprehend", detail);
|
|
525
|
+
err.state = state;
|
|
526
|
+
throw err;
|
|
527
|
+
}
|
|
528
|
+
gateDetail = `divergence ${divergence ?? 0} < ${threshold}`;
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
// 8) Persist gate + state
|
|
533
|
+
writeCoverageManifest(dotdir, { headSha, generatedAt: now(), plan, degradations });
|
|
534
|
+
state = patchPhase(state, PHASE_ID, { divergence, readings });
|
|
535
|
+
state = setGate(state, "comprehend", { passed: true, divergence, threshold, degraded });
|
|
536
|
+
state = addSpend(state, PHASE_ID, llm.drainSpend());
|
|
537
|
+
state = recordPhase(state, PHASE_ID, { status: "done", sha: headSha, now: now() });
|
|
538
|
+
state = pinSha(state, PHASE_ID, headSha);
|
|
539
|
+
|
|
540
|
+
const summary =
|
|
541
|
+
`Comprehension complete @ ${head7}: ${behaviors.length} behaviors inventoried, ` +
|
|
542
|
+
`coverage deep ${plan.deepPct}% / scanned ${plan.scanPct}% / skipped ${plan.skipPct}% (declared in coverage-manifest.md), ` +
|
|
543
|
+
`${droppedClaims} unverifiable claim(s) removed by the citation gate, ` +
|
|
544
|
+
`${degradations.length} degradation(s) declared. Gate: ${gateDetail}.` +
|
|
545
|
+
(degraded ? " HUMAN SKIM REQUIRED: review .dobetter/comprehension/ before `do-better audit` continues to D2." : "");
|
|
546
|
+
return { state, gate: { name: "comprehend", passed: true, human: false, detail: gateDetail }, summary };
|
|
547
|
+
} catch (err) {
|
|
548
|
+
if (!err.state) {
|
|
549
|
+
try { err.state = addSpend(state, PHASE_ID, llm.drainSpend()); } catch { err.state = state; }
|
|
550
|
+
}
|
|
551
|
+
throw err;
|
|
552
|
+
}
|
|
553
|
+
}
|