OverviewFilesCommitsIssuesDocsReleasesPeers

knowledge-ratchet commits / 760a806

Changes 3, 5, 6 — cross-project matches, unknown filter, shadow harness
Change 3: Cross-project similar-loop surfacing - loop-briefing.js: for each resolved loop in other projects, count file-basename overlap with the current project's danger files. Match if ≥1 overlap, sort by overlap count then by duration, show top 3 in a new CROSS-PROJECT SIMILAR LOOPS section. - Original intent was to wire loop-detector's findSimilarLoops, but its scoring (threshold ≥3, +2 per file overlap) expects a richer current fingerprint than SessionStart provides. Real data has mostly 1-file cross-project overlaps, which findSimilarLoops filters out. The inline matcher uses a lower threshold appropriate for that data. - loop-detector.js exports buildFingerprint for future use. - Verified: hypercore-kb session surfaces 27 cross-project matches sharing files like MEMORY.md, dashboard.js, rt-varying-crash-fix.md. Change 5: Drop unknown-project rules - rule-extractor.js dedup pass filters out rules with trigger.project === 'unknown'. These can never fire: loop-briefing matches rules by specific project name, and classify-project's git- root walker either returns a real project name or fails entirely. Unknown-project rules are unreachable by construction. - Dropped 21 stale unknowns from the previous corpus plus 1 new loop that re-classified unknown on the current run. Permanent filter prevents future accumulation. - Corpus: 271 → 251 rules after Change 5. Change 6: Shadow harness for longitudinal comparison - loop-briefing.js computes the NEW injected rule set AND a BASELINE rule set (what the pre-Change-2 briefing would have shown — filter by project, sort by source.duration, top 5). Logs the diff as one JSONL record per briefing to data/briefing-shadow.jsonl. - Record captures: counts.{projectRules, injected, baseline, needsInvestigation, crossProjectSurfaced, crossProjectTotal}, overlap count, removedByNew (rules baseline showed but quality filter suppressed), addedByNew (rules quality reordering promoted). - Side-effect only: does not affect injection. - Initial result for hypercore-kb session: overlap=1 out of 5. The quality filter reordered significantly — 4 rules removed (quality- doubt, wrong-output, runtime-error, other types that would have clotted the baseline top 5 by duration) and 4 rules promoted (needs-rethink, other types that were demoted in the baseline). Pipeline stays idempotent at 2.6s after initial run: Pre-dedup total: 251 Duplicates dropped: 0 Unknown-project rules dropped: 0 Total rules: 251 Final corpus state: Total: 251 rules Quality: 125 specific (49.8%), 98 partial (39.0%), 28 needs-investigation (11.2%) Unknown: 0 (filter working) Updates docs/audit-2026-04-10.md with Change 3/5/6 sections. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
760a8069186316715cd2408add7978f2c8c3b630
by plankatron <fleeky@gmail.com> · 2026-04-11T19:04:25.000Z

Changed files (5) +243 -32

