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/refresh.js
ADDED
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
// src/refresh.js — idempotent re-run (D6/D9/D10): diff vs pinned SHA, touch only
|
|
2
|
+
// changed files, flag stale claims (skill-rot doctrine), re-verify findings,
|
|
3
|
+
// behavior-diff regression hook, mark roadmap items done/regressed.
|
|
4
|
+
import fs from "node:fs";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import { OpError, gateError, git, gitHeadSha, sha256Hex } from "./utils.js";
|
|
7
|
+
import { recordPhase, addSpend, pinSha, recordRoadmapHash } from "./state.js";
|
|
8
|
+
import {
|
|
9
|
+
LAYOUT, readArtifact, writeArtifact, readFindings, annotateStale, verifyCitations,
|
|
10
|
+
} from "./artifacts.js";
|
|
11
|
+
import { withFallback } from "./llm.js";
|
|
12
|
+
import { runBehaviorDiff } from "./adlc.js";
|
|
13
|
+
import { rerunReproduction, markRoadmapResolved, markRoadmapRegressed } from "./roadmap.js";
|
|
14
|
+
|
|
15
|
+
export const PHASE_ID = "refresh";
|
|
16
|
+
const BEHAVIOR_CONFIG = "tmp/behavior.json";
|
|
17
|
+
const BEHAVIOR_BEFORE = "tmp/behavior-before.json";
|
|
18
|
+
const BEHAVIOR_AFTER = "tmp/behavior-after.json";
|
|
19
|
+
const NO_DIFF_NOTE = "behavior-diff unavailable — boundary regressions not checked";
|
|
20
|
+
|
|
21
|
+
export function changedFilesSince(root, pinnedSha, exec) {
|
|
22
|
+
const diff = git(root, ["diff", "--name-only", `${pinnedSha}..HEAD`], exec);
|
|
23
|
+
const untracked = git(root, ["ls-files", "--others", "--exclude-standard"], exec);
|
|
24
|
+
const all = [...diff.split("\n"), ...untracked.split("\n")].map((s) => s.trim()).filter(Boolean);
|
|
25
|
+
return [...new Set(all)].sort();
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function patchPhase(state, patch) {
|
|
29
|
+
return { ...state, phases: { ...state.phases, [PHASE_ID]: { ...state.phases[PHASE_ID], ...patch } } };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function finishState(state, ctx, { headSha, status, changedFiles, staleClaims }) {
|
|
33
|
+
let next = addSpend(state, PHASE_ID, ctx.llm.drainSpend());
|
|
34
|
+
next = recordPhase(next, PHASE_ID, { status, sha: headSha, now: ctx.now() });
|
|
35
|
+
next = patchPhase(next, { changedFiles, staleClaims });
|
|
36
|
+
if (status === "done") next = pinSha(next, PHASE_ID, headSha);
|
|
37
|
+
return next;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function gateFail(state, gate, detail) {
|
|
41
|
+
return gateError(gate, detail, state); // H15 — shared, well-formed message
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function coerceJson(raw) {
|
|
45
|
+
if (raw === null || raw === undefined) return null;
|
|
46
|
+
if (typeof raw === "object") return raw;
|
|
47
|
+
try { return JSON.parse(String(raw).replace(/^```[a-zA-Z]*\n?/, "").replace(/```\s*$/, "").trim()); }
|
|
48
|
+
catch { return null; }
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const STALE_SCOPE = (file) => !file.startsWith(".dobetter/");
|
|
52
|
+
|
|
53
|
+
function repinText(text, changedSet, sha7) {
|
|
54
|
+
return String(text ?? "").replace(
|
|
55
|
+
/([A-Za-z0-9_][A-Za-z0-9_.\/-]*):(\d+)@([0-9a-f]{7,40})/g,
|
|
56
|
+
(full, file, line) => (changedSet.has(file) ? `${file}:${line}@${sha7}` : full),
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// 3. stale-claim flagging across comprehension artifacts (never trust, always flag)
|
|
61
|
+
function flagStaleArtifacts(ctx, changedSet, pinnedSha) {
|
|
62
|
+
let staleCount = 0;
|
|
63
|
+
for (const rel of Object.values(LAYOUT.comprehension)) {
|
|
64
|
+
const art = readArtifact(ctx.dotdir, rel);
|
|
65
|
+
if (!art) continue;
|
|
66
|
+
const res = annotateStale(art.body, { changedFiles: [...changedSet], asOfSha: pinnedSha, now: ctx.now() });
|
|
67
|
+
if (res.staleCount > 0) {
|
|
68
|
+
writeArtifact(ctx.dotdir, rel, { meta: art.meta, body: res.body });
|
|
69
|
+
staleCount += res.staleCount;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return staleCount;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// 4. targeted re-scan: cheap tier, changed files only (D10)
|
|
76
|
+
async function refreshCodemap(ctx, changed, pinnedSha) {
|
|
77
|
+
const art = readArtifact(ctx.dotdir, LAYOUT.comprehension.codemap);
|
|
78
|
+
if (!art) return;
|
|
79
|
+
const note = await withFallback(ctx.llm, {
|
|
80
|
+
prompt: [
|
|
81
|
+
"These files changed since the pinned comprehension SHA. For each, give a one-line",
|
|
82
|
+
"purpose update for the codemap. Return plain markdown bullets only.",
|
|
83
|
+
`Pinned: ${pinnedSha}`,
|
|
84
|
+
...changed.map((f) => `- ${f}`),
|
|
85
|
+
].join("\n"),
|
|
86
|
+
tier: "cheap",
|
|
87
|
+
label: "codemap",
|
|
88
|
+
}, () => changed.map((f) => `- ${f} (structure-only)`).join("\n"));
|
|
89
|
+
const marker = "## Refresh notes";
|
|
90
|
+
const section = `${marker}\n\nChanged since \`${String(pinnedSha).slice(0, 7)}\` @ ${ctx.now()}:\n\n${String(note).trim()}\n`;
|
|
91
|
+
const base = art.body.includes(marker)
|
|
92
|
+
? art.body.slice(0, art.body.indexOf(marker)).replace(/\n+$/, "\n")
|
|
93
|
+
: `${art.body.replace(/\n+$/, "")}\n`;
|
|
94
|
+
writeArtifact(ctx.dotdir, LAYOUT.comprehension.codemap, { meta: art.meta, body: `${base}\n${section}` });
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// 4b. behavior-inventory entries citing changed files: re-verify citations
|
|
98
|
+
function reverifyInventory(ctx, changedSet) {
|
|
99
|
+
const art = readArtifact(ctx.dotdir, LAYOUT.comprehension.behaviorInventory);
|
|
100
|
+
if (!art) return 0;
|
|
101
|
+
let broken = 0;
|
|
102
|
+
for (const line of art.body.split("\n")) {
|
|
103
|
+
const cites = (line.match(/[A-Za-z0-9_][A-Za-z0-9_.\/-]*:\d+@[0-9a-f]{7,40}/g) ?? [])
|
|
104
|
+
.map((s) => { const m = s.match(/^(.*):(\d+)@([0-9a-f]+)$/); return { file: m[1], line: Number(m[2]), sha: m[3] }; })
|
|
105
|
+
.filter((c) => changedSet.has(c.file));
|
|
106
|
+
if (!cites.length) continue;
|
|
107
|
+
broken += verifyCitations(ctx.root, cites, ctx.exec).failed.length;
|
|
108
|
+
}
|
|
109
|
+
if (broken) ctx.log.warn(`${broken} behavior-inventory citation(s) no longer verify — entries flagged stale.`);
|
|
110
|
+
return broken;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// 5. re-verify stale findings (reproduce again, or kill the claim's staleness)
|
|
114
|
+
async function reverifyFindings(ctx, changedSet, headSha, pinned) {
|
|
115
|
+
const sha7 = headSha.slice(0, 7);
|
|
116
|
+
const resolvedIds = [];
|
|
117
|
+
let staleFlagged = 0;
|
|
118
|
+
for (const finding of readFindings(ctx.dotdir)) {
|
|
119
|
+
const cited = (finding.evidence ?? []).some((c) => changedSet.has(c.file));
|
|
120
|
+
if (!cited) continue;
|
|
121
|
+
staleFlagged += 1;
|
|
122
|
+
const rel = `${LAYOUT.findingsDir}/${finding.id}.md`;
|
|
123
|
+
const art = readArtifact(ctx.dotdir, rel);
|
|
124
|
+
if (!art) continue;
|
|
125
|
+
// asOfSha is the DIFF BASE (pinned), not pins.refresh (H12): "changed
|
|
126
|
+
// since <X>" must name the sha the changed-file diff was computed against —
|
|
127
|
+
// the same base flagStaleArtifacts uses. pins.refresh named the current
|
|
128
|
+
// HEAD on first refresh (vacuously "changed since itself") and a stale
|
|
129
|
+
// last-refresh sha thereafter, since the diff base never advances.
|
|
130
|
+
const annotated = annotateStale(art.body, { changedFiles: [...changedSet], asOfSha: pinned ?? headSha, now: ctx.now() }).body;
|
|
131
|
+
let verdict = null; // "resolved" | "verified" | null (stays stale)
|
|
132
|
+
if (finding.reproduction?.method === "reread") {
|
|
133
|
+
const raw = await withFallback(ctx.llm, {
|
|
134
|
+
prompt: [
|
|
135
|
+
"Re-verdict this previously verified finding against the CURRENT cited code slice.",
|
|
136
|
+
'Return ONLY JSON {"verdict":"CONFIRM"|"KILL"|"UNCERTAIN"}.',
|
|
137
|
+
"Claim:", finding.title,
|
|
138
|
+
"Evidence:", JSON.stringify(finding.evidence),
|
|
139
|
+
].join("\n"),
|
|
140
|
+
tier: "frontier",
|
|
141
|
+
label: "verdict",
|
|
142
|
+
jsonMode: true,
|
|
143
|
+
}, () => null);
|
|
144
|
+
const v = coerceJson(raw)?.verdict;
|
|
145
|
+
if (v === "CONFIRM") verdict = "verified";
|
|
146
|
+
else if (v === "KILL") verdict = "resolved";
|
|
147
|
+
} else {
|
|
148
|
+
const r = rerunReproduction(ctx.root, finding, ctx.exec);
|
|
149
|
+
if (r.reproduced === true) verdict = "verified";
|
|
150
|
+
else if (r.reproduced === false) verdict = "resolved";
|
|
151
|
+
}
|
|
152
|
+
if (verdict === "resolved") {
|
|
153
|
+
const body = `${annotated.replace(/\n+$/, "\n")}\n> RESOLVED @ ${sha7} (${ctx.now()}): reproduction no longer reproduces.\n`;
|
|
154
|
+
writeArtifact(ctx.dotdir, rel, { meta: { ...art.meta, stale: true }, body });
|
|
155
|
+
resolvedIds.push(finding.id);
|
|
156
|
+
} else if (verdict === "verified") {
|
|
157
|
+
const evidence = Array.isArray(art.meta.evidence)
|
|
158
|
+
? art.meta.evidence.map((e) => (typeof e === "string" ? e.replace(/@[0-9a-f]{7,40}/, `@${sha7}`) : { ...e, sha: sha7 }))
|
|
159
|
+
: art.meta.evidence;
|
|
160
|
+
const body = repinText(annotated, changedSet, sha7);
|
|
161
|
+
writeArtifact(ctx.dotdir, rel, { meta: { ...art.meta, evidence, stale: false, headSha }, body });
|
|
162
|
+
} else {
|
|
163
|
+
writeArtifact(ctx.dotdir, rel, { meta: { ...art.meta, stale: true }, body: annotated });
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
return { resolvedIds, staleFlagged };
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// 6. behavior-diff regression detection (declared degradation when unavailable)
|
|
170
|
+
function behaviorDiffStep(ctx) {
|
|
171
|
+
const cfg = path.join(ctx.dotdir, BEHAVIOR_CONFIG);
|
|
172
|
+
const before = path.join(ctx.dotdir, BEHAVIOR_BEFORE);
|
|
173
|
+
if (!fs.existsSync(cfg) || !fs.existsSync(before)) {
|
|
174
|
+
return { note: `${NO_DIFF_NOTE} (no captured baseline)`, regressions: null };
|
|
175
|
+
}
|
|
176
|
+
const after = path.join(ctx.dotdir, BEHAVIOR_AFTER);
|
|
177
|
+
const cap = runBehaviorDiff(ctx.adlc, "capture", ["--config", cfg, "--out", after, "--json"], { cwd: ctx.root, exec: ctx.exec });
|
|
178
|
+
if (cap.skipped) return { note: NO_DIFF_NOTE, regressions: null };
|
|
179
|
+
if (!cap.ok) return { note: "behavior-diff capture failed — boundary regressions not checked", regressions: null };
|
|
180
|
+
const cmp = runBehaviorDiff(ctx.adlc, "compare", [before, after, "--json"], { cwd: ctx.root, exec: ctx.exec });
|
|
181
|
+
if (cmp.skipped) return { note: NO_DIFF_NOTE, regressions: null };
|
|
182
|
+
if (!cmp.gateFailed) return { note: "behavior-diff: no boundary regressions detected", regressions: [] };
|
|
183
|
+
const raw = cmp.json?.changed ?? cmp.json?.differences ?? cmp.json?.regressions ?? [];
|
|
184
|
+
const regressions = (Array.isArray(raw) ? raw : []).map((r) => (typeof r === "string" ? r : r?.id ?? r?.name ?? JSON.stringify(r)));
|
|
185
|
+
return { note: `behavior-diff: ${regressions.length} changed behavior(s)`, regressions };
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
export async function run(ctx) {
|
|
189
|
+
const { root, dotdir, log } = ctx;
|
|
190
|
+
let state = ctx.state;
|
|
191
|
+
const pinned = state?.pins?.comprehend ?? state?.pins?.scan;
|
|
192
|
+
if (!pinned) {
|
|
193
|
+
throw new OpError("Nothing to refresh — no completed phase pins in state.json. Run `do-better scan` first.");
|
|
194
|
+
}
|
|
195
|
+
const headSha = gitHeadSha(root, ctx.exec);
|
|
196
|
+
const changed = changedFilesSince(root, pinned, ctx.exec);
|
|
197
|
+
if (!changed.length) {
|
|
198
|
+
state = finishState(state, ctx, { headSha, status: "done", changedFiles: 0, staleClaims: 0 });
|
|
199
|
+
return {
|
|
200
|
+
state,
|
|
201
|
+
gate: null,
|
|
202
|
+
summary: `Fresh @ ${headSha.slice(0, 7)} — no files changed since pinned ${String(pinned).slice(0, 7)}.`,
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
const changedSet = new Set(changed.filter(STALE_SCOPE));
|
|
206
|
+
|
|
207
|
+
// 3. flag stale claims
|
|
208
|
+
let staleClaims = flagStaleArtifacts(ctx, changedSet, pinned);
|
|
209
|
+
// 4. targeted re-scan, changed files only
|
|
210
|
+
await refreshCodemap(ctx, [...changedSet], pinned);
|
|
211
|
+
reverifyInventory(ctx, changedSet);
|
|
212
|
+
// 5. re-verify stale findings
|
|
213
|
+
const { resolvedIds, staleFlagged } = await reverifyFindings(ctx, changedSet, headSha, pinned);
|
|
214
|
+
staleClaims += staleFlagged;
|
|
215
|
+
|
|
216
|
+
// 7. roadmap living-document update
|
|
217
|
+
let roadmap = readArtifact(dotdir, LAYOUT.roadmap);
|
|
218
|
+
if (roadmap) {
|
|
219
|
+
let body = roadmap.body;
|
|
220
|
+
for (const id of resolvedIds) body = markRoadmapResolved(body, id, headSha);
|
|
221
|
+
if (body !== roadmap.body) {
|
|
222
|
+
writeArtifact(dotdir, LAYOUT.roadmap, { meta: roadmap.meta, body });
|
|
223
|
+
roadmap = { ...roadmap, body };
|
|
224
|
+
const raw = fs.readFileSync(path.join(dotdir, LAYOUT.roadmap), "utf8");
|
|
225
|
+
state = recordRoadmapHash(state, { sha256: sha256Hex(raw), headSha, now: ctx.now() });
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// 6. behavior-diff regression hook (may gate-fail → exit 2)
|
|
230
|
+
const diff = behaviorDiffStep(ctx);
|
|
231
|
+
if (diff.regressions && diff.regressions.length) {
|
|
232
|
+
if (roadmap) {
|
|
233
|
+
let body = roadmap.body;
|
|
234
|
+
for (const beh of diff.regressions) {
|
|
235
|
+
if (body.includes(beh)) continue; // a roadmap item already claims this behavior change
|
|
236
|
+
body = markRoadmapRegressed(body, beh, "boundary behavior changed with no roadmap item claiming it");
|
|
237
|
+
}
|
|
238
|
+
if (body !== roadmap.body) {
|
|
239
|
+
writeArtifact(dotdir, LAYOUT.roadmap, { meta: roadmap.meta, body });
|
|
240
|
+
const raw = fs.readFileSync(path.join(dotdir, LAYOUT.roadmap), "utf8");
|
|
241
|
+
state = recordRoadmapHash(state, { sha256: sha256Hex(raw), headSha, now: ctx.now() });
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
state = finishState(state, ctx, { headSha, status: "failed", changedFiles: changed.length, staleClaims });
|
|
245
|
+
throw gateFail(state, "refresh", `behavior-diff compare failed: ${diff.regressions.join(", ")} — boundary behavior changed without a claiming roadmap item`);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
state = finishState(state, ctx, { headSha, status: "done", changedFiles: changed.length, staleClaims });
|
|
249
|
+
const resolvedNote = resolvedIds.length ? `; resolved: ${resolvedIds.join(", ")} (marked ✅ done)` : "";
|
|
250
|
+
log.info(diff.note);
|
|
251
|
+
return {
|
|
252
|
+
state,
|
|
253
|
+
gate: null,
|
|
254
|
+
summary: `Refreshed @ ${headSha.slice(0, 7)}: ${changed.length} changed file(s), ${staleClaims} stale claim(s) flagged${resolvedNote}; ${diff.note}.`,
|
|
255
|
+
};
|
|
256
|
+
}
|