OverviewFilesCommitsIssuesDocsReleasesPeers

knowledge-ratchet commits / eeada29

AtomSpace integration + trusted-sensor scoring
Checkpoint of work that was sitting uncommitted in the tree. Not authored in this session -- committed as-is so it is not lost, and so it pairs with the atomspace/ layer tracked in 6382056, which these files call into. pipeline.js step 10: optional AtomSpace ingest, skips silently when CogServer is not running preflight-review.js findSharedModule + structural risk queries against the AtomSpace; skips test/example/demo paths and gates on HIGH danger only (medium hit 18/28 subsystems and blocked routine work) fuse-signals.js TRUSTED_SENSORS top-5 subset; per-turn score (length-independent) and trusted score as the primary quality signal realtime-score.js PROCEED_PATTERNS (weak-positive) and NEW_PROBLEM_PATTERNS (pivot detection, resets the stuck-loop counter) dashboard.js per-turn / trusted / correction-burst metrics, /graph D3 page and /graph.json live AtomSpace slice benchmark-ratchet.js cwdToDomain + per-domain novel-rate tracking data/patterns.json, data/principles.json, data/validation-report.json are regeneration churn only -- extractedAt/validatedAt restamped, no content change. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
eeada29342766df5e5ec652b9228c0ecacc37186
by plankatron <fleeky@gmail.com> · 2026-08-12T02:28:54.000Z

Changed files (9) +736 -111