README.md +2 -2
……@@ -50,8 +50,8 @@ node benchmark-ratchet.js
5050| Novel correction rate | 86.9% | >90% | Old mistakes eliminated (higher = better) |
5151| Correction density | 21.2% | <15% | Corrections per turn (lower = better) |
5252| Loop resolution | 8.2 turns | <6 turns | How fast loops resolve (lower = better) |
53| Learned rules | 271 | growing | Auto-extracted from resolved loops (content-addressed, idempotent across runs) |
54| Rule quality | 47.2% specific / 39.9% partial / 12.9% needs-investigation | more specific | Quality-aware injection prioritizes actionable rules |
53| Learned rules | 251 | growing | Auto-extracted from resolved loops (content-addressed, idempotent across runs, unknown-project filtered) |
54| Rule quality | 49.8% specific / 39.0% partial / 11.2% needs-investigation | more specific | Quality-aware injection prioritizes actionable rules |
5555
5656## Architecture
5757
docs/audit-2026-04-10.md +119 -22
……@@ -182,13 +182,104 @@ directory after backfill. The briefing header reads
182182rules all have at least one concrete signal (concrete fix files or a
183183concrete antipattern — not both missing).
184184
185## Change 3 — Wire `findSimilarLoops` into `loop-briefing.js`
185## Change 3 — Cross-project similar-loop surfacing
186
187Shipped. Original intent: wire `findSimilarLoops`. When implemented, it
188turned out `findSimilarLoops`'s scoring (+2 per file overlap, threshold
189≥3) expects a rich current fingerprint to work for cross-project matches.
190At SessionStart we only have project danger-file history, which typically
191yields 1-file overlaps with other-project loops (insufficient to clear
192threshold unless same type).
193
194Rather than force-fit `findSimilarLoops`, an inline custom matcher was
195added to `loop-briefing.js`: for each resolved loop in other projects,
196count file-basename overlap with the current project's danger files,
197keep any with ≥1 overlap, sort by overlap count then by duration, show
198top 3. `loop-detector.js` exports `buildFingerprint` for future use.
199
200**Verified:** hypercore-kb session surfaces 27 cross-project matches
201(dc-terrain, pear-git, hyper-social, hyper-tps) sharing files like
202`MEMORY.md`, `dashboard.js`, `rt-varying-crash-fix.md`. The top 3 are
203shown in a new `CROSS-PROJECT SIMILAR LOOPS` section in the briefing.
204
205## Change 5 — Drop unknown-project rules
206
207Shipped. Discovered while inspecting the 21 remaining unknowns after
208Change 4's dedup. The rules fall into categories:
209 - Personal-context bleed (one rule contained "Uphold crypto sale"
210 text — tax filing content captured as a correction)
211 - Planning/audit chatter misclassified as corrections
212 - Real loops stuck unknown because session cwd was missing or /tmp
213 - Rules referencing files that belong to real projects but whose
214 session context wasn't captured (e.g., fingerprints with
215 `benchmark-ratchet`, `promote`, `regress` which are knowledge-ratchet
216 files, but the session cwd didn't resolve via the git walker)
217
218None of these can ever fire: `loop-briefing.js` matches by exact project
219name, and live sessions now always classify to a real project via the
220git-root walker. Unknown-project rules are unreachable by construction.
221
222Fix: permanent filter in `rule-extractor.js`'s dedup pass.
223`const filtered = deduped.filter(r => r.trigger.project !== 'unknown')`.
224Catches both the existing 21 stale unknowns AND any future unknown rules
225that might be extracted from loops without usable cwd context.
226
227**Verified:** 271 → 251 rules (−21 dropped, +1 run-2 new loop dropped).
2280 unknown rules remain. Pipeline still idempotent at 2.6s on re-run.
229
230## Change 6 — Shadow harness for briefing comparison
231
232Shipped. Goal: longitudinal measurement of Change 2's quality filter
233and quality reordering by logging what the NEW briefing injected vs what
234a PRE-CHANGE-2 BASELINE briefing would have injected.
235
236Implementation: in `loop-briefing.js`, after computing the injectable
237rule set, also compute `shadowBaselineRules` = project rules sorted by
238`source.duration` (severity proxy), top 5. Log diff to
239`data/briefing-shadow.jsonl` on every briefing invocation.
240
241Record shape:
242
243```json
244{
245 "timestamp": "2026-04-11T19:02:06.079Z",
246 "project": "hypercore-kb",
247 "cwd": "/storage/projects/code/hypercore-kb",
248 "counts": {
249 "projectRules": 15,
250 "injected": 5,
251 "baseline": 5,
252 "needsInvestigation": 6,
253 "crossProjectSurfaced": 3,
254 "crossProjectTotal": 27
255 },
256 "overlap": 1,
257 "removedByNew": ["rule-...", "rule-...", ...],
258 "addedByNew": ["rule-...", "rule-...", ...]
259}
260```
261
262**Initial result for hypercore-kb:** overlap = 1 out of 5. Change 2's
263quality-tier reordering is significantly different from the baseline —
264it suppressed 4 rules (quality-doubt / wrong-output / runtime-error /
265other types whose duration would have put them in baseline's top 5)
266and promoted 4 rules (needs-rethink / other types whose quality was
267higher).
186268
187Still deferred. The function exists (`loop-detector.js:220`) and works.
188The wire-up is ~5 lines (import and call after the project filter).
189After Change 1, cross-project retrieval is less urgent — most loops
190now have specific project tags. Revisit once we see whether cross-project
191rule transfer is still a measurable gap.
269How to read the log longitudinally:
270
271 - `overlap` average over N sessions — measures how different
272 Change 2 is from baseline. 0 = completely different, 5 = identical.
273 - `removedByNew` accumulated — rules the quality filter rejected.
274 If any of these show up in later correction sequences, Change 2
275 was suppressing useful rules (false negatives).
276 - `addedByNew` accumulated — rules promoted by quality reordering.
277 If these correlate with fewer corrections in the session, Change 2
278 is surfacing useful rules.
279 - `crossProjectSurfaced` — how often Change 3 actually contributes.
280
281The harness doesn't produce a verdict on its own. It accumulates data
282for later offline analysis. Ship it, wait 2-3 weeks, read the log.
192283
193284## Change 4 — Content-addressed rule IDs + dedup pass
194285
……@@ -243,22 +334,24 @@ linearly with pipeline invocations.
243334- The benchmark history still shows old inflated numbers (462 → 498 →
244335 648 → 953 → 1048 → 271). Future entries will be stable.
245336
246## Deferred
337## Deferred (still)
247338
248### Cleanup of 21 remaining `unknown` rules
339### Rule-extractor's "dead rule" eviction based on hit/miss
249340
250The 108-rule unknown bucket has collapsed to 21 after Change 4's dedup
251(most of the 108 were duplicates of the same underlying unknown loops).
252They remain in the corpus as `needs-investigation` and are held back from
253injection by Change 2. A cleanup migration would drop them entirely but
254loses the audit trail. Scoped separately.
341Rules never fire-tracked. The original "live rule scoring" (Change C in
342the pre-audit plan) stayed deferred because the counterfactual problem
343(attributing a loop to any one injected rule) is genuinely hard. The
344shadow harness (Change 6) now at least captures which rules were
345injected, so a future change could tally per-rule firing frequency and
346hit-rate by correlating rule IDs in `removedByNew`/`addedByNew` with
347subsequent correction sequences in the same session. Not in scope.
255348
256### Shadow harness for real measurement
349### Correction-burst regex fix (not mine)
257350
258Still not built. The dashboard's benchmark trend is only 5 days old and
259now has a synthetic discontinuity from Change 4's collapse. Real before/
260after measurement of the changes requires either waiting 1-2 weeks for a
261clean history or building the shadow harness.
351`realtime-score.js` has an uncommitted fix for the learning-rate
352computation that collapses consecutive corrections into a single
353"burst." Not mine, left in the working tree as pre-existing work to
354avoid committing code I didn't author.
262355
263356## Measurement caveats
264357
……@@ -329,12 +422,16 @@ conversation transcript if anyone wants the reasoning in detail.
329422
330423**Changed:**
331424- `knowledge-ratchet/classify-project.js` (new, 147 LOC) — Change 1
332- `knowledge-ratchet/loop-detector.js` — imports shared classifier
333- `knowledge-ratchet/loop-briefing.js` — imports shared classifier + quality-aware injection
334- `knowledge-ratchet/rule-extractor.js` — `computeQuality` + backfill + content-addressed IDs + dedup pass
425- `knowledge-ratchet/loop-detector.js` — imports shared classifier, exports `buildFingerprint`
426- `knowledge-ratchet/loop-briefing.js` — imports shared classifier + quality-aware injection + inline cross-project matcher + shadow harness logging
427- `knowledge-ratchet/rule-extractor.js` — `computeQuality` + backfill + content-addressed IDs + dedup pass + unknown-project filter
428
429**Data added:**
430- `data/briefing-shadow.jsonl` — append-only shadow harness log (new)
335431
336432**Not changed (but modified in working tree by other work — not staged):**
337433- `realtime-score.js` — pre-existing uncommitted fix for correction-burst counting
338434- Various data files under `data/` — pipeline artifacts
339435
340**Total net: ~280 LOC across 4 files across 3 code commits.**
436**Total: ~380 LOC across 4 source files, 4 code commits
437(Change 1+2, audit doc, Change 4, Changes 3+5+6).**
loop-briefing.js +102 -0
……@@ -15,6 +15,7 @@ const { computeQuality, QUALITY_RANK } = require('./rule-extractor')
1515
1616const LOOPS_FILE = path.join(__dirname, 'data', 'loops.json')
1717const RULES_FILE = path.join(__dirname, 'data', 'learned-rules.json')
18const SHADOW_LOG = path.join(__dirname, 'data', 'briefing-shadow.jsonl')
1819
1920// classifyProject is imported from classify-project.js (shared with
2021// loop-detector.js). It always returns a non-empty string — either a
……@@ -104,6 +105,61 @@ function main () {
104105 }
105106 }
106107
108 // ─── Cross-project similar loops (Change 3) ───
109 // Surface resolved loops from OTHER projects whose filesTried overlap with
110 // this project's danger files. The goal: a rule learned in dc-terrain about
111 // MEMORY.md fumbling surfaces when working in hypercore-kb, because they're
112 // the same class of problem.
113 //
114 // Note: loop-detector's findSimilarLoops exists but its scoring expects a
115 // rich current fingerprint (≥2 file overlap to clear threshold for cross-
116 // project). At SessionStart we only have project danger-file history, which
117 // typically yields 1-file overlaps with other-project loops. So we do an
118 // inline match with a lower threshold (≥1 overlap) specific to this use
119 // case, rather than hammer findSimilarLoops's threshold to fit.
120 //
121 // Matching uses raw basenames (no extension stripping) since danger files
122 // often have meaningful extensions like .md that loop-detector's
123 // buildFingerprint preserves.
124 const dangerBasenamesSet = new Set(Object.keys(dangerFiles))
125 const crossProjectMatches = []
126 for (const l of (loopsData.loops || [])) {
127 if (l.project === project) continue
128 if (!l.resolved) continue
129 const lBasenames = (l.filesTried || []).map(f => path.basename(f))
130 const overlapFiles = lBasenames.filter(f => dangerBasenamesSet.has(f))
131 if (overlapFiles.length >= 1) {
132 crossProjectMatches.push({ loop: l, overlap: overlapFiles.length, overlapFiles })
133 }
134 }
135 crossProjectMatches.sort((a, b) => {
136 // Primary: more file overlap is better
137 if (b.overlap !== a.overlap) return b.overlap - a.overlap
138 // Tiebreak: longer loops = higher severity
139 return (b.loop.duration || 0) - (a.loop.duration || 0)
140 })
141 const topCross = crossProjectMatches.slice(0, 3)
142
143 if (topCross.length > 0) {
144 lines.push('')
145 lines.push(`CROSS-PROJECT SIMILAR LOOPS (${crossProjectMatches.length} by danger-file overlap, top 3):`)
146 for (const m of topCross) {
147 const l = m.loop
148 lines.push(` ${l.date} [${l.project}/${l.type}] ${l.duration} turns, shared: ${m.overlapFiles.slice(0, 3).join(', ')}`)
149 lines.push(` "${(l.corrections[0] || '').slice(0, 90)}"`)
150 if (l.resolution?.files?.length > 0) {
151 lines.push(` fix: ${l.resolution.files.map(f => path.basename(f)).slice(0, 3).join(', ')}`)
152 }
153 }
154 }
155
156 // Pre-declare shadow harness collectors. These are populated inside
157 // the rules-loading block below and logged at the end of main().
158 let shadowBaselineIds = []
159 let shadowNewIds = []
160 let shadowProjectRuleCount = 0
161 let shadowNeedsInvestigationCount = 0
162
107163 // Load learned rules for this project.
108164 //
109165 // Quality-aware injection (Change 2):
……@@ -137,6 +193,21 @@ function main () {
137193 const injectable = sortedRules.filter(r => r.quality !== 'needs-investigation')
138194 const needsInvestigationCount = sortedRules.length - injectable.length
139195
196 // ─── Shadow harness collection (Change 6) ───
197 // Compute the BASELINE rule set — what the pre-Change-2 briefing
198 // would have injected — so we can log the diff for longitudinal
199 // comparison. Baseline = filter by project exact match, sort by
200 // source.duration (severity proxy), take top 5. No quality filter,
201 // no cross-project, matching the pre-Change-2 behavior in order to
202 // measure what the quality filter and quality reorder changed.
203 const shadowBaselineRules = [...projectRules]
204 .sort((a, b) => (b.source?.duration || 0) - (a.source?.duration || 0))
205 .slice(0, 5)
206 shadowBaselineIds = shadowBaselineRules.map(r => r.id)
207 shadowNewIds = injectable.slice(0, 5).map(r => r.id)
208 shadowProjectRuleCount = projectRules.length
209 shadowNeedsInvestigationCount = needsInvestigationCount
210
140211 if (injectable.length > 0) {
141212 const header = needsInvestigationCount > 0
142213 ? `LEARNED RULES (${injectable.length} actionable, ${needsInvestigationCount} need investigation):`
……@@ -165,6 +236,37 @@ function main () {
165236 lines.push('═'.repeat(50))
166237 lines.push('')
167238
239 // ─── Shadow harness log (Change 6) ───
240 // Append one JSONL record capturing what this briefing injected vs
241 // what a pre-Change-2 baseline briefing would have injected. Read
242 // longitudinally to answer: "is the quality filter removing rules
243 // that would have helped?" (false negatives) and "is quality
244 // reordering surfacing rules that wouldn't have been seen?"
245 // (effective promotions). This does not block injection — it's a
246 // side-effect log for measurement.
247 try {
248 const overlap = shadowNewIds.filter(id => shadowBaselineIds.includes(id))
249 const removedByNew = shadowBaselineIds.filter(id => !shadowNewIds.includes(id))
250 const addedByNew = shadowNewIds.filter(id => !shadowBaselineIds.includes(id))
251 const shadowEntry = {
252 timestamp: new Date().toISOString(),
253 project,
254 cwd,
255 counts: {
256 projectRules: shadowProjectRuleCount,
257 injected: shadowNewIds.length,
258 baseline: shadowBaselineIds.length,
259 needsInvestigation: shadowNeedsInvestigationCount,
260 crossProjectSurfaced: topCross.length,
261 crossProjectTotal: crossProjectMatches.length
262 },
263 overlap: overlap.length,
264 removedByNew, // rules baseline would have shown but we suppressed
265 addedByNew // rules we surfaced by quality reordering
266 }
267 fs.appendFileSync(SHADOW_LOG, JSON.stringify(shadowEntry) + '\n')
268 } catch {}
269
168270 // Mark as delivered so we don't repeat
169271 fs.writeFileSync(BRIEFING_FLAG, new Date().toISOString())
170272
loop-detector.js +1 -1
……@@ -361,6 +361,6 @@ function main () {
361361 console.log('═'.repeat(60))
362362}
363363
364module.exports = { extractLoops, findSimilarLoops }
364module.exports = { extractLoops, findSimilarLoops, buildFingerprint }
365365
366366if (require.main === module) main()
rule-extractor.js +19 -7
……@@ -280,17 +280,28 @@ function main () {
280280
281281 const deduped = [...Array.from(byFingerprint.values()), ...noFingerprint]
282282
283 // Backfill quality on every rule in the deduped output. This catches
283 // ─── Drop unknown-project rules (Change 5) ───
284 // Rules with trigger.project === 'unknown' can never fire at runtime.
285 // loop-briefing.js matches rules by specific project name, and
286 // classify-project.js's walker either finds a .git (returning a real
287 // project name) or fails completely. No live session ever classifies
288 // as 'unknown', so these rules are unreachable by design. Dropping them
289 // is a one-shot migration (catches pre-Change-4 stale unknowns) AND a
290 // permanent filter (prevents future unknown rules from accumulating).
291 const filtered = deduped.filter(r => r.trigger.project !== 'unknown')
292 const unknownDropped = deduped.length - filtered.length
293
294 // Backfill quality on every rule in the filtered output. This catches
284295 // pre-Change 2 rules that lack a .quality field, and also refreshes
285296 // the tag for any rule whose antipattern or fixFiles changed since
286297 // the last extraction.
287 for (const rule of deduped) {
298 for (const rule of filtered) {
288299 rule.quality = computeQuality(rule)
289300 }
290301
291302 // Group by project
292303 const byProject = {}
293 for (const rule of deduped) {
304 for (const rule of filtered) {
294305 const proj = rule.trigger.project
295306 if (!byProject[proj]) byProject[proj] = []
296307 byProject[proj].push(rule)
……@@ -304,7 +315,8 @@ function main () {
304315 console.log(` New rules added: ${added}`)
305316 console.log(` Pre-dedup total: ${merged.length}`)
306317 console.log(` Duplicates dropped: ${duplicatesDropped}`)
307 console.log(` Total rules: ${deduped.length}`)
318 console.log(` Unknown-project rules dropped: ${unknownDropped}`)
319 console.log(` Total rules: ${filtered.length}`)
308320
309321 console.log('\n BY PROJECT')
310322 console.log(' ' + '-'.repeat(56))
……@@ -322,7 +334,7 @@ function main () {
322334 }
323335
324336 // Show top rules by severity (longest loops)
325 const bySeverity = [...deduped].sort((a, b) => b.source.duration - a.source.duration)
337 const bySeverity = [...filtered].sort((a, b) => b.source.duration - a.source.duration)
326338
327339 console.log('\n HIGHEST SEVERITY RULES (longest loops)')
328340 console.log(' ' + '-'.repeat(56))
……@@ -334,8 +346,8 @@ function main () {
334346 // Save
335347 const report = {
336348 generated: new Date().toISOString(),
337 totalRules: deduped.length,
338 rules: deduped,
349 totalRules: filtered.length,
350 rules: filtered,
339351 }
340352 fs.writeFileSync(RULES_FILE, JSON.stringify(report, null, 2))
341353 console.log(`\nSaved: ${RULES_FILE}`)