| … | … | @@ -175,6 +175,24 @@ function generate () { |
| 175 | 175 | const liveTraj = liveSession?.trajectory === 'improving' ? '▲ improving' : liveSession?.trajectory === 'declining' ? '▼ declining' : '▬ stable' |
| 176 | 176 | const liveTrajColor = liveSession?.trajectory === 'improving' ? 'green' : liveSession?.trajectory === 'declining' ? 'red' : 'dim' |
| 177 | 177 | |
| 178 | // Live learning rate |
| 179 | let liveLearningRate = null |
| 180 | if (liveSession?.turnScores) { |
| 181 | let learned = 0, failed = 0 |
| 182 | const ts = liveSession.turnScores |
| 183 | for (let i = 0; i < ts.length; i++) { |
| 184 | if (ts[i] === 0.2) { |
| 185 | const after = ts.slice(i + 1, i + 4) |
| 186 | if (after.includes(0.2)) failed++ |
| 187 | else learned++ |
| 188 | } |
| 189 | } |
| 190 | const total = learned + failed |
| 191 | if (total > 0) liveLearningRate = Math.round(learned / total * 100) |
| 192 | } |
| 193 | const liveLearningStr = liveLearningRate !== null ? `${liveLearningRate}% learning` : '' |
| 194 | const liveLearningColor = liveLearningRate > 70 ? 'green' : liveLearningRate > 40 ? 'yellow' : 'red' |
| 195 | |
| 178 | 196 | // ─── Generate HTML ─── |
| 179 | 197 | const html = `<!DOCTYPE html> |
| 180 | 198 | <html lang="en"> |
| … | … | @@ -217,7 +235,7 @@ function generate () { |
| 217 | 235 | </head> |
| 218 | 236 | <body> |
| 219 | 237 | <h1>Knowledge Ratchet</h1> |
| 220 | | <h2>Intelligence quality tracking — generated ${new Date().toISOString().slice(0, 19)}</h2> |
| 238 | <h2>Self-improving AI quality system — tracks corrections, detects failure loops, auto-generates prevention rules</h2> |
| 221 | 239 | |
| 222 | 240 | ${liveSession && liveSession.turns > 0 ? ` |
| 223 | 241 | <div class="card" style="border-color: ${liveBorder}; margin-bottom: 16px;"> |
| … | … | @@ -227,6 +245,7 @@ ${liveSession && liveSession.turns > 0 ? ` |
| 227 | 245 | <div><span class="green" style="font-size:20px">${liveSession.approvals} approved</span> <span class="red" style="font-size:20px">${liveSession.corrections} corrected</span><span class="dim"> in ${liveSession.turns} turns</span></div> |
| 228 | 246 | <div><span class="dim">${liveSession.toolCalls} tools</span></div> |
| 229 | 247 | <div><span class="${liveTrajColor}">${liveTraj}</span></div> |
| 248 | ${liveLearningRate !== null ? `<div><span class="${liveLearningColor}" style="font-size:18px">${liveLearningStr}</span></div>` : ''} |
| 230 | 249 | </div> |
| 231 | 250 | </div> |
| 232 | 251 | ` : ''} |
| … | … | @@ -258,7 +277,7 @@ ${liveSession && liveSession.turns > 0 ? ` |
| 258 | 277 | <!-- Weekly Fused Score Trend --> |
| 259 | 278 | <div class="chart-container"> |
| 260 | 279 | <div class="card-title">Weekly Fused Score Trend</div> |
| 261 | | ${renderLineChart(weeklyData.map(w => w.score), weeklyData.map(w => w.week), { height: 120, color: '#58a6ff', label: 'score' })} |
| 280 | ${renderLineChart(weeklyData.map(w => w.score), weeklyData.map(w => w.week), { height: 140, color: '#58a6ff', label: 'Fused Score', format: 'auto' })} |
| 262 | 281 | </div> |
| 263 | 282 | |
| 264 | 283 | <!-- Weekly Correction Density --> |
| … | … | @@ -273,7 +292,7 @@ ${liveSession && liveSession.turns > 0 ? ` |
| 273 | 292 | ${renderLineChart( |
| 274 | 293 | Object.entries(loopsWeekly).sort().map(([, v]) => v.total), |
| 275 | 294 | Object.entries(loopsWeekly).sort().map(([k]) => k), |
| 276 | | { height: 100, color: '#f85149', label: 'loops' } |
| 295 | { height: 120, color: '#f85149', label: 'Loops Per Week', format: 'int' } |
| 277 | 296 | )} |
| 278 | 297 | </div> |
| 279 | 298 | |
| … | … | @@ -334,7 +353,7 @@ ${benchmarkHistory.length >= 2 ? ` |
| 334 | 353 | ${renderLineChart( |
| 335 | 354 | benchmarkHistory.filter(h => h.novelRate != null).map(h => h.novelRate), |
| 336 | 355 | benchmarkHistory.filter(h => h.novelRate != null).map(h => h.date), |
| 337 | | { height: 100, color: '#3fb950', label: 'novel rate' } |
| 356 | { height: 120, color: '#3fb950', label: 'Novel Rate', format: 'pct' } |
| 338 | 357 | )} |
| 339 | 358 | </div> |
| 340 | 359 | <div class="card"> |
| … | … | @@ -342,7 +361,7 @@ ${benchmarkHistory.length >= 2 ? ` |
| 342 | 361 | ${renderLineChart( |
| 343 | 362 | benchmarkHistory.filter(h => h.density != null).map(h => h.density), |
| 344 | 363 | benchmarkHistory.filter(h => h.density != null).map(h => h.date), |
| 345 | | { height: 100, color: '#f85149', label: 'density' } |
| 364 | { height: 120, color: '#f85149', label: 'Density', format: 'pct' } |
| 346 | 365 | )} |
| 347 | 366 | </div> |
| 348 | 367 | <div class="card"> |
| … | … | @@ -350,7 +369,7 @@ ${benchmarkHistory.length >= 2 ? ` |
| 350 | 369 | ${renderLineChart( |
| 351 | 370 | benchmarkHistory.filter(h => h.avgResolution != null).map(h => h.avgResolution), |
| 352 | 371 | benchmarkHistory.filter(h => h.avgResolution != null).map(h => h.date), |
| 353 | | { height: 100, color: '#d29922', label: 'turns' } |
| 372 | { height: 120, color: '#d29922', label: 'Turns to Resolve', format: 'auto' } |
| 354 | 373 | )} |
| 355 | 374 | </div> |
| 356 | 375 | <div class="card"> |
| … | … | @@ -358,7 +377,7 @@ ${benchmarkHistory.length >= 2 ? ` |
| 358 | 377 | ${renderLineChart( |
| 359 | 378 | benchmarkHistory.filter(h => h.rulesCount != null).map(h => h.rulesCount), |
| 360 | 379 | benchmarkHistory.filter(h => h.rulesCount != null).map(h => h.date), |
| 361 | | { height: 100, color: '#58a6ff', label: 'rules' } |
| 380 | { height: 120, color: '#58a6ff', label: 'Rules', format: 'int' } |
| 362 | 381 | )} |
| 363 | 382 | </div> |
| 364 | 383 | </div> |
| … | … | @@ -434,64 +453,112 @@ if (serveArg) { |
| 434 | 453 | // ─── SVG Chart Renderers ─── |
| 435 | 454 | |
| 436 | 455 | function renderLineChart (values, labels, opts = {}) { |
| 437 | | const { height = 100, color = '#58a6ff', label = '' } = opts |
| 456 | const { height = 120, color = '#58a6ff', label = '', format = 'auto' } = opts |
| 438 | 457 | if (values.length === 0) return '<div class="dim">No data</div>' |
| 439 | 458 | |
| 440 | 459 | const w = 600 |
| 441 | 460 | const h = height |
| 442 | | const pad = 30 |
| 443 | | const min = Math.min(...values) * 0.95 |
| 444 | | const max = Math.max(...values) * 1.05 |
| 461 | const padL = 50 // left pad for Y-axis labels |
| 462 | const padR = 20 |
| 463 | const padT = 25 |
| 464 | const padB = 25 |
| 465 | const chartW = w - padL - padR |
| 466 | const chartH = h - padT - padB |
| 467 | |
| 468 | const rawMin = Math.min(...values) |
| 469 | const rawMax = Math.max(...values) |
| 470 | const margin = (rawMax - rawMin) * 0.15 || 0.05 |
| 471 | const min = rawMin - margin |
| 472 | const max = rawMax + margin |
| 445 | 473 | const range = max - min || 1 |
| 446 | 474 | |
| 475 | // Format values for display |
| 476 | function fmt (v) { |
| 477 | if (format === 'pct') return (v * 100).toFixed(0) + '%' |
| 478 | if (format === 'int') return Math.round(v).toString() |
| 479 | if (v >= 100) return Math.round(v).toString() |
| 480 | if (v >= 1) return v.toFixed(1) |
| 481 | return (v * 100).toFixed(0) + '%' |
| 482 | } |
| 483 | |
| 484 | // Y-axis: 3-4 gridlines with labels |
| 485 | const gridLines = [] |
| 486 | const steps = 4 |
| 487 | for (let i = 0; i <= steps; i++) { |
| 488 | const v = min + (range * i / steps) |
| 489 | const y = h - padB - (i / steps) * chartH |
| 490 | gridLines.push({ v, y }) |
| 491 | } |
| 492 | |
| 447 | 493 | const points = values.map((v, i) => { |
| 448 | | const x = pad + (i / Math.max(1, values.length - 1)) * (w - pad * 2) |
| 449 | | const y = h - pad - ((v - min) / range) * (h - pad * 2) |
| 450 | | return `${x},${y}` |
| 494 | const x = padL + (i / Math.max(1, values.length - 1)) * chartW |
| 495 | const y = h - padB - ((v - min) / range) * chartH |
| 496 | return { x, y, v } |
| 451 | 497 | }) |
| 452 | 498 | |
| 453 | | const labelElements = labels.map((l, i) => { |
| 499 | const xLabels = labels.map((l, i) => { |
| 454 | 500 | if (i % Math.max(1, Math.floor(labels.length / 6)) !== 0) return '' |
| 455 | | const x = pad + (i / Math.max(1, values.length - 1)) * (w - pad * 2) |
| 456 | | return `<text x="${x}" y="${h - 5}" fill="#484f58" font-size="10" text-anchor="middle">${l.slice(5)}</text>` |
| 501 | const x = padL + (i / Math.max(1, values.length - 1)) * chartW |
| 502 | return `<text x="${x}" y="${h - 3}" fill="#484f58" font-size="10" text-anchor="middle">${l.slice(5)}</text>` |
| 457 | 503 | }).join('') |
| 458 | 504 | |
| 459 | 505 | return `<svg viewBox="0 0 ${w} ${h}" xmlns="http://www.w3.org/2000/svg"> |
| 460 | | <polyline points="${points.join(' ')}" fill="none" stroke="${color}" stroke-width="2"/> |
| 461 | | ${values.map((v, i) => { |
| 462 | | const x = pad + (i / Math.max(1, values.length - 1)) * (w - pad * 2) |
| 463 | | const y = h - pad - ((v - min) / range) * (h - pad * 2) |
| 464 | | return `<circle cx="${x}" cy="${y}" r="3" fill="${color}"/>` |
| 465 | | }).join('')} |
| 466 | | ${labelElements} |
| 467 | | <text x="${pad}" y="15" fill="#8b949e" font-size="11">${label} ${values[values.length - 1]?.toFixed(3) || ''}</text> |
| 506 | ${gridLines.map(g => ` |
| 507 | <line x1="${padL}" y1="${g.y}" x2="${w - padR}" y2="${g.y}" stroke="#21262d" stroke-width="1"/> |
| 508 | <text x="${padL - 5}" y="${g.y + 4}" fill="#484f58" font-size="9" text-anchor="end">${fmt(g.v)}</text> |
| 509 | `).join('')} |
| 510 | <polyline points="${points.map(p => p.x + ',' + p.y).join(' ')}" fill="none" stroke="${color}" stroke-width="2"/> |
| 511 | ${points.map(p => `<circle cx="${p.x}" cy="${p.y}" r="3" fill="${color}"><title>${fmt(p.v)}</title></circle>`).join('')} |
| 512 | ${points.map(p => `<text x="${p.x}" y="${p.y - 8}" fill="${color}" font-size="9" text-anchor="middle">${fmt(p.v)}</text>`).join('')} |
| 513 | ${xLabels} |
| 514 | <text x="${padL}" y="14" fill="#8b949e" font-size="11">${label}</text> |
| 468 | 515 | </svg>` |
| 469 | 516 | } |
| 470 | 517 | |
| 471 | 518 | function renderBarChart (weeklyData, opts = {}) { |
| 472 | | const { height = 120 } = opts |
| 519 | const { height = 140 } = opts |
| 473 | 520 | if (weeklyData.length === 0) return '<div class="dim">No data</div>' |
| 474 | 521 | |
| 475 | 522 | const w = 600 |
| 476 | 523 | const h = height |
| 477 | | const pad = 30 |
| 478 | | const barW = Math.max(8, (w - pad * 2) / weeklyData.length / 2 - 2) |
| 479 | | |
| 480 | | const maxVal = Math.max(...weeklyData.map(d => d.corrections + d.approvals), 1) |
| 524 | const padL = 50 |
| 525 | const padR = 20 |
| 526 | const padT = 25 |
| 527 | const padB = 25 |
| 528 | const chartW = w - padL - padR |
| 529 | const chartH = h - padT - padB |
| 530 | const barW = Math.max(8, chartW / weeklyData.length / 2 - 2) |
| 531 | |
| 532 | const maxVal = Math.max(...weeklyData.map(d => Math.max(d.corrections, d.approvals)), 1) |
| 533 | |
| 534 | // Y-axis gridlines |
| 535 | const gridLines = [] |
| 536 | const steps = 4 |
| 537 | for (let i = 0; i <= steps; i++) { |
| 538 | const v = Math.round(maxVal * i / steps) |
| 539 | const y = h - padB - (i / steps) * chartH |
| 540 | gridLines.push({ v, y }) |
| 541 | } |
| 481 | 542 | |
| 482 | 543 | return `<svg viewBox="0 0 ${w} ${h}" xmlns="http://www.w3.org/2000/svg"> |
| 544 | ${gridLines.map(g => ` |
| 545 | <line x1="${padL}" y1="${g.y}" x2="${w - padR}" y2="${g.y}" stroke="#21262d" stroke-width="1"/> |
| 546 | <text x="${padL - 5}" y="${g.y + 4}" fill="#484f58" font-size="9" text-anchor="end">${g.v}</text> |
| 547 | `).join('')} |
| 483 | 548 | ${weeklyData.map((d, i) => { |
| 484 | | const x = pad + (i / Math.max(1, weeklyData.length - 1)) * (w - pad * 2) |
| 485 | | const corrH = (d.corrections / maxVal) * (h - pad * 2) |
| 486 | | const apprH = (d.approvals / maxVal) * (h - pad * 2) |
| 549 | const x = padL + (i / Math.max(1, weeklyData.length - 1)) * chartW |
| 550 | const corrH = (d.corrections / maxVal) * chartH |
| 551 | const apprH = (d.approvals / maxVal) * chartH |
| 487 | 552 | return ` |
| 488 | | <rect x="${x - barW}" y="${h - pad - corrH}" width="${barW}" height="${corrH}" fill="#da3633" opacity="0.7"/> |
| 489 | | <rect x="${x}" y="${h - pad - apprH}" width="${barW}" height="${apprH}" fill="#238636" opacity="0.7"/> |
| 553 | <rect x="${x - barW}" y="${h - padB - corrH}" width="${barW}" height="${corrH}" fill="#da3633" opacity="0.7"/> |
| 554 | <rect x="${x}" y="${h - padB - apprH}" width="${barW}" height="${apprH}" fill="#238636" opacity="0.7"/> |
| 555 | <text x="${x - barW/2}" y="${h - padB - corrH - 3}" fill="#da3633" font-size="8" text-anchor="middle">${d.corrections}</text> |
| 556 | <text x="${x + barW/2}" y="${h - padB - apprH - 3}" fill="#238636" font-size="8" text-anchor="middle">${d.approvals}</text> |
| 490 | 557 | <text x="${x}" y="${h - 5}" fill="#484f58" font-size="9" text-anchor="middle">${d.week.slice(5)}</text> |
| 491 | 558 | ` |
| 492 | 559 | }).join('')} |
| 493 | | <text x="${pad}" y="15" fill="#da3633" font-size="11">■ corrections</text> |
| 494 | | <text x="${pad + 100}" y="15" fill="#238636" font-size="11">■ approvals</text> |
| 560 | <text x="${padL}" y="14" fill="#da3633" font-size="11">■ corrections</text> |
| 561 | <text x="${padL + 100}" y="14" fill="#238636" font-size="11">■ approvals</text> |
| 495 | 562 | </svg>` |
| 496 | 563 | } |
| 497 | 564 | |