benchmark-ratchet.js +116 -8
……@@ -431,16 +431,32 @@ function benchmarkR3 () {
431431// R2: Novel vs Repeat Corrections
432432// ═══════════════════════════════════════════════════════════
433433
434// Infer domain from a cwd path.
435function cwdToDomain (cwd) {
436 if (!cwd) return 'unknown'
437 if (cwd.includes('/code/godot/')) return 'godot'
438 if (cwd.includes('/hyper-fleeky/')) return 'p2p-stack'
439 if (cwd.includes('knowledge-ratchet')) return 'ratchet'
440 if (cwd.includes('hypercore-kb')) return 'p2p-stack'
441 return 'unknown'
442}
443
434444function benchmarkR2 () {
435445 const detail = loadJSON(path.join(DATA_DIR, 'fused-turns-detail.json'))
436446 if (!detail) { console.log(' No detail data.'); return null }
437447
438 // Collect all correction texts chronologically
448 // Collect all correction texts chronologically, tagged with domain
439449 const allCorrections = []
440450 for (const sess of detail.sessions) {
451 const domain = cwdToDomain(sess.cwd)
441452 for (const turn of (sess.turns || [])) {
442453 if (turn.isCorrection && turn.text) {
443 allCorrections.push({ text: turn.text, date: sess.date, session: sess.sessionId })
454 allCorrections.push({
455 text: turn.text,
456 date: sess.date,
457 session: sess.sessionId,
458 domain
459 })
444460 }
445461 }
446462 }
……@@ -464,6 +480,8 @@ function benchmarkR2 () {
464480 const novelByWeek = {}
465481 const repeatByWeek = {}
466482 const pastSets = []
483 // Per-domain tracking
484 const byDomain = {}
467485
468486 for (const corr of allCorrections) {
469487 const words = wordSet(corr.text)
……@@ -486,6 +504,23 @@ function benchmarkR2 () {
486504 novelByWeek[week] = (novelByWeek[week] || 0) + 1
487505 }
488506 pastSets.push(words)
507
508 // Per-domain aggregation
509 if (!byDomain[corr.domain]) byDomain[corr.domain] = { novel: 0, repeat: 0 }
510 if (isRepeat) byDomain[corr.domain].repeat++
511 else byDomain[corr.domain].novel++
512 }
513
514 // Compute per-domain novel rate
515 const perDomain = {}
516 for (const [d, stats] of Object.entries(byDomain)) {
517 const total = stats.novel + stats.repeat
518 perDomain[d] = {
519 novel: stats.novel,
520 repeat: stats.repeat,
521 total,
522 novelRate: total > 0 ? stats.novel / total : null
523 }
489524 }
490525
491526 const allWeeks = [...new Set([...Object.keys(novelByWeek), ...Object.keys(repeatByWeek)])].sort()
……@@ -536,7 +571,16 @@ function benchmarkR2 () {
536571 console.log(` Delta: ${delta >= 0 ? '+' : ''}${(delta * 100).toFixed(0)}pp`)
537572 }
538573
539 return { novel, repeat, novelRate, weekCount: allWeeks.length }
574 // Print per-domain breakdown
575 console.log('\n BY DOMAIN (novel rate — higher means ratchet preventing repeats)')
576 console.log(' ' + '-'.repeat(60))
577 for (const [d, stats] of Object.entries(perDomain).sort((a, b) => b[1].total - a[1].total)) {
578 if (stats.total < 5) continue
579 const pct = (stats.novelRate * 100).toFixed(1)
580 console.log(` ${d.padEnd(15)} ${pct}% novel (${stats.novel}/${stats.total})`)
581 }
582
583 return { novel, repeat, novelRate, weekCount: allWeeks.length, perDomain }
540584}
541585
542586// ═══════════════════════════════════════════════════════════
……@@ -547,9 +591,20 @@ function benchmarkR4 () {
547591 const loops = loadJSON(path.join(DATA_DIR, 'loops.json'))
548592 if (!loops) { console.log(' No loops data.'); return null }
549593
550 const resolved = loops.loops.filter(l => l.resolved && l.date)
594 // Compute resolution RATE — resolved vs total (catches stuck loops)
595 const allLoops = loops.loops.filter(l => l.date)
596 const resolved = allLoops.filter(l => l.resolved)
597 const unresolvedCount = allLoops.length - resolved.length
598 const resolutionRate = allLoops.length > 0 ? resolved.length / allLoops.length : 0
599
551600 if (resolved.length < 5) { console.log(' Not enough resolved loops.'); return null }
552601
602 // Duration percentiles (quick fixes vs sagas)
603 const sortedDurations = resolved.map(l => l.duration).sort((a, b) => a - b)
604 const p50 = sortedDurations[Math.floor(sortedDurations.length * 0.5)]
605 const p90 = sortedDurations[Math.floor(sortedDurations.length * 0.9)]
606 const longHaulRate = sortedDurations.filter(d => d > 10).length / sortedDurations.length
607
553608 // Group by type and week, track avg duration
554609 const byTypeWeek = {}
555610 for (const l of resolved) {
……@@ -574,10 +629,14 @@ function benchmarkR4 () {
574629 const allWeeks = Object.keys(overallWeekly).sort()
575630
576631 console.log('═'.repeat(60))
577 console.log(' R4: LOOP RESOLUTION SPEED')
632 console.log(' R4: LOOP RESOLUTION')
578633 console.log('═'.repeat(60))
579 console.log(` Resolved loops: ${resolved.length}`)
580 console.log(` Avg resolution: ${(resolved.reduce((s, l) => s + l.duration, 0) / resolved.length).toFixed(1)} turns`)
634 console.log(` Total loops: ${allLoops.length} (resolved: ${resolved.length}, stuck: ${unresolvedCount})`)
635 console.log(` Resolution rate: ${(resolutionRate * 100).toFixed(1)}% ${resolutionRate >= 0.8 ? '✓' : resolutionRate >= 0.6 ? '~' : '✗'}`)
636 console.log(` Avg duration: ${(resolved.reduce((s, l) => s + l.duration, 0) / resolved.length).toFixed(1)} turns`)
637 console.log(` P50 duration: ${p50} turns (half resolved this fast or faster)`)
638 console.log(` P90 duration: ${p90} turns (tail is here — long sagas)`)
639 console.log(` Long-haul rate: ${(longHaulRate * 100).toFixed(1)}% (fraction of loops >10 turns)`)
581640
582641 console.log('\n WEEKLY AVG RESOLUTION (turns to fix)')
583642 console.log(' ' + '-'.repeat(60))
……@@ -629,7 +688,17 @@ function benchmarkR4 () {
629688 console.log(` ${arrow} ${type.padEnd(20)} ${firstAvg.toFixed(1)} → ${secondAvg.toFixed(1)} turns (${delta >= 0 ? '+' : ''}${delta.toFixed(1)})`)
630689 }
631690
632 return { avgResolution: resolved.reduce((s, l) => s + l.duration, 0) / resolved.length, weekCount: allWeeks.length }
691 return {
692 avgResolution: resolved.reduce((s, l) => s + l.duration, 0) / resolved.length,
693 resolutionRate,
694 p50,
695 p90,
696 longHaulRate,
697 totalLoops: allLoops.length,
698 resolvedCount: resolved.length,
699 unresolvedCount,
700 weekCount: allWeeks.length
701 }
633702}
634703
635704// ─── Main ───
……@@ -688,8 +757,23 @@ function main () {
688757 timestamp: new Date().toISOString(),
689758 novelRate: results.r2?.novelRate ?? null,
690759 repeatRate: results.r2 ? 1 - results.r2.novelRate : null,
760 // Per-domain novel rates (R2 broken down by domain)
761 novelRateByDomain: results.r2?.perDomain ?? null,
691762 density: results.r3?.overallDensity ?? null,
763 // R4 enhanced fields
692764 avgResolution: results.r4?.avgResolution ?? null,
765 resolutionRate: results.r4?.resolutionRate ?? null,
766 p50Duration: results.r4?.p50 ?? null,
767 p90Duration: results.r4?.p90 ?? null,
768 longHaulRate: results.r4?.longHaulRate ?? null,
769 totalLoopsAll: results.r4?.totalLoops ?? null,
770 stuckLoops: results.r4?.unresolvedCount ?? null,
771 // Trusted quality (from fused-scores latest)
772 trustedScore: null,
773 perTurnScore: null,
774 avgTurns: null,
775 saturationLevels: null,
776 // Legacy fields
693777 totalLoops: results.r4?.weekCount ?? null,
694778 sessions: results.r3?.weekCount ?? null,
695779 rulesCount: null,
……@@ -701,6 +785,30 @@ function main () {
701785 try { snapshot.rulesCount = JSON.parse(fs.readFileSync(rulesFile, 'utf8')).totalRules } catch {}
702786 }
703787
788 // Load latest trustedScore/perTurnScore from fused-scores weekly
789 const fusedFile = path.join(DATA_DIR, 'fused-scores.json')
790 if (fs.existsSync(fusedFile)) {
791 try {
792 const fused = JSON.parse(fs.readFileSync(fusedFile, 'utf8'))
793 const weeks = Object.entries(fused.weekly || {}).sort()
794 if (weeks.length > 0) {
795 const latest = weeks[weeks.length - 1][1]
796 snapshot.trustedScore = latest.avgTrustedScore ?? null
797 snapshot.perTurnScore = latest.avgPerTurnScore ?? null
798 snapshot.avgTurns = latest.avgTurns ?? null
799 }
800 } catch {}
801 }
802
803 // Load saturation distribution from advanced metrics
804 const advFile = path.join(DATA_DIR, 'advanced-metrics.json')
805 if (fs.existsSync(advFile)) {
806 try {
807 const adv = JSON.parse(fs.readFileSync(advFile, 'utf8'))
808 snapshot.saturationLevels = adv.aggregate?.saturation ?? null
809 } catch {}
810 }
811
704812 // Dedup by date — only one entry per day
705813 history = history.filter(h => h.date !== snapshot.date)
706814 history.push(snapshot)
dashboard.js +161 -80
……@@ -61,9 +61,32 @@ function generate () {
6161 return acc
6262 }, { turns: 0, corr: 0 })
6363
64 // Find last session date in this week for range labels
65 const weekSessions = (fused.sessions || []).filter(s => {
66 const d = new Date(s.date)
67 d.setDate(d.getDate() - d.getDay())
68 return d.toISOString().slice(0, 10) === week
69 })
70 const lastDate = weekSessions.length > 0
71 ? weekSessions.sort((a, b) => (a.date || '').localeCompare(b.date || ''))[weekSessions.length - 1].date
72 : week
73 const weekLabel = lastDate !== week
74 ? `${week.slice(5)}…${lastDate.slice(8)}`
75 : week.slice(5)
76
6477 weeklyData.push({
6578 week,
79 weekLabel,
6680 score: data.avgScore,
81 // Per-turn score: quality independent of session length
82 perTurnScore: data.avgPerTurnScore || data.avgScore,
83 // Trusted score: top-5 sensors only — PRIMARY quality signal
84 trustedScore: data.avgTrustedScore,
85 // Correction burst: longest streak of consecutive corrections (lower = healthier)
86 avgCorrectionBurst: data.avgCorrectionBurst,
87 maxCorrectionBurst: data.maxCorrectionBurst || 0,
88 avgTurns: data.avgTurns || 0,
89 avgDensity: data.avgDensity || null,
6790 sessions: data.sessions,
6891 corrections: data.corrections,
6992 approvals: data.approvals,
……@@ -234,8 +257,9 @@ function generate () {
234257</style>
235258</head>
236259<body>
237<h1>Knowledge Ratchet</h1>
238<h2>Self-improving AI quality system — tracks corrections, detects failure loops, auto-generates prevention rules</h2>
260<h1>Knowledge Ratchet <a href="graph" style="font-size: 14px; color: #58a6ff; text-decoration: none; margin-left: 16px;">→ AtomSpace Graph</a></h1>
261<h2>Self-improving AI quality system — tracks corrections, detects failure loops, auto-generates prevention rules
262<br>Data through: ${benchmarkHistory.length > 0 ? benchmarkHistory[benchmarkHistory.length - 1].date : 'n/a'} | ${sessionData.length} sessions</h2>
239263
240264${liveSession && liveSession.turns > 0 ? `
241265<div class="card" style="border-color: ${liveBorder}; margin-bottom: 16px;">
……@@ -274,10 +298,97 @@ ${liveSession && liveSession.turns > 0 ? `
274298 </div>
275299</div>
276300
277<!-- Weekly Fused Score Trend -->
301<!-- Daily Ratchet Trends (primary) -->
302${benchmarkHistory.length >= 1 ? `
303<h3>Daily Ratchet Trends</h3>
304<div class="grid">
305 <div class="card">
306 <div class="card-title">Novel Correction Rate (higher = better)</div>
307 ${renderLineChart(
308 benchmarkHistory.filter(h => h.novelRate != null).map(h => h.novelRate),
309 benchmarkHistory.filter(h => h.novelRate != null).map(h => h.date),
310 { height: 120, color: '#3fb950', label: 'Novel Rate', format: 'pct' }
311 )}
312 </div>
313 <div class="card">
314 <div class="card-title">Correction Density (lower = better)</div>
315 ${renderLineChart(
316 benchmarkHistory.filter(h => h.density != null).map(h => h.density),
317 benchmarkHistory.filter(h => h.density != null).map(h => h.date),
318 { height: 120, color: '#f85149', label: 'Density', format: 'pct' }
319 )}
320 </div>
321 <div class="card">
322 <div class="card-title">Loop Resolution Speed (lower = better)</div>
323 ${renderLineChart(
324 benchmarkHistory.filter(h => h.avgResolution != null).map(h => h.avgResolution),
325 benchmarkHistory.filter(h => h.avgResolution != null).map(h => h.date),
326 { height: 120, color: '#d29922', label: 'Turns to Resolve', format: 'auto' }
327 )}
328 </div>
329 <div class="card">
330 <div class="card-title">Learned Rules (higher = more knowledge)</div>
331 ${renderLineChart(
332 benchmarkHistory.filter(h => h.rulesCount != null).map(h => h.rulesCount),
333 benchmarkHistory.filter(h => h.rulesCount != null).map(h => h.date),
334 { height: 120, color: '#58a6ff', label: 'Rules', format: 'int' }
335 )}
336 </div>
337</div>
338
339<div class="card" style="margin-top:16px; margin-bottom:16px">
340 <div class="card-title">Ratchet Scorecard</div>
341 <table>
342 <tr><th>Metric</th><th>Current</th><th>Target</th><th>Status</th></tr>
343 <tr>
344 <td>Novel correction rate</td>
345 <td>${benchmarkHistory.length > 0 && benchmarkHistory[benchmarkHistory.length-1].novelRate != null ? (benchmarkHistory[benchmarkHistory.length-1].novelRate * 100).toFixed(0) + '%' : 'n/a'}</td>
346 <td>&gt;90%</td>
347 <td>${benchmarkHistory.length > 0 && benchmarkHistory[benchmarkHistory.length-1].novelRate > 0.9 ? '<span class="green">&#10003;</span>' : benchmarkHistory[benchmarkHistory.length-1]?.novelRate > 0.8 ? '<span class="yellow">~</span>' : '<span class="red">&#10007;</span>'}</td>
348 </tr>
349 <tr>
350 <td>Correction density</td>
351 <td>${benchmarkHistory.length > 0 && benchmarkHistory[benchmarkHistory.length-1].density != null ? (benchmarkHistory[benchmarkHistory.length-1].density * 100).toFixed(1) + '%' : 'n/a'}</td>
352 <td>&lt;15%</td>
353 <td>${benchmarkHistory.length > 0 && benchmarkHistory[benchmarkHistory.length-1].density < 0.15 ? '<span class="green">&#10003;</span>' : benchmarkHistory[benchmarkHistory.length-1]?.density < 0.25 ? '<span class="yellow">~</span>' : '<span class="red">&#10007;</span>'}</td>
354 </tr>
355 <tr>
356 <td>Avg loop resolution</td>
357 <td>${benchmarkHistory.length > 0 && benchmarkHistory[benchmarkHistory.length-1].avgResolution != null ? benchmarkHistory[benchmarkHistory.length-1].avgResolution.toFixed(1) + ' turns' : 'n/a'}</td>
358 <td>&lt;6 turns</td>
359 <td>${benchmarkHistory.length > 0 && benchmarkHistory[benchmarkHistory.length-1].avgResolution < 6 ? '<span class="green">&#10003;</span>' : benchmarkHistory[benchmarkHistory.length-1]?.avgResolution < 10 ? '<span class="yellow">~</span>' : '<span class="red">&#10007;</span>'}</td>
360 </tr>
361 <tr>
362 <td>Learned rules</td>
363 <td>${learnedRules?.totalRules || 0}</td>
364 <td>growing</td>
365 <td><span class="blue">${learnedRules?.totalRules > 0 ? '&#10003;' : '~'}</span></td>
366 </tr>
367 </table>
368</div>
369` : ''}
370
371<!-- Primary quality metric: Trusted score (top-5 sensors, noise-free) -->
372<div class="chart-container">
373 <div class="card-title">★ Trusted Score (PRIMARY) — top-5 sensors only, ratchet's real quality signal</div>
374 ${renderLineChart(weeklyData.map(w => w.trustedScore || 0), weeklyData.map(w => w.weekLabel), { height: 140, color: '#3fb950', label: 'Trusted Score', format: 'auto' })}
375</div>
376
377<!-- Secondary metrics for debugging -->
378<div class="chart-container">
379 <div class="card-title">Per-Turn Score (all 16 sensors) — includes noise; see audit for caveats</div>
380 ${renderLineChart(weeklyData.map(w => w.perTurnScore), weeklyData.map(w => w.weekLabel), { height: 100, color: '#8b949e', label: 'Per-Turn Score', format: 'auto' })}
381</div>
382
278383<div class="chart-container">
279 <div class="card-title">Weekly Fused Score (higher = better)</div>
280 ${renderLineChart(weeklyData.map(w => w.score), weeklyData.map(w => w.week), { height: 140, color: '#58a6ff', label: 'Fused Score', format: 'auto' })}
384 <div class="card-title">Avg Correction Burst — longest run of consecutive corrections per session (lower = healthier; 1 = isolated mistakes, 2+ = stuck patterns)</div>
385 ${renderLineChart(weeklyData.map(w => w.avgCorrectionBurst || 0), weeklyData.map(w => w.weekLabel), { height: 100, color: '#f0883e', label: 'Avg Burst', format: 'auto' })}
386</div>
387
388<!-- Session Length: ambition, not quality -->
389<div class="chart-container">
390 <div class="card-title">Avg Turns Per Session — ambition level (not a quality metric)</div>
391 ${renderLineChart(weeklyData.map(w => w.avgTurns), weeklyData.map(w => w.weekLabel), { height: 100, color: '#bc8cff', label: 'Avg Turns', format: 'int' })}
281392</div>
282393
283394<!-- Weekly Correction Density -->
……@@ -334,7 +445,7 @@ ${Object.entries(dangerMap).map(([proj, subs]) => {
334445
335446<!-- Sensor Weights -->
336447<h3>Sensor Weights</h3>
337<p class="dim" style="margin-bottom:8px">The system uses 16 sensors to score each turn. Weights auto-adjust based on which sensors best predict corrections. Higher weight = more influence on the fused score.</p>
448<p class="dim" style="margin-bottom:8px">The system uses 16 sensors to score each turn. Weights auto-adjust based on which sensors best predict corrections. Higher weight = more influence on per-turn scoring.</p>
338449<table>
339450 <tr><th>Sensor</th><th>Weight</th><th>Bar</th></tr>
340451 ${fused?.sensorWeights ? Object.entries(fused.sensorWeights).sort((a, b) => b[1] - a[1]).map(([name, weight]) => {
……@@ -347,76 +458,8 @@ ${Object.entries(dangerMap).map(([proj, subs]) => {
347458 }).join('\n ') : '<tr><td colspan="3" class="dim">No weight data</td></tr>'}
348459</table>
349460
350<!-- Ratchet Intelligence Trends -->
351<h3>Intelligence Ratchet Trends</h3>
352<p class="dim" style="margin-bottom:8px">These lines should trend in the right direction over time. If they don't, the ratchet isn't working.</p>
353${benchmarkHistory.length >= 1 ? `
354<div class="grid">
355 <div class="card">
356 <div class="card-title">Novel Correction Rate (higher = better)</div>
357 ${renderLineChart(
358 benchmarkHistory.filter(h => h.novelRate != null).map(h => h.novelRate),
359 benchmarkHistory.filter(h => h.novelRate != null).map(h => h.date),
360 { height: 120, color: '#3fb950', label: 'Novel Rate', format: 'pct' }
361 )}
362 </div>
363 <div class="card">
364 <div class="card-title">Correction Density (lower = better)</div>
365 ${renderLineChart(
366 benchmarkHistory.filter(h => h.density != null).map(h => h.density),
367 benchmarkHistory.filter(h => h.density != null).map(h => h.date),
368 { height: 120, color: '#f85149', label: 'Density', format: 'pct' }
369 )}
370 </div>
371 <div class="card">
372 <div class="card-title">Loop Resolution Speed (lower = better)</div>
373 ${renderLineChart(
374 benchmarkHistory.filter(h => h.avgResolution != null).map(h => h.avgResolution),
375 benchmarkHistory.filter(h => h.avgResolution != null).map(h => h.date),
376 { height: 120, color: '#d29922', label: 'Turns to Resolve', format: 'auto' }
377 )}
378 </div>
379 <div class="card">
380 <div class="card-title">Learned Rules (higher = more knowledge)</div>
381 ${renderLineChart(
382 benchmarkHistory.filter(h => h.rulesCount != null).map(h => h.rulesCount),
383 benchmarkHistory.filter(h => h.rulesCount != null).map(h => h.date),
384 { height: 120, color: '#58a6ff', label: 'Rules', format: 'int' }
385 )}
386 </div>
387</div>
388
389<div class="card" style="margin-top:16px">
390 <div class="card-title">Ratchet Scorecard — Green = target met, Yellow = working on it, Red = failing</div>
391 <table>
392 <tr><th>Metric</th><th>Current</th><th>Target</th><th>Status</th></tr>
393 <tr>
394 <td>Novel correction rate</td>
395 <td>${benchmarkHistory.length > 0 && benchmarkHistory[benchmarkHistory.length-1].novelRate != null ? (benchmarkHistory[benchmarkHistory.length-1].novelRate * 100).toFixed(0) + '%' : 'n/a'}</td>
396 <td>&gt;90%</td>
397 <td>${benchmarkHistory.length > 0 && benchmarkHistory[benchmarkHistory.length-1].novelRate > 0.9 ? '<span class="green">✓</span>' : benchmarkHistory[benchmarkHistory.length-1]?.novelRate > 0.8 ? '<span class="yellow">~</span>' : '<span class="red">✗</span>'}</td>
398 </tr>
399 <tr>
400 <td>Correction density</td>
401 <td>${benchmarkHistory.length > 0 && benchmarkHistory[benchmarkHistory.length-1].density != null ? (benchmarkHistory[benchmarkHistory.length-1].density * 100).toFixed(1) + '%' : 'n/a'}</td>
402 <td>&lt;15%</td>
403 <td>${benchmarkHistory.length > 0 && benchmarkHistory[benchmarkHistory.length-1].density < 0.15 ? '<span class="green">✓</span>' : benchmarkHistory[benchmarkHistory.length-1]?.density < 0.25 ? '<span class="yellow">~</span>' : '<span class="red">✗</span>'}</td>
404 </tr>
405 <tr>
406 <td>Avg loop resolution</td>
407 <td>${benchmarkHistory.length > 0 && benchmarkHistory[benchmarkHistory.length-1].avgResolution != null ? benchmarkHistory[benchmarkHistory.length-1].avgResolution.toFixed(1) + ' turns' : 'n/a'}</td>
408 <td>&lt;6 turns</td>
409 <td>${benchmarkHistory.length > 0 && benchmarkHistory[benchmarkHistory.length-1].avgResolution < 6 ? '<span class="green">✓</span>' : benchmarkHistory[benchmarkHistory.length-1]?.avgResolution < 10 ? '<span class="yellow">~</span>' : '<span class="red">✗</span>'}</td>
410 </tr>
411 <tr>
412 <td>Learned rules</td>
413 <td>${learnedRules?.totalRules || 0}</td>
414 <td>growing</td>
415 <td><span class="blue">${learnedRules?.totalRules > 0 ? '✓' : '~'}</span></td>
416 </tr>
417 </table>
418</div>
419` : '<div class="card"><div class="dim">Not enough benchmark history yet. Run a few sessions to see trends.</div></div>'}
461<!-- Ratchet Intelligence Trends (formerly buried at bottom) -->
462<div class="dim" style="margin-bottom: 4px; font-size: 11px;">Remaining sections: sensor weights, loops by project, subsystem danger map are below.</div>
420463
421464<div class="timestamp">
422465 Dashboard generated: ${new Date().toISOString()}<br>
……@@ -432,17 +475,55 @@ ${benchmarkHistory.length >= 1 ? `
432475
433476function serve (port) {
434477 const http = require('http')
478 const url = require('url')
435479 generate() // initial generation
436 http.createServer((req, res) => {
480 http.createServer(async (req, res) => {
481 const parsed = url.parse(req.url, true)
437482 try {
483 // /graph — render the D3 graph visualization page
484 if (parsed.pathname === '/graph' || parsed.pathname === '/graph.html') {
485 const graphHtml = fs.readFileSync(path.join(__dirname, 'atomspace/graph.html'), 'utf8')
486 res.writeHead(200, { 'Content-Type': 'text/html' })
487 res.end(graphHtml)
488 return
489 }
490
491 // /graph.json — live AtomSpace slice as JSON
492 if (parsed.pathname === '/graph.json') {
493 const { exportGraph } = require('./atomspace/graph-export')
494 const opts = {}
495 if (parsed.query.module) opts.module = parsed.query.module
496 if (parsed.query.minWisdom) opts.minWisdom = parseFloat(parsed.query.minWisdom)
497 if (parsed.query.limit) opts.limit = parseInt(parsed.query.limit)
498 const data = await exportGraph(opts)
499 res.writeHead(200, { 'Content-Type': 'application/json' })
500 res.end(JSON.stringify(data))
501 return
502 }
503
504 // /fires.json — recent rule firings for live dashboard
505 if (parsed.pathname === '/fires.json') {
506 const { readRecent, readSince } = require('./atomspace/fire-log')
507 const since = parsed.query.since ? parseInt(parsed.query.since) : null
508 const limit = parsed.query.limit ? parseInt(parsed.query.limit) : 50
509 const fires = since ? readSince(since) : readRecent(limit)
510 res.writeHead(200, { 'Content-Type': 'application/json' })
511 res.end(JSON.stringify({ fires, count: fires.length, at: Date.now() }))
512 return
513 }
514
515 // Default: serve the regenerated dashboard
438516 generate() // regenerate on each request — always fresh data
439517 res.writeHead(200, { 'Content-Type': 'text/html' })
440518 res.end(fs.readFileSync(OUTPUT_FILE, 'utf8'))
441519 } catch (err) {
442520 res.writeHead(500, { 'Content-Type': 'text/plain' })
443 res.end('Dashboard generation failed: ' + err.message)
521 res.end('Dashboard error: ' + err.message)
444522 }
445 }).listen(port, () => console.log(`Ratchet dashboard: http://localhost:${port}`))
523 }).listen(port, () => {
524 console.log(`Ratchet dashboard: http://localhost:${port}`)
525 console.log(`AtomSpace graph: http://localhost:${port}/graph`)
526 })
446527}
447528
448529// ─── CLI ───
……@@ -558,7 +639,7 @@ function renderBarChart (weeklyData, opts = {}) {
558639 <rect x="${x}" y="${h - padB - apprH}" width="${barW}" height="${apprH}" fill="#238636" opacity="0.7"/>
559640 <text x="${x - barW/2}" y="${h - padB - corrH - 3}" fill="#da3633" font-size="8" text-anchor="middle">${d.corrections}</text>
560641 <text x="${x + barW/2}" y="${h - padB - apprH - 3}" fill="#238636" font-size="8" text-anchor="middle">${d.approvals}</text>
561 <text x="${x}" y="${h - 5}" fill="#484f58" font-size="9" text-anchor="middle">${d.week.slice(5)}</text>
642 <text x="${x}" y="${h - 5}" fill="#484f58" font-size="9" text-anchor="middle">${d.weekLabel || d.week.slice(5)}</text>
562643 `
563644 }).join('')}
564645 <text x="${padL}" y="14" fill="#da3633" font-size="11">■ corrections</text>
data/patterns.json +5 -5
……@@ -77,7 +77,7 @@
7777 "During the pear-git-v2 session (2026-03-30), wrote 30+ commits of code without ever successfully completing a Sign In in a browser. Each \"fix\" added complexity on top of unverified changes, creating a compounding mess. curl testing is not sufficient for browser code.",
7878 "During the skinned BLAS session, we checked out commit `470ee20948` but scons said \"up to date\" and the binary still contained `6e725d14c` (a different commit). The user tested the wrong binary multiple times, wasting significant time."
7979 ],
80 "extractedAt": "2026-04-07T01:40:34.854Z"
80 "extractedAt": "2026-04-07T03:11:31.383Z"
8181 },
8282 {
8383 "topic": "deployment",
……@@ -163,7 +163,7 @@
163163 "During the dc-terrain mountain generator work (2026-03-19), 8+ \"fix\" commits were made without understanding the root cause. Each guess introduced new problems. The actual bug (missing mountain_pipelines_ declaration + wrong shader binding layout) could have been found in 10 minutes by diffing the working noise shader composition vs the broken mountain composition.",
164164 "User has startup scripts (`/home/fleeky/scrips/` on stortron) that set the correct port, prefix, tmux window, and flags. Starting manually with wrong args breaks the reverse proxy (503 Service Unavailable) and wastes time debugging."
165165 ],
166 "extractedAt": "2026-04-07T01:40:34.854Z"
166 "extractedAt": "2026-04-07T03:11:31.383Z"
167167 },
168168 {
169169 "topic": "discipline",
……@@ -249,7 +249,7 @@
249249 "User has startup scripts (`/home/fleeky/scrips/` on stortron) that set the correct port, prefix, tmux window, and flags. Starting manually with wrong args breaks the reverse proxy (503 Service Unavailable) and wastes time debugging.",
250250 "During the skinned BLAS session, we checked out commit `470ee20948` but scons said \"up to date\" and the binary still contained `6e725d14c` (a different commit). The user tested the wrong binary multiple times, wasting significant time."
251251 ],
252 "extractedAt": "2026-04-07T01:40:34.854Z"
252 "extractedAt": "2026-04-07T03:11:31.383Z"
253253 },
254254 {
255255 "topic": "system-knowledge",
……@@ -323,7 +323,7 @@
323323 "User has startup scripts (`/home/fleeky/scrips/` on stortron) that set the correct port, prefix, tmux window, and flags. Starting manually with wrong args breaks the reverse proxy (503 Service Unavailable) and wastes time debugging.",
324324 "During the skinned BLAS session, we checked out commit `470ee20948` but scons said \"up to date\" and the binary still contained `6e725d14c` (a different commit). The user tested the wrong binary multiple times, wasting significant time."
325325 ],
326 "extractedAt": "2026-04-07T01:40:34.854Z"
326 "extractedAt": "2026-04-07T03:11:31.383Z"
327327 },
328328 {
329329 "topic": "persistence",
……@@ -361,6 +361,6 @@
361361 "rsync + git clean deleted .pear-git-updater.json on stortron, breaking the entire self-update chain. Manual copies also pollute the git working tree.",
362362 "During the skinned BLAS session, we checked out commit `470ee20948` but scons said \"up to date\" and the binary still contained `6e725d14c` (a different commit). The user tested the wrong binary multiple times, wasting significant time."
363363 ],
364 "extractedAt": "2026-04-07T01:40:34.854Z"
364 "extractedAt": "2026-04-07T03:11:31.383Z"
365365 }
366366]
data/principles.json +5 -5
……@@ -31,7 +31,7 @@
3131 "verify-binary-matches-source"
3232 ],
3333 "factCount": 23,
34 "validatedAt": "2026-04-07T01:40:34.855Z",
34 "validatedAt": "2026-04-07T03:11:31.384Z",
3535 "contradictions": [],
3636 "overrideCount": 0,
3737 "status": "active"
……@@ -70,7 +70,7 @@
7070 "use-service-scripts"
7171 ],
7272 "factCount": 25,
73 "validatedAt": "2026-04-07T01:40:34.855Z",
73 "validatedAt": "2026-04-07T03:11:31.384Z",
7474 "contradictions": [],
7575 "overrideCount": 0,
7676 "status": "active"
……@@ -109,7 +109,7 @@
109109 "verify-binary-matches-source"
110110 ],
111111 "factCount": 25,
112 "validatedAt": "2026-04-07T01:40:34.855Z",
112 "validatedAt": "2026-04-07T03:11:31.384Z",
113113 "contradictions": [],
114114 "overrideCount": 0,
115115 "status": "active"
……@@ -144,7 +144,7 @@
144144 "verify-binary-matches-source"
145145 ],
146146 "factCount": 21,
147 "validatedAt": "2026-04-07T01:40:34.855Z",
147 "validatedAt": "2026-04-07T03:11:31.384Z",
148148 "contradictions": [],
149149 "overrideCount": 0,
150150 "status": "active"
……@@ -167,7 +167,7 @@
167167 "verify-binary-matches-source"
168168 ],
169169 "factCount": 9,
170 "validatedAt": "2026-04-07T01:40:34.855Z",
170 "validatedAt": "2026-04-07T03:11:31.384Z",
171171 "contradictions": [],
172172 "overrideCount": 0,
173173 "status": "active"
data/validation-report.json +1 -1
……@@ -1,5 +1,5 @@
11{
2 "timestamp": "2026-04-07T01:40:34.893Z",
2 "timestamp": "2026-04-07T03:11:31.421Z",
33 "trainingSize": 21,
44 "testSize": 9,
55 "principleCount": 5,
fuse-signals.js +87 -1
……@@ -625,6 +625,27 @@ function analyzeSession (scannedSession, weights, llmIndex) {
625625 const gitBranch = mode(branches) || null
626626 const cwd = mode(cwds) || null
627627
628 // Derived metrics for clearer observability
629 const density = turns.length > 0 ? corrections / turns.length : 0
630 const approvalDensity = turns.length > 0 ? approvals / turns.length : 0
631
632 // Trusted-sensor-only per-turn score (top 5 sensors from audit)
633 // High signal-to-noise; not polluted by rare-event sensors
634 const TRUSTED_SENSORS = ['correction', 'approval', 'userProvidesAnswer', 'buildErrors', 'editSurvival']
635 let trustedSum = 0, trustedTurnCount = 0
636 for (const t of turnScores) {
637 const sensors = t.sensors
638 if (!sensors) continue
639 let sum = 0, count = 0
640 for (const k of TRUSTED_SENSORS) {
641 if (sensors[k] != null) { sum += sensors[k]; count++ }
642 }
643 if (count > 0) { trustedSum += (sum / count); trustedTurnCount++ }
644 }
645 const trustedScore = trustedTurnCount > 0
646 ? Math.round((trustedSum / trustedTurnCount) * 1000) / 1000
647 : null
648
628649 return {
629650 sessionId,
630651 date: sessionDate?.slice(0, 10),
……@@ -633,9 +654,27 @@ function analyzeSession (scannedSession, weights, llmIndex) {
633654 cwd,
634655 turns: turns.length,
635656 score: Math.round(avgScore * 1000) / 1000,
657 // Per-turn score: quality independent of session length
658 perTurnScore: Math.round(turnAvg * 1000) / 1000,
659 // Trusted score (top-5 sensors only — PRIMARY quality signal)
660 trustedScore,
661 // Session-level aggregate (what penalizes long sessions)
662 sessionSignalScore: Math.round(sessionSignalAvg * 1000) / 1000,
663 // Derived densities
664 density: Math.round(density * 1000) / 1000,
665 approvalDensity: Math.round(approvalDensity * 1000) / 1000,
666 // Correction burst — first-class metric. Length of longest run of consecutive
667 // corrections. 1 = healthy (isolated mistakes); 2+ = stuck pattern.
668 correctionBurst: maxCorrectionStreak,
636669 corrections,
637670 approvals,
638671 totalToolCalls,
672 // Session length as first-class metric — longer = more ambitious work
673 sessionLength: {
674 turns: turns.length,
675 toolCalls: totalToolCalls,
676 toolsPerTurn: turns.length > 0 ? Math.round((totalToolCalls / turns.length) * 10) / 10 : 0
677 },
639678 trajectory,
640679 sessionSignals,
641680 sensorAverages,
……@@ -691,16 +730,53 @@ function computeWeeklyScores (sessions) {
691730 d.setDate(d.getDate() - d.getDay())
692731 const weekKey = d.toISOString().slice(0, 10)
693732 if (!weeks[weekKey]) {
694 weeks[weekKey] = { sessions: 0, totalScore: 0, corrections: 0, approvals: 0, toolCalls: 0 }
733 weeks[weekKey] = {
734 sessions: 0,
735 totalScore: 0,
736 totalPerTurnScore: 0,
737 totalTrustedScore: 0,
738 trustedScoreSessions: 0,
739 totalTurns: 0,
740 totalDensity: 0,
741 totalCorrectionBurst: 0,
742 burstSessions: 0,
743 maxCorrectionBurst: 0,
744 corrections: 0,
745 approvals: 0,
746 toolCalls: 0
747 }
695748 }
696749 weeks[weekKey].sessions++
697750 weeks[weekKey].totalScore += s.score
751 weeks[weekKey].totalPerTurnScore += (s.perTurnScore != null ? s.perTurnScore : s.score)
752 // Trusted score aggregate (NEW primary metric)
753 if (s.trustedScore != null) {
754 weeks[weekKey].totalTrustedScore += s.trustedScore
755 weeks[weekKey].trustedScoreSessions++
756 }
757 // Correction-burst aggregate — longest streak of consecutive corrections
758 if (s.correctionBurst != null) {
759 weeks[weekKey].totalCorrectionBurst += s.correctionBurst
760 weeks[weekKey].burstSessions++
761 weeks[weekKey].maxCorrectionBurst = Math.max(weeks[weekKey].maxCorrectionBurst, s.correctionBurst)
762 }
763 weeks[weekKey].totalTurns += s.turns
764 weeks[weekKey].totalDensity += (s.density != null ? s.density : (s.corrections / Math.max(1, s.turns)))
698765 weeks[weekKey].corrections += s.corrections
699766 weeks[weekKey].approvals += s.approvals
700767 weeks[weekKey].toolCalls += s.totalToolCalls
701768 }
702769 for (const w of Object.values(weeks)) {
703770 w.avgScore = Math.round((w.totalScore / w.sessions) * 1000) / 1000
771 w.avgPerTurnScore = Math.round((w.totalPerTurnScore / w.sessions) * 1000) / 1000
772 w.avgTrustedScore = w.trustedScoreSessions > 0
773 ? Math.round((w.totalTrustedScore / w.trustedScoreSessions) * 1000) / 1000
774 : null
775 w.avgCorrectionBurst = w.burstSessions > 0
776 ? Math.round((w.totalCorrectionBurst / w.burstSessions) * 100) / 100
777 : null
778 w.avgTurns = Math.round(w.totalTurns / w.sessions)
779 w.avgDensity = Math.round((w.totalDensity / w.sessions) * 1000) / 1000
704780 }
705781 return weeks
706782}
……@@ -817,6 +893,16 @@ async function main () {
817893 cwd: s.cwd,
818894 turns: s.turns,
819895 score: s.score,
896 // Per-turn score (length-independent)
897 perTurnScore: s.perTurnScore,
898 // Trusted-score (top-5 sensors only) — PRIMARY quality metric
899 trustedScore: s.trustedScore,
900 sessionSignalScore: s.sessionSignalScore,
901 density: s.density,
902 approvalDensity: s.approvalDensity,
903 // Correction-burst: longest run of consecutive corrections (added 2026-04-13)
904 correctionBurst: s.correctionBurst,
905 sessionLength: s.sessionLength,
820906 corrections: s.corrections,
821907 approvals: s.approvals,
822908 totalToolCalls: s.totalToolCalls,
pipeline.js +13 -0
……@@ -64,6 +64,19 @@ async function main () {
6464 // Step 9: Dashboard
6565 run('Dashboard', `node ${path.join(DIR, 'dashboard.js')} --no-open`)
6666
67 // Step 10: AtomSpace ingest (optional — skips if CogServer not running)
68 try {
69 const { isCogServerUp } = require('./atomspace/cogserver')
70 const up = await isCogServerUp()
71 if (up) {
72 run('AtomSpace Ingest', `node ${path.join(DIR, 'atomspace', 'ingest.js')}`)
73 } else {
74 console.log(' AtomSpace: CogServer not running, skipping ingest')
75 }
76 } catch {
77 // atomspace module not available — skip silently
78 }
79
6780 const total = Date.now() - start
6881 console.log(`\nFast pipeline: ${(total / 1000).toFixed(1)}s`)
6982}
preflight-review.js +224 -4
……@@ -13,6 +13,12 @@ const PRINCIPLES_FILE = path.join(__dirname, 'active-principles.md')
1313const CORRECTIONS_LOG = path.join(__dirname, 'corrections.log')
1414const PROJECT_MAPS_FILE = path.join(__dirname, 'data', 'project-maps.json')
1515
16// AtomSpace structural queries (optional — graceful fallback if CogServer down)
17let atomspaceQuery = null
18let fireLog = null
19try { atomspaceQuery = require('./atomspace/query') } catch {}
20try { fireLog = require('./atomspace/fire-log') } catch {}
21
1622// Shared modules and their downstream consumers
1723// This is the map of files where edits need pre-flight review
1824const SHARED_MODULE_MAP = {
……@@ -106,7 +112,7 @@ function loadRecentCorrections (filePath) {
106112}
107113
108114// Check which shared module pattern matches this file
109function findSharedModule (filePath) {
115async function findSharedModule (filePath) {
110116 // First check hardcoded map (manually curated danger files)
111117 for (const [pattern, info] of Object.entries(SHARED_MODULE_MAP)) {
112118 if (filePath.includes(pattern)) {
……@@ -114,7 +120,14 @@ function findSharedModule (filePath) {
114120 }
115121 }
116122
117 // Fallback: check autodiscovered project map for high-danger subsystems
123 // Skip non-production paths — tests, examples, demos aren't shared module risk
124 const lowerPath = filePath.toLowerCase()
125 if (/\/(tests?|examples?|demos?|fixtures?|__tests__|__mocks__)\//.test(lowerPath)) {
126 return null
127 }
128
129 // Fallback: check autodiscovered project map for HIGH-danger subsystems only.
130 // Medium is too common (18/28 subsystems) and blocks routine development.
118131 if (fs.existsSync(PROJECT_MAPS_FILE)) {
119132 try {
120133 const maps = JSON.parse(fs.readFileSync(PROJECT_MAPS_FILE, 'utf8'))
……@@ -126,7 +139,7 @@ function findSharedModule (filePath) {
126139 const matchesFile = (subData.files || []).some(f =>
127140 filePath.endsWith(f) || filePath.endsWith('/' + f)
128141 )
129 if (matchesFile && subData.dangerLevel !== 'low') {
142 if (matchesFile && subData.dangerLevel === 'high') {
130143 return {
131144 pattern: `${projectName}/${subName}`,
132145 description: `Auto-detected: ${projectName}/${subName} subsystem (${subData.fileCount} files, ${subData.loopCount} past loops, danger=${subData.dangerLevel})`,
……@@ -141,6 +154,28 @@ function findSharedModule (filePath) {
141154 } catch {}
142155 }
143156
157 // Final fallback: query AtomSpace for structural risk.
158 // Require genuine danger signal: 3+ past loops or high danger with downstream consumers.
159 // A single past loop is routine development, not a blocking signal.
160 if (atomspaceQuery) {
161 try {
162 const br = await atomspaceQuery.blastRadius(filePath)
163 if (br) {
164 const isGenuineRisk = br.loops.length >= 3 ||
165 (br.dangerLevel === 'high' && br.consumers.length > 0) ||
166 (br.loops.length >= 1 && br.consumers.length >= 3)
167 if (isGenuineRisk) {
168 return {
169 pattern: br.file,
170 description: `AtomSpace: ${br.loops.length} past loops, danger=${br.dangerLevel}, ${br.consumers.length} downstream consumers`,
171 downstream: br.consumers.slice(0, 5).map(c => c.replace('file:', '')),
172 loopHistory: br.loops.length > 0 ? `${br.loops.length} loops linked in AtomSpace` : null
173 }
174 }
175 }
176 } catch {}
177 }
178
144179 return null
145180}
146181
……@@ -175,7 +210,7 @@ async function main () {
175210 process.exit(0)
176211 }
177212
178 const sharedModule = findSharedModule(filePath)
213 const sharedModule = await findSharedModule(filePath)
179214 if (!sharedModule) {
180215 process.exit(0) // not a shared module, allow
181216 }
……@@ -191,6 +226,32 @@ async function main () {
191226 const corrections = loadRecentCorrections(filePath)
192227 const downstream = sharedModule.downstream
193228
229 // AtomSpace structural + semantic + contrast context (optional, non-blocking)
230 let structural = null
231 let fileCtx = null
232 let unfamiliarity = null
233 let resolutions = null
234 let contrast = null
235 if (atomspaceQuery) {
236 try { structural = await atomspaceQuery.blastRadiusEnhanced(filePath) } catch {}
237 try { fileCtx = await atomspaceQuery.fileContext(filePath) } catch {}
238 try { unfamiliarity = await atomspaceQuery.unfamiliarityScore(filePath) } catch {}
239 if (structural && structural.loops.length > 0) {
240 try { resolutions = await atomspaceQuery.findResolution(filePath) } catch {}
241 }
242 // Contrast-derived meta-cognition: "last time this was the fix / last time this was wasted effort"
243 try { contrast = await atomspaceQuery.contrastGuidance(filePath) } catch {}
244 }
245
246 // Live session context (streaming observability)
247 let liveSession = null
248 let preferredApproaches = null
249 if (atomspaceQuery) {
250 try { liveSession = await atomspaceQuery.liveSessionContext(filePath) } catch {}
251 // Preferred-approach from success memory (Phase 2.3)
252 try { preferredApproaches = await atomspaceQuery.preferredApproachFor(filePath) } catch {}
253 }
254
194255 // Build the briefing
195256 const briefing = []
196257 briefing.push('PRE-FLIGHT REVIEW — SHARED MODULE EDIT')
……@@ -206,6 +267,120 @@ async function main () {
206267 briefing.push(` → ${dep}`)
207268 }
208269 briefing.push(` ${downstream.length} project(s) share this pattern`)
270
271 // Add AtomSpace context if available
272 if (fileCtx || structural || unfamiliarity) {
273 briefing.push('')
274 briefing.push('ATOMSPACE CONTEXT')
275 briefing.push('─'.repeat(30))
276
277 // File classification — language/domain/role
278 if (fileCtx) {
279 const parts = []
280 if (fileCtx.language !== 'unknown') parts.push(`language=${fileCtx.language}`)
281 if (fileCtx.domain !== 'unknown') parts.push(`domain=${fileCtx.domain}`)
282 if (fileCtx.role !== 'unknown') parts.push(`role=${fileCtx.role}`)
283 if (fileCtx.isExportedAPI) parts.push(`exports=${fileCtx.exports} (public API)`)
284 if (parts.length) briefing.push(` File: ${parts.join(', ')}`)
285 }
286
287 // Unfamiliarity signal
288 if (unfamiliarity && unfamiliarity.familiarity !== 'unknown') {
289 const warn = (unfamiliarity.familiarity === 'unfamiliar' || unfamiliarity.familiarity === 'never-visited')
290 briefing.push(` Module: ${unfamiliarity.module} (${unfamiliarity.sessions} sessions — ${unfamiliarity.familiarity})${warn ? ' ⚠' : ''}`)
291 }
292
293 // Structural blast radius
294 if (structural) {
295 if (structural.consumers.length) {
296 briefing.push(` ${structural.consumers.length} files import from this file`)
297 for (const c of structural.consumers.slice(0, 3)) {
298 briefing.push(` ← ${c.replace('file:', '')}`)
299 }
300 }
301 if (structural.loops.length) {
302 briefing.push(` ${structural.loops.length} past correction loops on this file`)
303 }
304 if (structural.rules.length) {
305 const hw = structural.rulesHighWisdom || 0
306 const lw = structural.rulesLowWisdom || 0
307 const wisdomNote = hw > 0 ? ` (${hw} high-wisdom)` : (lw > 0 ? ` (${lw} stale-referents)` : '')
308 briefing.push(` ${structural.rules.length} learned rules apply${wisdomNote}`)
309 }
310 if (typeof structural.riskScore === 'number') {
311 briefing.push(` Combined risk score: ${structural.riskScore}`)
312 }
313 }
314
315 // Past resolutions for similar loops
316 if (resolutions && resolutions.matches.length > 0) {
317 briefing.push('')
318 briefing.push(' SIMILAR PAST RESOLUTIONS')
319 for (const m of resolutions.matches.slice(0, 3)) {
320 briefing.push(` [${m.type || '?'}] fix was in: ${m.resolutionFiles.slice(0, 2).join(', ')}`)
321 }
322 }
323
324 // Live session observability — what happened THIS session
325 if (liveSession && liveSession.sessionId) {
326 const hasLiveSignal = liveSession.editsForThisFile > 0 ||
327 liveSession.correctionsNearThisFile > 0 ||
328 liveSession.totalSessionCorrections > 2
329 if (hasLiveSignal) {
330 briefing.push('')
331 briefing.push(' THIS SESSION (live)')
332 if (liveSession.editsForThisFile > 0) {
333 briefing.push(` You've edited this file ${liveSession.editsForThisFile}x this session`)
334 }
335 if (liveSession.correctionsNearThisFile > 0) {
336 briefing.push(` ⚠ ${liveSession.correctionsNearThisFile} corrections occurred while editing this file`)
337 for (const c of liveSession.recentCorrections.slice(0, 2)) {
338 briefing.push(` turn ${c.turn}: "${c.text.slice(0, 80)}..."`)
339 }
340 }
341 if (liveSession.totalSessionCorrections > 2) {
342 briefing.push(` Session has ${liveSession.totalSessionCorrections} corrections total — slow down`)
343 }
344 }
345 }
346
347 // Preferred-approach from past successes (Phase 2.3)
348 if (preferredApproaches && preferredApproaches.length > 0) {
349 briefing.push('')
350 briefing.push(' ★ PAST SUCCESSFUL APPROACHES')
351 for (const p of preferredApproaches.slice(0, 2)) {
352 briefing.push(` [${p.shape} ×${p.successCount}] ${p.id.replace('preferred:', '').split('::')[0]}`)
353 if (p.sampleApproval) briefing.push(` e.g. "${p.sampleApproval}"`)
354 }
355 }
356
357 // Contrast-derived meta-cognition (highest-value signal)
358 if (contrast) {
359 const hasSignal = (contrast.worksFor && contrast.worksFor.length > 0) ||
360 (contrast.failedFor && contrast.failedFor.length > 0) ||
361 (contrast.patterns && contrast.patterns.length > 0)
362 if (hasSignal) {
363 briefing.push('')
364 briefing.push(' META-COGNITION (contrast-derived)')
365 if (contrast.worksFor.length > 0) {
366 briefing.push(` ✓ This file FIXED ${contrast.worksFor.length} past loops — it's usually the right place`)
367 }
368 if (contrast.failedFor.length > 0) {
369 briefing.push(` ✗ This file was WASTED EFFORT in ${contrast.failedFor.length} past loops — fix was elsewhere`)
370 }
371 // Cross-loop patterns
372 for (const p of (contrast.patterns || []).slice(0, 1)) {
373 briefing.push(` Pattern [${p.context}] (${p.loopCount} loops):`)
374 if (p.tryFirst.length) {
375 briefing.push(` TRY: ${p.tryFirst.slice(0, 3).join(', ')}`)
376 }
377 if (p.avoidFirst.length) {
378 briefing.push(` AVOID: ${p.avoidFirst.slice(0, 3).join(', ')}`)
379 }
380 }
381 }
382 }
383 }
209384 briefing.push('')
210385
211386 if (principles.length > 0) {
……@@ -242,6 +417,51 @@ async function main () {
242417 briefing.push('confirm. Tell Claude to proceed or adjust the approach.')
243418 briefing.push('─'.repeat(50))
244419
420 // Log fired rules/meta atoms for utility tracking + live dashboard
421 if (fireLog) {
422 try {
423 const sessionId = (() => {
424 try { return require('fs').readFileSync('/tmp/claude-atomspace-session', 'utf8').trim() } catch { return null }
425 })()
426 const contextExcerpt = `edit ${filePath}`
427 // Log rules that would be shown
428 if (structural && structural.rules) {
429 for (const r of structural.rules.slice(0, 5)) {
430 fireLog.logFire({
431 session: sessionId,
432 hook: 'preflight',
433 atom: r,
434 context: contextExcerpt,
435 trigger: filePath
436 })
437 }
438 }
439 // Log meta atoms from contrast guidance
440 if (contrast) {
441 for (const m of (contrast.worksFor || []).slice(0, 3)) {
442 fireLog.logFire({
443 session: sessionId,
444 hook: 'preflight',
445 atom: `meta:loop:${m.loopId}`,
446 atomType: 'meta-loop',
447 context: contextExcerpt,
448 trigger: filePath
449 })
450 }
451 for (const p of (contrast.patterns || []).slice(0, 2)) {
452 fireLog.logFire({
453 session: sessionId,
454 hook: 'preflight',
455 atom: `meta:pattern:${p.context}`,
456 atomType: 'pattern',
457 context: contextExcerpt,
458 trigger: filePath
459 })
460 }
461 }
462 } catch {}
463 }
464
245465 // Mark as reviewed for this session (won't block again on same file)
246466 // The next edit to this file will pass through — if the user overrides,
247467 // that's tracked by the override-tracker for principle demotion
realtime-score.js +124 -7
……@@ -62,6 +62,58 @@ const APPROVAL_PATTERNS = _tuned?.approval || [
6262 /\b(thanks|thank you)\b/i,
6363]
6464
65// Weak-positive patterns — not full approval but proceed signal.
66const PROCEED_PATTERNS = [
67 /\b(continue|proceed|go ahead|go on|keep going|next)\b/i,
68 /^\s*(yes|yep|yeah|sure|ok|okay)\b/i,
69 /\b(do it|do that|try it|let'?s try)\b/i
70]
71
72// Direction / new-problem signals — user giving a new task or pivoting.
73// These reset the stuck-loop counter because we're starting fresh.
74const NEW_PROBLEM_PATTERNS = [
75 // Directives that start new work
76 /^\s*(now|next|also|and|then)\b/i,
77 /\b(let'?s|lets)\s+(add|build|make|create|try|do|check|write|fix|update|run|test|deploy|restart)\b/i,
78 // Imperative commands (usually building on prior state)
79 /^\s*(add|build|make|create|fix|update|run|test|deploy|restart|remove|delete|show|tell)\b/i,
80 // Explicit pivots
81 /\b(actually|instead|wait|hold on)\b.*\b(let'?s|try|do)\b/i,
82 /^\s*i (want|need|would like) (you|to|us)\b/i
83]
84
85// Simple commands — one-word or short imperative that's not a correction
86const COMMAND_PATTERNS = [
87 /^\s*(restart|run|test|build|commit|push|deploy|check|show|list|help)\s*\??\s*$/i
88]
89
90/**
91 * Classify a user prompt semantically.
92 * Returns: 'correction' | 'approval' | 'proceed' | 'new-problem' | 'command' | 'question' | 'neutral'
93 *
94 * Categories affect state differently:
95 * correction → corrections++, keeps problem-turns running
96 * approval → full completion, resets problem, increments problemCount
97 * proceed → weak reset on stuck-loop if stuck, else continues problem
98 * new-problem → full reset (new work starting)
99 * command → weak reset (using prior state for a new action)
100 * question → neutral (info-seeking, doesn't advance or block)
101 * neutral → no signal
102 */
103function classifyPrompt (text) {
104 if (!text) return 'neutral'
105 const t = text.trim()
106 // Order matters — check corrections first (strongest negative)
107 if (CORRECTION_PATTERNS.some(p => p.test(t))) return 'correction'
108 if (APPROVAL_PATTERNS.some(p => p.test(t))) return 'approval'
109 if (COMMAND_PATTERNS.some(p => p.test(t))) return 'command'
110 if (NEW_PROBLEM_PATTERNS.some(p => p.test(t))) return 'new-problem'
111 if (PROCEED_PATTERNS.some(p => p.test(t))) return 'proceed'
112 // Question heuristic
113 if (/\?/.test(t) && /\b(why|how|what|when|where|which|can you|do you|is there)\b/i.test(t)) return 'question'
114 return 'neutral'
115}
116
65117// Load skip patterns from tuned config, fall back to defaults
66118const _skipFromConfig = (() => {
67119 if (fs.existsSync(TUNED_PATTERNS_FILE)) {
……@@ -97,7 +149,15 @@ const INTERRUPTION_PATTERN = /^\[Request interrupted/
97149function loadState () {
98150 if (!fs.existsSync(STATE_FILE)) return createFreshState()
99151 try {
100 return JSON.parse(fs.readFileSync(STATE_FILE, 'utf8'))
152 const state = JSON.parse(fs.readFileSync(STATE_FILE, 'utf8'))
153 // Migrate old state: ensure new problem-tracking fields exist
154 // Seed from existing counters so context-saturation etc work immediately
155 if (state._problemStartTurn == null) state._problemStartTurn = 0
156 if (state._problemTurns == null) state._problemTurns = state.turns || 0
157 if (state._problemCount == null) state._problemCount = (state.approvals || 0) + 1
158 if (state._lastApprovalTurn == null) state._lastApprovalTurn = 0
159 if (state._lastProceedTurn == null) state._lastProceedTurn = 0
160 return state
101161 } catch {
102162 return createFreshState()
103163 }
……@@ -129,6 +189,14 @@ function createFreshState () {
129189 _recentCorrections: [], // last 5 correction texts
130190 _loopActive: false,
131191 _loopMatches: null,
192 // Problem-boundary tracking (NEW)
193 // "Problem turns" = turns since last approval/proceed (or session start).
194 // Resets on approval, proceed signal, or topic shift.
195 _problemStartTurn: 0, // turn # when current problem started
196 _problemTurns: 0, // turns spent on current problem
197 _problemCount: 1, // # of problems tackled this session
198 _lastApprovalTurn: 0, // last turn with explicit approval
199 _lastProceedTurn: 0 // last turn with weak-positive signal
132200 }
133201}
134202
……@@ -308,15 +376,55 @@ async function handlePrompt () {
308376
309377 const state = loadState()
310378
311 // Check for correction/approval
312 const isCorrection = CORRECTION_PATTERNS.some(p => p.test(userText))
313 const isApproval = APPROVAL_PATTERNS.some(p => p.test(userText))
379 // Semantic prompt classification
380 const category = classifyPrompt(userText)
381 const isCorrection = category === 'correction'
382 const isApproval = category === 'approval'
314383 const isInterruption = INTERRUPTION_PATTERN.test(userText)
315384
316385 if (isCorrection) state.corrections++
317386 if (isApproval) state.approvals++
318387 if (isInterruption) state.interruptions++
319388
389 // Problem-boundary tracking based on semantic category
390 // Each category has a distinct effect on stuck-loop counter:
391 switch (category) {
392 case 'approval':
393 // Full completion — new problem begins
394 state._lastApprovalTurn = state.turns
395 state._problemStartTurn = state.turns
396 state._problemTurns = 0
397 state._problemCount = (state._problemCount || 1) + 1
398 break
399 case 'new-problem':
400 case 'command':
401 // Fresh work starting — reset problem-turns, count as weak completion
402 state._problemStartTurn = state.turns
403 state._problemTurns = 0
404 state._problemCount = (state._problemCount || 1) + 1
405 state._lastProceedTurn = state.turns
406 break
407 case 'proceed':
408 // Weak reset — resets stuck-loop if currently stuck, else continues problem
409 state._lastProceedTurn = state.turns
410 if (state._problemTurns > 4) {
411 state._problemStartTurn = state.turns
412 state._problemTurns = 0
413 }
414 break
415 case 'correction':
416 // Stays in current problem; counter advances as normal
417 break
418 case 'question':
419 case 'neutral':
420 // No categorical effect — fall through to counter update
421 break
422 }
423
424 // Always update problem-turns (resets above take effect)
425 state._problemTurns = state.turns - (state._problemStartTurn || 0)
426 state._lastPromptCategory = category
427
320428 // Check edit survival — if this turn is a correction and last turn had edits,
321429 // those edits likely didn't survive
322430 if (isCorrection && state._lastTurnEdits.length > 0) {
……@@ -471,15 +579,24 @@ function handleStatus () {
471579 : state.trajectory === 'declining' ? '▼'
472580 : '▬'
473581
474 // Compute learning rate: what % of corrections led to clean turns?
582 // Compute learning rate: what % of correction BURSTS led to clean turns?
583 // A burst = consecutive corrections collapsed into one event.
584 // Learned = burst followed by 3+ non-correction turns.
585 // Failed = burst followed by another burst within 3 turns.
475586 const scores = state.turnScores || []
476587 let corrLearned = 0
477588 let corrFailed = 0
478 for (let i = 0; i < scores.length; i++) {
589 let i = 0
590 while (i < scores.length) {
479591 if (scores[i] === 0.2) {
480 const after = scores.slice(i + 1, i + 4)
592 // Found a correction burst — skip consecutive corrections
593 while (i < scores.length && scores[i] === 0.2) i++
594 // Now check what follows
595 const after = scores.slice(i, i + 3)
481596 if (after.includes(0.2)) corrFailed++
482597 else corrLearned++
598 } else {
599 i++
483600 }
484601 }
485602 const totalCorr = corrLearned + corrFailed