@dzhechkov/harness-core 0.3.112 → 0.3.114
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/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/rake-analyzer.d.ts +82 -0
- package/dist/rake-analyzer.d.ts.map +1 -0
- package/dist/rake-analyzer.js +249 -0
- package/dist/rake-analyzer.js.map +1 -0
- package/dist/session-retro.d.ts +78 -0
- package/dist/session-retro.d.ts.map +1 -0
- package/dist/session-retro.js +325 -0
- package/dist/session-retro.js.map +1 -0
- package/package.json +4 -4
- package/src/index.ts +2 -0
- package/src/rake-analyzer.ts +265 -0
- package/src/session-retro.ts +337 -0
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-session retro & co-learning loop (feature session-retro-colearn, ADR-001).
|
|
3
|
+
*
|
|
4
|
+
* At session end, `dz retro` mines the CURRENT session transcript for recurring PROCESS rakes, drills the
|
|
5
|
+
* user (socratic + checklist), and teaches/reinforces the agent — from the same mistake ("учиться вместе").
|
|
6
|
+
* The recurrence ledger IS the `dz teach` store (domain `retro`), so agent-recall and user-recurrence read
|
|
7
|
+
* ONE store (Step-0 recall: a feedback loop needs collect + rank + apply, not two write-only logs).
|
|
8
|
+
*
|
|
9
|
+
* parse/detect/render are PURE + deterministic (sorted, no clock/random); the stream/find helpers do disk
|
|
10
|
+
* I/O with TOP-LEVEL node:fs (harness-core is ESM — a lazy require() is undefined at runtime; the R1 footgun)
|
|
11
|
+
* and NEVER slurp a whole transcript (they reach ~95 MB — read + split lines, parse line-by-line).
|
|
12
|
+
*
|
|
13
|
+
* SAFETY PROPERTY (ADR-001 §3, load-bearing): a rake seen for the FIRST time (effective count < threshold)
|
|
14
|
+
* is taught silently but NOT drilled — no nagging on a one-off. Drills are for recurrent patterns only.
|
|
15
|
+
*/
|
|
16
|
+
import { existsSync, readFileSync, readdirSync, statSync, openSync, readSync, closeSync } from 'node:fs';
|
|
17
|
+
import { join } from 'node:path';
|
|
18
|
+
import { homedir } from 'node:os';
|
|
19
|
+
export const RETRO_DOMAIN = 'retro';
|
|
20
|
+
export const DEFAULT_DRILL_THRESHOLD = 2;
|
|
21
|
+
export const PROCESS_SIGNATURES = [
|
|
22
|
+
{
|
|
23
|
+
id: 'claimed-done-without-verify',
|
|
24
|
+
label: 'claimed done/fixed without running a verification',
|
|
25
|
+
socratic: 'Before you typed "done" — what exact command would have PROVEN it? Predict it, then check whether you actually ran it.',
|
|
26
|
+
checklist: 'Run the verification (test / build / repro) and READ its output BEFORE claiming done. No completion claim without fresh evidence.',
|
|
27
|
+
skill: 'validate',
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
id: 'n-fix-cycles',
|
|
31
|
+
label: 'multiple fix→break→fix cycles on one file (no root cause)',
|
|
32
|
+
socratic: 'After the 2nd failed fix — did you find the ROOT cause, or keep patching symptoms? Predict the real cause before the next change.',
|
|
33
|
+
checklist: 'Stop after 2 failed attempts. Revert, find the root cause (trace the bad value to its source), then ONE fix.',
|
|
34
|
+
skill: 'systematic-debugging',
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
id: 'ignored-user-correction',
|
|
38
|
+
label: 'the user had to correct the same point repeatedly',
|
|
39
|
+
socratic: 'When the user said "нет/wrong" the 2nd time — what did you keep assuming? Predict the misread before re-reading their message.',
|
|
40
|
+
checklist: 'On the 2nd correction, STOP and re-read the user\'s messages literally. Restate the ask back before acting.',
|
|
41
|
+
},
|
|
42
|
+
];
|
|
43
|
+
for (const s of PROCESS_SIGNATURES)
|
|
44
|
+
Object.freeze(s);
|
|
45
|
+
Object.freeze(PROCESS_SIGNATURES);
|
|
46
|
+
const byStr = (a, b) => (a < b ? -1 : a > b ? 1 : 0);
|
|
47
|
+
// NB: `\b` is an ASCII word boundary — it does NOT anchor Cyrillic (the R2 cross-model-QE lesson), so the
|
|
48
|
+
// Russian alternatives use a leading letter-class lookbehind only (no TRAILING lookahead — it would reject
|
|
49
|
+
// inflected stems like "прошли/проходят/исправила"; cross-model QE caught the truncated-stem miss).
|
|
50
|
+
const DONE_RE = /(?<![a-zа-яё])(done|fixed|works now|passes|passing|готово|исправил\w*|работает|прошл\w*|проход\w*)/i;
|
|
51
|
+
const VERIFY_RE = /\b(test|tests|vitest|pytest|jest|npm test|pnpm test|npm run|tsc|typecheck|noEmit|cargo test|go test|build|repro|coverage|lint)\b/i;
|
|
52
|
+
// Negation immediately before a done-claim ("not done", "isn't fixed", "не готово") — suppress the accusation.
|
|
53
|
+
const NEG_RE = /\b(not|isn'?t|aren'?t|wasn'?t|won'?t|can'?t|couldn'?t|didn'?t|no longer)\b|(?<![a-zа-яё])(не|нет|ещё не|еще не)(?![a-zа-яё])/i;
|
|
54
|
+
// Explicit corrections only — dropped bare "again/wrong" (matched "thanks again" / "don't get me wrong").
|
|
55
|
+
const CORRECTION_RE = /(?<![a-zа-яё])(нет,|не так|неверно|не то|переделай)(?![a-zа-яё])|\b(that'?s not right|not right|that'?s wrong|incorrect|redo this|you misread)\b/i;
|
|
56
|
+
const WINDOW = 8;
|
|
57
|
+
/**
|
|
58
|
+
* Detect PROCESS rakes over the event stream. PURE + deterministic. Conservative (high-precision): prefer a
|
|
59
|
+
* miss to a false accusation (a wrong "you claimed done without testing" erodes trust worse than a miss).
|
|
60
|
+
* Returns ONE aggregated hit per signature that fired, `withinSession` = occurrence count.
|
|
61
|
+
*/
|
|
62
|
+
export function detectProcessRakes(events) {
|
|
63
|
+
const counts = new Map();
|
|
64
|
+
const bump = (sig, ev) => {
|
|
65
|
+
const c = counts.get(sig) ?? { n: 0, evidence: [] };
|
|
66
|
+
c.n += 1;
|
|
67
|
+
if (c.evidence.length < 3)
|
|
68
|
+
c.evidence.push(ev.replace(/\s+/g, ' ').trim().slice(0, 120));
|
|
69
|
+
counts.set(sig, c);
|
|
70
|
+
};
|
|
71
|
+
// NB: no `didnt-read-before-edit` signature — the harness ENFORCES read-before-edit (an Edit fails
|
|
72
|
+
// without a prior Read), so a genuine violation is near-impossible; that signal was pure artifact
|
|
73
|
+
// (cross-session / bounded-window reads, 88 false hits on the dogfood) and was dropped after cross-model QE.
|
|
74
|
+
const editsPerFile = new Map();
|
|
75
|
+
const failedAfterEdit = new Set(); // files that had a TEST failure after being edited
|
|
76
|
+
let lastEditedFile;
|
|
77
|
+
const TESTFAIL_RE = /\b(fail(ed|ing|s)?|assertion|assert|expected|not ok|panic|traceback|error ts\d|\d+ failed)\b/i;
|
|
78
|
+
for (let i = 0; i < events.length; i++) {
|
|
79
|
+
const e = events[i];
|
|
80
|
+
if (e.kind === 'tool' && e.tool === 'Edit' && e.file) {
|
|
81
|
+
editsPerFile.set(e.file, (editsPerFile.get(e.file) ?? 0) + 1);
|
|
82
|
+
lastEditedFile = e.file;
|
|
83
|
+
const edits = editsPerFile.get(e.file);
|
|
84
|
+
if (edits >= 3 && failedAfterEdit.has(e.file)) {
|
|
85
|
+
bump('n-fix-cycles', `${edits} edits to ${e.file} with a failing test between`);
|
|
86
|
+
failedAfterEdit.delete(e.file);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
// Only a TEST/BUILD failure (not a generic Read error) arms the most-recently-edited file, so an
|
|
90
|
+
// UNRELATED failure no longer globally triggers a fix-cycle (cross-model QE High).
|
|
91
|
+
if (e.kind === 'tool' && e.ok === false && lastEditedFile !== undefined && TESTFAIL_RE.test(e.text))
|
|
92
|
+
failedAfterEdit.add(lastEditedFile);
|
|
93
|
+
// claimed-done-without-verify: a done-claim that (a) FOLLOWS a code change in the window AND (b) has NO
|
|
94
|
+
// verification tool in the window. The change-in-window gate cuts prose "done"/"tests pass" that made
|
|
95
|
+
// no edit (measured over-firing on the dogfood — NFR-2 conservative).
|
|
96
|
+
if (e.kind === 'assistant') {
|
|
97
|
+
const m = DONE_RE.exec(e.text);
|
|
98
|
+
if (m) {
|
|
99
|
+
const before = e.text.slice(Math.max(0, m.index - 30), m.index);
|
|
100
|
+
const negated = NEG_RE.test(before) || NEG_RE.test(e.text.slice(m.index, m.index + 6));
|
|
101
|
+
if (!negated) {
|
|
102
|
+
let verified = false, changed = false;
|
|
103
|
+
for (let j = Math.max(0, i - WINDOW); j < i; j++) {
|
|
104
|
+
const p = events[j];
|
|
105
|
+
if (p.kind !== 'tool')
|
|
106
|
+
continue;
|
|
107
|
+
if (VERIFY_RE.test(`${p.tool ?? ''} ${p.text}`))
|
|
108
|
+
verified = true;
|
|
109
|
+
if (p.tool === 'Edit' || p.tool === 'Write')
|
|
110
|
+
changed = true;
|
|
111
|
+
}
|
|
112
|
+
if (changed && !verified)
|
|
113
|
+
bump('claimed-done-without-verify', e.text.slice(0, 120));
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
// ignored-user-correction: 2nd+ correction within a short window of user turns.
|
|
118
|
+
if (e.kind === 'user' && CORRECTION_RE.test(e.text)) {
|
|
119
|
+
let priorCorrections = 0;
|
|
120
|
+
for (let j = Math.max(0, i - WINDOW * 2); j < i; j++) {
|
|
121
|
+
const p = events[j];
|
|
122
|
+
if (p.kind === 'user' && CORRECTION_RE.test(p.text))
|
|
123
|
+
priorCorrections++;
|
|
124
|
+
}
|
|
125
|
+
if (priorCorrections >= 1)
|
|
126
|
+
bump('ignored-user-correction', e.text);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
const hits = [];
|
|
130
|
+
for (const sig of PROCESS_SIGNATURES) {
|
|
131
|
+
const c = counts.get(sig.id);
|
|
132
|
+
if (c)
|
|
133
|
+
hits.push({ signature: sig.id, label: sig.label, withinSession: c.n, evidence: c.evidence });
|
|
134
|
+
}
|
|
135
|
+
return hits.sort((a, b) => b.withinSession - a.withinSession || byStr(a.signature, b.signature));
|
|
136
|
+
}
|
|
137
|
+
const sigById = (id) => PROCESS_SIGNATURES.find((s) => s.id === id);
|
|
138
|
+
/** The stable store-key lesson for a signature (so teach/reinforce dedups on it and the ledger counts it). */
|
|
139
|
+
export function retroLessonText(sig) {
|
|
140
|
+
const s = sigById(sig);
|
|
141
|
+
return `Process rake [${sig}]: ${s ? s.label : sig}. ${s?.checklist ?? ''}`.trim();
|
|
142
|
+
}
|
|
143
|
+
/** Render the mix drill: a socratic predict-then-reveal prompt, a marker, then the concrete checklist. */
|
|
144
|
+
export function renderDrill(sig, effective) {
|
|
145
|
+
const skill = sig.skill ? ` (see the \`${sig.skill}\` skill)` : '';
|
|
146
|
+
return [
|
|
147
|
+
` 🔁 ${sig.label} — ${effective}× (recurring)`,
|
|
148
|
+
` ${sig.socratic}`,
|
|
149
|
+
` --- reveal (cover this, predict first) ---`,
|
|
150
|
+
` ✅ ${sig.checklist}${skill}`,
|
|
151
|
+
].join('\n');
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Build the retro. PURE. A hit is DRILLED only when `ledgerCount + withinSession >= threshold` (recurrent);
|
|
155
|
+
* otherwise it ACCRUES (taught silently, no drill) — the load-bearing anti-noise property (ADR-001 §3).
|
|
156
|
+
*/
|
|
157
|
+
export function buildRetro(hits, ledger, totalEvents, drillThreshold = DEFAULT_DRILL_THRESHOLD) {
|
|
158
|
+
const items = hits.map((hit) => {
|
|
159
|
+
const ledgerCount = ledger.get(hit.signature) ?? 0;
|
|
160
|
+
const effective = ledgerCount + hit.withinSession;
|
|
161
|
+
if (effective >= drillThreshold) {
|
|
162
|
+
const sig = sigById(hit.signature);
|
|
163
|
+
const drill = sig ? renderDrill(sig, effective) : undefined;
|
|
164
|
+
return drill !== undefined
|
|
165
|
+
? { hit, ledgerCount, effective, status: 'drill', drill }
|
|
166
|
+
: { hit, ledgerCount, effective, status: 'drill' };
|
|
167
|
+
}
|
|
168
|
+
return { hit, ledgerCount, effective, status: 'accrue' };
|
|
169
|
+
});
|
|
170
|
+
return {
|
|
171
|
+
items,
|
|
172
|
+
drilled: items.filter((i) => i.status === 'drill').length,
|
|
173
|
+
accrued: items.filter((i) => i.status === 'accrue').length,
|
|
174
|
+
totalEvents,
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
/** Human render of the retro. Deterministic. */
|
|
178
|
+
export function renderRetro(retro) {
|
|
179
|
+
if (retro.items.length === 0)
|
|
180
|
+
return `retro: no process rakes detected in ${retro.totalEvents} event(s). Clean session.`;
|
|
181
|
+
const lines = [`retro: ${retro.drilled} recurring rake(s) to drill, ${retro.accrued} accruing (from ${retro.totalEvents} events):`, ''];
|
|
182
|
+
for (const it of retro.items) {
|
|
183
|
+
if (it.status === 'drill' && it.drill) {
|
|
184
|
+
lines.push(it.drill);
|
|
185
|
+
lines.push('');
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
const accruing = retro.items.filter((i) => i.status === 'accrue');
|
|
189
|
+
if (accruing.length > 0) {
|
|
190
|
+
lines.push(' accruing (first time — taught, not drilled yet):');
|
|
191
|
+
for (const it of accruing)
|
|
192
|
+
lines.push(` · ${it.hit.label} (×${it.hit.withinSession} this session)`);
|
|
193
|
+
}
|
|
194
|
+
return lines.join('\n');
|
|
195
|
+
}
|
|
196
|
+
/** Cap the read at the last N bytes for very large transcripts (a retro is about the RECENT session), so
|
|
197
|
+
* memory stays bounded rather than slurping a multi-hundred-MB file whole (cross-model QE). */
|
|
198
|
+
const MAX_READ_BYTES = 48 * 1024 * 1024;
|
|
199
|
+
function readBounded(path) {
|
|
200
|
+
let size = 0;
|
|
201
|
+
try {
|
|
202
|
+
size = statSync(path).size;
|
|
203
|
+
}
|
|
204
|
+
catch {
|
|
205
|
+
return '';
|
|
206
|
+
}
|
|
207
|
+
if (size <= MAX_READ_BYTES) {
|
|
208
|
+
try {
|
|
209
|
+
return readFileSync(path, 'utf8');
|
|
210
|
+
}
|
|
211
|
+
catch {
|
|
212
|
+
return '';
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
// Read only the tail; drop the first (partial) line.
|
|
216
|
+
const fd = openSync(path, 'r');
|
|
217
|
+
try {
|
|
218
|
+
const buf = Buffer.allocUnsafe(MAX_READ_BYTES);
|
|
219
|
+
const bytes = readSync(fd, buf, 0, MAX_READ_BYTES, size - MAX_READ_BYTES);
|
|
220
|
+
const tail = buf.toString('utf8', 0, bytes);
|
|
221
|
+
const nl = tail.indexOf('\n');
|
|
222
|
+
return nl >= 0 ? tail.slice(nl + 1) : tail;
|
|
223
|
+
}
|
|
224
|
+
catch {
|
|
225
|
+
return '';
|
|
226
|
+
}
|
|
227
|
+
finally {
|
|
228
|
+
closeSync(fd);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
const isObj = (x) => x !== null && typeof x === 'object';
|
|
232
|
+
/**
|
|
233
|
+
* Parse a Claude Code JSONL transcript into a normalized event stream. Bad/`null`/malformed lines are
|
|
234
|
+
* skipped (never throws — cross-model QE caught a crash on a `null` line and a `[null]` content block).
|
|
235
|
+
* Text blocks WITHIN one message are merged into a SINGLE assistant/user event, so a multi-block turn
|
|
236
|
+
* ("Done." + "Fixed.") counts as ONE claim, not two (else the anti-noise guarantee is defeated).
|
|
237
|
+
*/
|
|
238
|
+
export function streamSessionEvents(path) {
|
|
239
|
+
const out = [];
|
|
240
|
+
const raw = readBounded(path);
|
|
241
|
+
if (raw === '')
|
|
242
|
+
return out;
|
|
243
|
+
for (const line of raw.split('\n')) {
|
|
244
|
+
const t = line.trim();
|
|
245
|
+
if (t === '')
|
|
246
|
+
continue;
|
|
247
|
+
let obj;
|
|
248
|
+
try {
|
|
249
|
+
obj = JSON.parse(t);
|
|
250
|
+
}
|
|
251
|
+
catch {
|
|
252
|
+
continue;
|
|
253
|
+
}
|
|
254
|
+
if (!isObj(obj))
|
|
255
|
+
continue;
|
|
256
|
+
const msg = obj.message;
|
|
257
|
+
if (!isObj(msg))
|
|
258
|
+
continue;
|
|
259
|
+
const role = typeof msg.role === 'string' ? msg.role : '';
|
|
260
|
+
const content = msg.content;
|
|
261
|
+
if (typeof content === 'string') {
|
|
262
|
+
if (content.trim() !== '')
|
|
263
|
+
out.push({ kind: role === 'assistant' ? 'assistant' : 'user', text: content });
|
|
264
|
+
continue;
|
|
265
|
+
}
|
|
266
|
+
if (!Array.isArray(content))
|
|
267
|
+
continue;
|
|
268
|
+
const textParts = [];
|
|
269
|
+
for (const b of content) {
|
|
270
|
+
if (!isObj(b))
|
|
271
|
+
continue; // guard a `[null]` block (cross-model QE)
|
|
272
|
+
if (b.type === 'text' && typeof b.text === 'string') {
|
|
273
|
+
textParts.push(b.text);
|
|
274
|
+
}
|
|
275
|
+
else if (b.type === 'tool_use') {
|
|
276
|
+
const input = isObj(b.input) ? b.input : undefined;
|
|
277
|
+
const file = input?.file_path ?? input?.path;
|
|
278
|
+
const name = typeof b.name === 'string' ? b.name : undefined;
|
|
279
|
+
// Capture the Bash COMMAND as the event text so a real verification (`pnpm tsc`, `npm test`) is
|
|
280
|
+
// visible — dropping it made the "done without verify" check blind (cross-model QE).
|
|
281
|
+
const text = (name === 'Bash' && typeof input?.command === 'string') ? input.command : (name ?? '');
|
|
282
|
+
out.push({ kind: 'tool', text, ...(name ? { tool: name } : {}), ...(file ? { file } : {}) });
|
|
283
|
+
}
|
|
284
|
+
else if (b.type === 'tool_result') {
|
|
285
|
+
const c = b.content;
|
|
286
|
+
const text = typeof c === 'string' ? c : JSON.stringify(c ?? '');
|
|
287
|
+
out.push({ kind: 'tool', text: text.slice(0, 2000), ok: b.is_error !== true });
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
if (textParts.length > 0)
|
|
291
|
+
out.push({ kind: role === 'assistant' ? 'assistant' : 'user', text: textParts.join('\n') });
|
|
292
|
+
}
|
|
293
|
+
return out;
|
|
294
|
+
}
|
|
295
|
+
/** Find the most recently modified session transcript (roam state, then ~/.claude/projects). Null if none. */
|
|
296
|
+
export function findLatestTranscript(repoRoot) {
|
|
297
|
+
let best = null;
|
|
298
|
+
const consider = (p) => {
|
|
299
|
+
try {
|
|
300
|
+
const st = statSync(p);
|
|
301
|
+
// tie-break on path so equal mtimes are deterministic (cross-model QE).
|
|
302
|
+
if (st.isFile() && (best === null || st.mtimeMs > best.mtime || (st.mtimeMs === best.mtime && p < best.path)))
|
|
303
|
+
best = { path: p, mtime: st.mtimeMs };
|
|
304
|
+
}
|
|
305
|
+
catch { /* skip */ }
|
|
306
|
+
};
|
|
307
|
+
const scanDir = (dir) => {
|
|
308
|
+
try {
|
|
309
|
+
if (existsSync(dir))
|
|
310
|
+
for (const e of readdirSync(dir))
|
|
311
|
+
if (e.endsWith('.jsonl'))
|
|
312
|
+
consider(join(dir, e));
|
|
313
|
+
}
|
|
314
|
+
catch { /* ignore */ }
|
|
315
|
+
};
|
|
316
|
+
scanDir(join(repoRoot, 'roam', 'claude-state'));
|
|
317
|
+
// ~/.claude/projects/<encoded-repoRoot>/<uuid>.jsonl (the contract's second source).
|
|
318
|
+
try {
|
|
319
|
+
const enc = repoRoot.replace(/\//g, '-');
|
|
320
|
+
scanDir(join(homedir(), '.claude', 'projects', enc));
|
|
321
|
+
}
|
|
322
|
+
catch { /* ignore */ }
|
|
323
|
+
return best === null ? null : best.path;
|
|
324
|
+
}
|
|
325
|
+
//# sourceMappingURL=session-retro.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"session-retro.js","sourceRoot":"","sources":["../src/session-retro.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dzhechkov/harness-core",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.114",
|
|
4
4
|
"description": "Shared harness logic - skill loading, additive apply, and the init/sync/verify/doctor operations.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -30,13 +30,13 @@
|
|
|
30
30
|
"@dzhechkov/adapter-openclaude": "^0.1.0",
|
|
31
31
|
"@dzhechkov/adapter-opencode": "^0.2.0",
|
|
32
32
|
"yaml": "^2.0.0",
|
|
33
|
+
"@dzhechkov/adapter-copilot": "0.1.1",
|
|
33
34
|
"@dzhechkov/adapter-gemini": "0.1.1",
|
|
34
35
|
"@dzhechkov/adapter-cursor": "0.1.1",
|
|
35
36
|
"@dzhechkov/adapter-agents-md": "0.1.1",
|
|
36
|
-
"@dzhechkov/adapter-
|
|
37
|
-
"@dzhechkov/core": "0.2.14",
|
|
37
|
+
"@dzhechkov/adapter-windsurf": "0.1.1",
|
|
38
38
|
"@dzhechkov/memory": "0.2.9",
|
|
39
|
-
"@dzhechkov/
|
|
39
|
+
"@dzhechkov/core": "0.2.14"
|
|
40
40
|
},
|
|
41
41
|
"peerDependenciesMeta": {
|
|
42
42
|
"@ruvector/rvf": {
|
package/src/index.ts
CHANGED
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MR rake analyzer (feature mr-rake-analyzer, ADR-001).
|
|
3
|
+
*
|
|
4
|
+
* Mines a project's review corpus for RECURRING mistakes ("rakes") and closes them into self-learning.
|
|
5
|
+
* The parse/normalize/detect/render functions are PURE + deterministic (sorted, no clock/random) so the
|
|
6
|
+
* same corpus yields a byte-identical report; the load/scan helpers do disk I/O with TOP-LEVEL node:fs
|
|
7
|
+
* imports (harness-core is ESM — a lazy require() is undefined at runtime; the R1 footgun).
|
|
8
|
+
*
|
|
9
|
+
* Signature is DETERMINISTIC (ADR-001 §1): a rule table of known rake classes, with an unmatched finding
|
|
10
|
+
* falling to a normalized-text bucket so novel recurrences still cluster. LLM classification is an optional
|
|
11
|
+
* amplifier, never in this core.
|
|
12
|
+
*
|
|
13
|
+
* SAFETY PROPERTY (ADR-001 §3, load-bearing): a finding whose signature appears in fewer than
|
|
14
|
+
* `thresholds.candidate` DISTINCT sources is a one-off — it is NEVER a rake and never reaches teach/critic.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { existsSync, readFileSync, readdirSync, statSync, realpathSync } from 'node:fs';
|
|
18
|
+
import { join } from 'node:path';
|
|
19
|
+
|
|
20
|
+
export type Severity = 'blocker' | 'high' | 'medium' | 'low' | 'unknown';
|
|
21
|
+
const SEVERITY_RANK: Record<Severity, number> = { blocker: 4, high: 3, medium: 2, low: 1, unknown: 0 };
|
|
22
|
+
|
|
23
|
+
export interface Finding {
|
|
24
|
+
readonly source: string; // artifact id (e.g. features/<slug>/08_qe_report.md)
|
|
25
|
+
readonly severity: Severity;
|
|
26
|
+
readonly text: string;
|
|
27
|
+
readonly site?: string; // file:line if present
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface Rake {
|
|
31
|
+
readonly signature: string;
|
|
32
|
+
readonly label: string;
|
|
33
|
+
readonly sources: readonly string[]; // DISTINCT sources (sorted)
|
|
34
|
+
readonly count: number; // = sources.length
|
|
35
|
+
readonly severity: Severity; // max across the group
|
|
36
|
+
readonly examples: readonly Finding[]; // up to 3, sorted
|
|
37
|
+
readonly status: 'candidate' | 'confirmed';
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface RakeThresholds { readonly candidate: number; readonly confirmed: number }
|
|
41
|
+
export const DEFAULT_RAKE_THRESHOLDS: RakeThresholds = { candidate: 2, confirmed: 3 };
|
|
42
|
+
|
|
43
|
+
export interface RakeReport {
|
|
44
|
+
readonly rakes: readonly Rake[];
|
|
45
|
+
readonly totalFindings: number;
|
|
46
|
+
readonly oneOffs: number; // signatures below the candidate threshold (dropped)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface RakeSignature { readonly id: string; readonly label: string; readonly patterns: readonly RegExp[] }
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Known rake classes (extensible, data-only). Seeded from the classes that actually recur in this repo's
|
|
53
|
+
* QE reports — that IS the dogfood. First match in order wins; unmatched → normalized-text bucket.
|
|
54
|
+
*/
|
|
55
|
+
export const RAKE_SIGNATURES: readonly RakeSignature[] = [
|
|
56
|
+
{ id: 'esm-require-footgun', label: 'ESM lazy require() undefined at runtime', patterns: [/require\(['"]node:/, /\besm\b.*require/i, /lazy require/i] },
|
|
57
|
+
{ id: 'untested-adr-property', label: 'ADR-named safety property left untested', patterns: [/load-bearing.*(untested|not\s+tested|no\s+test)/i, /adr.*names.*(property|test)/i, /safety property.*test/i] },
|
|
58
|
+
{ id: 'claim-check-fp', label: 'claim-check false positive / untagged count', patterns: [/claim-check.*(false positive|\bfp\b)/i, /untagged.*(count|claim)/i, /metric term/i] },
|
|
59
|
+
// "traversal" alone over-matches (AST/tree traversal); require a filesystem-scope token to CO-OCCUR
|
|
60
|
+
// (or a literal `../`) — cross-model QE caught the over-match.
|
|
61
|
+
{ id: 'path-traversal', label: 'path not constrained to the repo (traversal)', patterns: [/\.\.\//, /(?=.*travers)(?=.*(repo|root|\bpath\b|director|\/etc\/))/i, /escapes.{0,12}repo/i] },
|
|
62
|
+
{ id: 'silent-drop-or-inject', label: 'silent drop / silent injection (no report)', patterns: [/silent(ly)?\s+(drop|inject|discard|dropped)/i, /no silent (injection|caps|drop)/i] },
|
|
63
|
+
{ id: 'swallow-generic-exception', label: 'generic except/catch swallows real bugs', patterns: [/except\s+Exception/i, /catch.*swallow/i, /generic (exception|catch)/i] },
|
|
64
|
+
{ id: 'determinism-hole', label: 'non-deterministic output (unsorted/clock/random)', patterns: [/non-determinis/i, /determinism hole/i, /unsorted|not sorted/i] },
|
|
65
|
+
{ id: 'cross-model-self-qe', label: 'coder self-QE instead of cross-model', patterns: [/self-qe/i, /coder.*(review|qe).*(itself|self)/i, /cross-model/i] },
|
|
66
|
+
{ id: 'malformed-input-bypass', label: 'malformed input bypasses validation', patterns: [/array.*(pass|bypass)/i, /malformed.*(bypass|pass|manifest)/i, /typeof.*object/i] },
|
|
67
|
+
];
|
|
68
|
+
// Deep-freeze so an external caller can't inject a `/g`-flag regex whose `.test()` mutates lastIndex and
|
|
69
|
+
// makes signatureOf non-deterministic (cross-model QE). None of the patterns above use `g`/`y`.
|
|
70
|
+
for (const s of RAKE_SIGNATURES) { Object.freeze(s.patterns); Object.freeze(s); }
|
|
71
|
+
Object.freeze(RAKE_SIGNATURES);
|
|
72
|
+
|
|
73
|
+
const byStr = (a: string, b: string): number => (a < b ? -1 : a > b ? 1 : 0);
|
|
74
|
+
const uniqSorted = (xs: readonly string[]): string[] => [...new Set(xs)].sort(byStr);
|
|
75
|
+
const maxSeverity = (a: Severity, b: Severity): Severity => (SEVERITY_RANK[a] >= SEVERITY_RANK[b] ? a : b);
|
|
76
|
+
|
|
77
|
+
const STOPWORDS = new Set(['the', 'a', 'an', 'and', 'or', 'to', 'of', 'in', 'on', 'is', 'it', 'that', 'this', 'for', 'with', 'as', 'at', 'by', 'be', 'not', 'no', 'its', 'when', 'if', 'was', 'are', 'но', 'и', 'в', 'на', 'что', 'это', 'не', 'из', 'за', 'для']);
|
|
78
|
+
|
|
79
|
+
/** Normalize a finding's text to a stable clustering key: lowercase, strip sites/numbers/punct, top significant words. */
|
|
80
|
+
export function normalizeText(text: string): string {
|
|
81
|
+
const cleaned = text
|
|
82
|
+
.toLowerCase()
|
|
83
|
+
.replace(/[\w./-]+:\d+/g, ' ') // drop file:line
|
|
84
|
+
.replace(/`[^`]*`/g, ' ') // drop code literals
|
|
85
|
+
.replace(/[^a-zа-я\s]/gi, ' '); // drop digits/punct
|
|
86
|
+
const words = cleaned.split(/\s+/).filter((w) => w.length >= 4 && !STOPWORDS.has(w));
|
|
87
|
+
return uniqSorted(words).slice(0, 6).join(' ');
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const SEV_MAP: Record<string, Severity> = {
|
|
91
|
+
blocker: 'blocker', critical: 'blocker', crit: 'blocker',
|
|
92
|
+
high: 'high', hi: 'high',
|
|
93
|
+
medium: 'medium', med: 'medium',
|
|
94
|
+
low: 'low', nit: 'low',
|
|
95
|
+
};
|
|
96
|
+
const toSeverity = (raw: string): Severity => SEV_MAP[raw.trim().toLowerCase()] ?? 'unknown';
|
|
97
|
+
const SITE_RE = /([\w./-]+\.(?:ts|js|tsx|jsx|py|go|md|json|yml|yaml):\d+)/;
|
|
98
|
+
|
|
99
|
+
/** The signature of a finding: first matching rule, else the normalized-text bucket. */
|
|
100
|
+
export function signatureOf(finding: Finding): { id: string; label: string } {
|
|
101
|
+
for (const s of RAKE_SIGNATURES) {
|
|
102
|
+
if (s.patterns.some((p) => p.test(finding.text))) return { id: s.id, label: s.label };
|
|
103
|
+
}
|
|
104
|
+
const key = normalizeText(finding.text);
|
|
105
|
+
if (key !== '') return { id: `text:${key}`, label: key };
|
|
106
|
+
// No significant words (code-only / very short). Key on the LITERAL text so two DIFFERENT such findings
|
|
107
|
+
// never merge into a false "unclassified ×N" rake (cross-model QE) — but two IDENTICAL ones still cluster.
|
|
108
|
+
const literal = finding.text.toLowerCase().replace(/\s+/g, ' ').trim().slice(0, 80);
|
|
109
|
+
return { id: `literal:${literal}`, label: literal === '' ? 'unclassified finding' : literal };
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Parse one markdown artifact into findings. Handles (a) severity table rows `| … | High | <text> | … |`,
|
|
114
|
+
* (b) inline markers `[High]` / `**High —**` / `Sev — <text>`. Deterministic; unknown formats yield nothing.
|
|
115
|
+
*/
|
|
116
|
+
export function extractFindings(markdown: string, source: string): Finding[] {
|
|
117
|
+
const out: Finding[] = [];
|
|
118
|
+
const push = (severity: Severity, text: string): void => {
|
|
119
|
+
const t = text.replace(/\s+/g, ' ').trim();
|
|
120
|
+
if (t.length < 8) return; // too short to be a finding
|
|
121
|
+
const site = SITE_RE.exec(t)?.[1];
|
|
122
|
+
out.push(site ? { source, severity, text: t, site } : { source, severity, text: t });
|
|
123
|
+
};
|
|
124
|
+
for (const line of markdown.split('\n')) {
|
|
125
|
+
// (a) table row: | ... | <sev> | <finding> | ...
|
|
126
|
+
const cells = line.includes('|') ? line.split('|').map((c) => c.trim()) : null;
|
|
127
|
+
if (cells && cells.length >= 4) {
|
|
128
|
+
const sevCell = cells.find((c) => SEV_MAP[c.toLowerCase()] !== undefined);
|
|
129
|
+
if (sevCell) {
|
|
130
|
+
const sevIdx = cells.indexOf(sevCell);
|
|
131
|
+
const finding = cells.slice(sevIdx + 1).find((c) => c.length >= 8 && !/^-+$/.test(c));
|
|
132
|
+
if (finding) { push(toSeverity(sevCell), finding); continue; }
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
// (b) inline `[High] text` / `**High —** text` / `- High: text`. The bracketed form `[High] text`
|
|
136
|
+
// needs no separator (the brackets delimit); the bare form `High: text` requires one so prose like
|
|
137
|
+
// "high latency" doesn't register (cross-model QE: a missing separator silently dropped findings).
|
|
138
|
+
const bracketed = /^[\s\-*>]*\**\[(blocker|critical|high|medium|med|low)\]\**\s*[—:\-]?\s*(.+)$/i.exec(line);
|
|
139
|
+
const bare = /^[\s\-*>]*\**(blocker|critical|high|medium|med|low)\**\s*[—:]\s*(.+)$/i.exec(line);
|
|
140
|
+
const m = bracketed ?? bare;
|
|
141
|
+
if (m && m[1] && m[2]) push(toSeverity(m[1]), m[2]);
|
|
142
|
+
}
|
|
143
|
+
return out;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Detect rakes: group findings by signature, count DISTINCT sources, keep only groups at/above the candidate
|
|
148
|
+
* threshold (a below-threshold group is a one-off, NEVER a rake — the load-bearing anti-noise property).
|
|
149
|
+
* PURE + deterministic (ADR-001 §1): rakes sorted by (count desc, severity desc, signature asc).
|
|
150
|
+
*/
|
|
151
|
+
export function detectRakes(findings: readonly Finding[], thresholds: RakeThresholds = DEFAULT_RAKE_THRESHOLDS): RakeReport {
|
|
152
|
+
const groups = new Map<string, { label: string; findings: Finding[] }>();
|
|
153
|
+
for (const f of findings) {
|
|
154
|
+
const sig = signatureOf(f);
|
|
155
|
+
const g = groups.get(sig.id);
|
|
156
|
+
if (g) g.findings.push(f);
|
|
157
|
+
else groups.set(sig.id, { label: sig.label, findings: [f] });
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const rakes: Rake[] = [];
|
|
161
|
+
let oneOffs = 0;
|
|
162
|
+
for (const [signature, g] of groups) {
|
|
163
|
+
const sources = uniqSorted(g.findings.map((f) => f.source));
|
|
164
|
+
const count = sources.length;
|
|
165
|
+
if (count < thresholds.candidate) { oneOffs++; continue; }
|
|
166
|
+
const severity = g.findings.reduce<Severity>((m, f) => maxSeverity(m, f.severity), 'unknown');
|
|
167
|
+
const examples = [...g.findings]
|
|
168
|
+
.sort((a, b) => SEVERITY_RANK[b.severity] - SEVERITY_RANK[a.severity] || byStr(a.source, b.source))
|
|
169
|
+
.slice(0, 3);
|
|
170
|
+
rakes.push({ signature, label: g.label, sources, count, severity, examples, status: count >= thresholds.confirmed ? 'confirmed' : 'candidate' });
|
|
171
|
+
}
|
|
172
|
+
rakes.sort((a, b) => b.count - a.count || SEVERITY_RANK[b.severity] - SEVERITY_RANK[a.severity] || byStr(a.signature, b.signature));
|
|
173
|
+
return { rakes, totalFindings: findings.length, oneOffs };
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/** Human render of the rake report. Deterministic. */
|
|
177
|
+
export function renderRakeReport(report: RakeReport): string {
|
|
178
|
+
if (report.rakes.length === 0) {
|
|
179
|
+
return `mr-rakes: no recurring rakes (${report.totalFindings} finding(s), ${report.oneOffs} one-off signature(s) below threshold).`;
|
|
180
|
+
}
|
|
181
|
+
const lines = [`mr-rakes: ${report.rakes.length} rake(s) from ${report.totalFindings} finding(s) (${report.oneOffs} one-off(s) dropped):`, ''];
|
|
182
|
+
for (const r of report.rakes) {
|
|
183
|
+
lines.push(` [${r.status}] ${r.severity.toUpperCase()} ×${r.count} — ${r.label} (${r.signature})`);
|
|
184
|
+
lines.push(` sources: ${r.sources.join(', ')}`);
|
|
185
|
+
}
|
|
186
|
+
return lines.join('\n');
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/** The teachable rule text for a rake (fed to `dz teach`). Deterministic. */
|
|
190
|
+
export function rakeAsLesson(rake: Rake): string {
|
|
191
|
+
return `Project rake (recurred in ${rake.count} reviews): ${rake.label}. First seen: ${rake.examples[0]?.site ?? rake.sources[0]}. Watch for this class before it ships again.`;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/** Severity → teach reward. Higher-severity rakes are higher-signal lessons. */
|
|
195
|
+
export function rakeReward(rake: Rake): number {
|
|
196
|
+
return ({ blocker: 0.95, high: 0.9, medium: 0.8, low: 0.7, unknown: 0.7 } as const)[rake.severity];
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/** Render the CONFIRMED rakes as a project-critic SKILL.md section (sink B). Deterministic; confirmed only. */
|
|
200
|
+
export function renderCriticSection(report: RakeReport): string {
|
|
201
|
+
const confirmed = report.rakes.filter((r) => r.status === 'confirmed');
|
|
202
|
+
const lines = [
|
|
203
|
+
'## Recurring mistakes (auto-mined by `dz mr-rakes`)',
|
|
204
|
+
'',
|
|
205
|
+
confirmed.length === 0
|
|
206
|
+
? '_No confirmed recurring rakes yet._'
|
|
207
|
+
: 'These classes of mistake have recurred across this project\'s reviews. Flag them before they ship again:',
|
|
208
|
+
'',
|
|
209
|
+
];
|
|
210
|
+
for (const r of confirmed) {
|
|
211
|
+
lines.push(`- **${r.label}** (${r.severity}, ×${r.count}) — e.g. ${r.examples[0]?.site ?? r.sources[0]}.`);
|
|
212
|
+
}
|
|
213
|
+
return lines.join('\n');
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// ── Thin I/O (top-level fs; never throws) ────────────────────────────────────────────────────────────
|
|
217
|
+
|
|
218
|
+
/** Find review artifacts: each `features/<slug>/08_qe_report.md` plus any `REVIEW`-named markdown. Sorted. */
|
|
219
|
+
export function findReviewArtifacts(repoRoot: string): string[] {
|
|
220
|
+
if (typeof repoRoot !== 'string' || repoRoot === '') return []; // fail-open on bad runtime input (cross-model QE)
|
|
221
|
+
const candidates: string[] = [];
|
|
222
|
+
const featuresDir = join(repoRoot, 'features');
|
|
223
|
+
try {
|
|
224
|
+
if (existsSync(featuresDir)) {
|
|
225
|
+
for (const slug of readdirSync(featuresDir)) {
|
|
226
|
+
const qe = join(featuresDir, slug, '08_qe_report.md');
|
|
227
|
+
if (existsSync(qe)) candidates.push(`features/${slug}/08_qe_report.md`);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
} catch { /* ignore */ }
|
|
231
|
+
// Shallow scan of the repo root for REVIEW-named markdown (mr-review outputs land there).
|
|
232
|
+
try {
|
|
233
|
+
for (const entry of readdirSync(repoRoot)) {
|
|
234
|
+
if (/REVIEW.*\.md$/i.test(entry)) {
|
|
235
|
+
try { if (statSync(join(repoRoot, entry)).isFile()) candidates.push(entry); } catch { /* ignore */ }
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
} catch { /* ignore */ }
|
|
239
|
+
|
|
240
|
+
// Dedupe by PHYSICAL identity (realpath), not path string — two paths (e.g. a symlinked feature dir)
|
|
241
|
+
// pointing at ONE file must count as ONE source, else a single review fakes a rake (cross-model QE:
|
|
242
|
+
// the real load-bearing breach). Keep the first (sorted) relative path per physical file.
|
|
243
|
+
const seenReal = new Set<string>();
|
|
244
|
+
const out: string[] = [];
|
|
245
|
+
for (const rel of uniqSorted(candidates)) {
|
|
246
|
+
let real: string;
|
|
247
|
+
try { real = realpathSync(join(repoRoot, rel)); } catch { real = join(repoRoot, rel); }
|
|
248
|
+
if (seenReal.has(real)) continue;
|
|
249
|
+
seenReal.add(real);
|
|
250
|
+
out.push(rel);
|
|
251
|
+
}
|
|
252
|
+
return out;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/** Analyze the whole repo corpus. Impure wrapper: find artifacts → extract → detect. Never throws. */
|
|
256
|
+
export function analyzeCorpus(repoRoot: string, thresholds: RakeThresholds = DEFAULT_RAKE_THRESHOLDS): RakeReport {
|
|
257
|
+
const findings: Finding[] = [];
|
|
258
|
+
for (const rel of findReviewArtifacts(repoRoot)) {
|
|
259
|
+
try {
|
|
260
|
+
const md = readFileSync(join(repoRoot, rel), 'utf8');
|
|
261
|
+
findings.push(...extractFindings(md, rel));
|
|
262
|
+
} catch { /* skip unreadable artifact */ }
|
|
263
|
+
}
|
|
264
|
+
return detectRakes(findings, thresholds);
|
|
265
|
+
}
|