@cleocode/cleo 2026.3.20 → 2026.3.22

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.
Files changed (150) hide show
  1. package/dist/cli/index.js +39394 -38817
  2. package/dist/cli/index.js.map +4 -4
  3. package/dist/mcp/index.js +35841 -36702
  4. package/dist/mcp/index.js.map +4 -4
  5. package/drizzle-brain.config.ts +7 -0
  6. package/drizzle-nexus.config.ts +7 -0
  7. package/drizzle-tasks.config.ts +7 -0
  8. package/migrations/drizzle-brain/20260301230215_workable_spitfire/migration.sql +68 -0
  9. package/migrations/drizzle-brain/20260301230215_workable_spitfire/snapshot.json +651 -0
  10. package/migrations/drizzle-brain/20260302050325_unknown_justin_hammer/migration.sql +23 -0
  11. package/migrations/drizzle-brain/20260302050325_unknown_justin_hammer/snapshot.json +884 -0
  12. package/migrations/drizzle-brain/20260302061755_unusual_jamie_braddock/migration.sql +2 -0
  13. package/migrations/drizzle-brain/20260302061755_unusual_jamie_braddock/snapshot.json +908 -0
  14. package/migrations/drizzle-brain/20260302193548_luxuriant_glorian/migration.sql +20 -0
  15. package/migrations/drizzle-brain/20260302193548_luxuriant_glorian/snapshot.json +1078 -0
  16. package/migrations/drizzle-brain/20260304045002_white_thunderbolt_ross/migration.sql +16 -0
  17. package/migrations/drizzle-brain/20260304045002_white_thunderbolt_ross/snapshot.json +1233 -0
  18. package/migrations/drizzle-nexus/20260305070805_quick_ted_forrester/migration.sql +46 -0
  19. package/migrations/drizzle-nexus/20260305070805_quick_ted_forrester/snapshot.json +461 -0
  20. package/migrations/drizzle-tasks/20260308024513_oval_king_bedlam/migration.sql +32 -0
  21. package/migrations/drizzle-tasks/20260308024513_oval_king_bedlam/snapshot.json +3727 -0
  22. package/package.json +14 -4
  23. package/packages/ct-skills/skills/ct-cleo/SKILL.md +344 -81
  24. package/packages/ct-skills/skills/ct-grade/SKILL.md +20 -4
  25. package/packages/ct-skills/skills/ct-grade/agents/analysis-reporter.md +203 -0
  26. package/packages/ct-skills/skills/ct-grade/agents/blind-comparator.md +157 -0
  27. package/packages/ct-skills/skills/ct-grade/agents/scenario-runner.md +134 -0
  28. package/packages/ct-skills/skills/ct-grade/eval-viewer/generate_grade_review.py +1138 -0
  29. package/packages/ct-skills/skills/ct-grade/eval-viewer/generate_grade_viewer.py +544 -0
  30. package/packages/ct-skills/skills/ct-grade/eval-viewer/generate_review.py +283 -0
  31. package/packages/ct-skills/skills/ct-grade/eval-viewer/grade-review.html +1574 -0
  32. package/packages/ct-skills/skills/ct-grade/eval-viewer/viewer.html +219 -0
  33. package/packages/ct-skills/skills/ct-grade/evals/evals.json +94 -0
  34. package/packages/ct-skills/skills/ct-grade/references/ab-test-methodology.md +150 -0
  35. package/packages/ct-skills/skills/ct-grade/references/domains.md +137 -0
  36. package/packages/ct-skills/skills/ct-grade/references/grade-spec.md +236 -0
  37. package/packages/ct-skills/skills/ct-grade/references/scenario-playbook.md +234 -0
  38. package/packages/ct-skills/skills/ct-grade/references/token-tracking.md +120 -0
  39. package/packages/ct-skills/skills/ct-grade/scripts/audit_analyzer.py +279 -0
  40. package/packages/ct-skills/skills/ct-grade/scripts/generate_report.py +283 -0
  41. package/packages/ct-skills/skills/ct-grade/scripts/run_ab_test.py +504 -0
  42. package/packages/ct-skills/skills/ct-grade/scripts/run_all.py +287 -0
  43. package/packages/ct-skills/skills/ct-grade/scripts/setup_run.py +183 -0
  44. package/packages/ct-skills/skills/ct-grade/scripts/token_tracker.py +630 -0
  45. package/packages/ct-skills/skills/ct-grade-v2-1/SKILL.md +237 -0
  46. package/packages/ct-skills/skills/ct-grade-v2-1/agents/analysis-reporter.md +203 -0
  47. package/packages/ct-skills/skills/ct-grade-v2-1/agents/blind-comparator.md +157 -0
  48. package/packages/ct-skills/skills/ct-grade-v2-1/agents/scenario-runner.md +179 -0
  49. package/packages/ct-skills/skills/ct-grade-v2-1/evals/evals.json +74 -0
  50. package/packages/ct-skills/skills/ct-grade-v2-1/grade-viewer/build_op_stats.py +174 -0
  51. package/packages/ct-skills/skills/ct-grade-v2-1/grade-viewer/eval-analysis.json +41 -0
  52. package/packages/ct-skills/skills/ct-grade-v2-1/grade-viewer/eval-report.md +34 -0
  53. package/packages/ct-skills/skills/ct-grade-v2-1/grade-viewer/generate_grade_review.py +1023 -0
  54. package/packages/ct-skills/skills/ct-grade-v2-1/grade-viewer/generate_grade_viewer.py +548 -0
  55. package/packages/ct-skills/skills/ct-grade-v2-1/grade-viewer/grade-review-eval.html +613 -0
  56. package/packages/ct-skills/skills/ct-grade-v2-1/grade-viewer/grade-review.html +1532 -0
  57. package/packages/ct-skills/skills/ct-grade-v2-1/grade-viewer/viewer.html +620 -0
  58. package/packages/ct-skills/skills/ct-grade-v2-1/manifest-entry.json +31 -0
  59. package/packages/ct-skills/skills/ct-grade-v2-1/references/ab-testing.md +233 -0
  60. package/packages/ct-skills/skills/ct-grade-v2-1/references/domains-ssot.md +156 -0
  61. package/packages/ct-skills/skills/ct-grade-v2-1/references/grade-spec-v2.md +167 -0
  62. package/packages/ct-skills/skills/ct-grade-v2-1/references/playbook-v2.md +393 -0
  63. package/packages/ct-skills/skills/ct-grade-v2-1/references/token-tracking.md +202 -0
  64. package/packages/ct-skills/skills/ct-grade-v2-1/scripts/generate_report.py +419 -0
  65. package/packages/ct-skills/skills/ct-grade-v2-1/scripts/run_ab_test.py +493 -0
  66. package/packages/ct-skills/skills/ct-grade-v2-1/scripts/run_scenario.py +396 -0
  67. package/packages/ct-skills/skills/ct-grade-v2-1/scripts/setup_run.py +207 -0
  68. package/packages/ct-skills/skills/ct-grade-v2-1/scripts/token_tracker.py +175 -0
  69. package/packages/ct-skills/skills/ct-orchestrator/SKILL.md +1 -29
  70. package/packages/ct-skills/skills/ct-orchestrator/manifest-entry.json +19 -0
  71. package/packages/ct-skills/skills/ct-skill-creator/SKILL.md +0 -12
  72. package/packages/ct-skills/skills/ct-skill-creator/agents/analyzer.md +276 -0
  73. package/packages/ct-skills/skills/ct-skill-creator/agents/comparator.md +204 -0
  74. package/packages/ct-skills/skills/ct-skill-creator/agents/grader.md +225 -0
  75. package/packages/ct-skills/skills/ct-skill-creator/assets/eval_review.html +146 -0
  76. package/packages/ct-skills/skills/ct-skill-creator/eval-viewer/generate_review.py +471 -0
  77. package/packages/ct-skills/skills/ct-skill-creator/eval-viewer/viewer.html +1325 -0
  78. package/packages/ct-skills/skills/ct-skill-creator/manifest-entry.json +17 -0
  79. package/packages/ct-skills/skills/ct-skill-creator/references/dynamic-context.md +228 -0
  80. package/packages/ct-skills/skills/ct-skill-creator/references/frontmatter.md +83 -0
  81. package/packages/ct-skills/skills/ct-skill-creator/references/invocation-control.md +165 -0
  82. package/packages/ct-skills/skills/ct-skill-creator/references/provider-deployment.md +175 -0
  83. package/packages/ct-skills/skills/ct-skill-creator/references/schemas.md +430 -0
  84. package/packages/ct-skills/skills/ct-skill-creator/scripts/__init__.py +1 -0
  85. package/packages/ct-skills/skills/ct-skill-creator/scripts/aggregate_benchmark.py +401 -0
  86. package/packages/ct-skills/skills/ct-skill-creator/scripts/generate_report.py +326 -0
  87. package/packages/ct-skills/skills/ct-skill-creator/scripts/improve_description.py +247 -0
  88. package/packages/ct-skills/skills/ct-skill-creator/scripts/run_eval.py +310 -0
  89. package/packages/ct-skills/skills/ct-skill-creator/scripts/run_loop.py +328 -0
  90. package/packages/ct-skills/skills/ct-skill-creator/scripts/utils.py +47 -0
  91. package/packages/ct-skills/skills/ct-skill-validator/SKILL.md +178 -0
  92. package/packages/ct-skills/skills/ct-skill-validator/agents/ecosystem-checker.md +151 -0
  93. package/packages/ct-skills/skills/ct-skill-validator/assets/valid-skill-example.md +13 -0
  94. package/packages/ct-skills/skills/ct-skill-validator/evals/eval_set.json +14 -0
  95. package/packages/ct-skills/skills/ct-skill-validator/evals/evals.json +52 -0
  96. package/packages/ct-skills/skills/ct-skill-validator/manifest-entry.json +20 -0
  97. package/packages/ct-skills/skills/ct-skill-validator/references/cleo-ecosystem-rules.md +163 -0
  98. package/packages/ct-skills/skills/ct-skill-validator/references/validation-rules.md +168 -0
  99. package/packages/ct-skills/skills/ct-skill-validator/scripts/__init__.py +0 -0
  100. package/packages/ct-skills/skills/ct-skill-validator/scripts/audit_body.py +242 -0
  101. package/packages/ct-skills/skills/ct-skill-validator/scripts/check_ecosystem.py +169 -0
  102. package/packages/ct-skills/skills/ct-skill-validator/scripts/check_manifest.py +172 -0
  103. package/packages/ct-skills/skills/ct-skill-validator/scripts/generate_validation_report.py +442 -0
  104. package/packages/ct-skills/skills/ct-skill-validator/scripts/validate.py +422 -0
  105. /package/{drizzle → migrations/drizzle-tasks}/20260224040019_baseline/migration.sql +0 -0
  106. /package/{drizzle → migrations/drizzle-tasks}/20260224040019_baseline/snapshot.json +0 -0
  107. /package/{drizzle → migrations/drizzle-tasks}/20260224040238_add-audit-log/migration.sql +0 -0
  108. /package/{drizzle → migrations/drizzle-tasks}/20260224040238_add-audit-log/snapshot.json +0 -0
  109. /package/{drizzle → migrations/drizzle-tasks}/20260224144602_closed_grim_reaper/migration.sql +0 -0
  110. /package/{drizzle → migrations/drizzle-tasks}/20260224144602_closed_grim_reaper/snapshot.json +0 -0
  111. /package/{drizzle → migrations/drizzle-tasks}/20260225024442_sync-lifecycle-enums-and-arch-decisions/migration.sql +0 -0
  112. /package/{drizzle → migrations/drizzle-tasks}/20260225024442_sync-lifecycle-enums-and-arch-decisions/snapshot.json +0 -0
  113. /package/{drizzle → migrations/drizzle-tasks}/20260227014821_adr-system-and-status-registry/migration.sql +0 -0
  114. /package/{drizzle → migrations/drizzle-tasks}/20260227014821_adr-system-and-status-registry/snapshot.json +0 -0
  115. /package/{drizzle → migrations/drizzle-tasks}/20260227021231_add-cancelled-pipeline-status/migration.sql +0 -0
  116. /package/{drizzle → migrations/drizzle-tasks}/20260227021231_add-cancelled-pipeline-status/snapshot.json +0 -0
  117. /package/{drizzle → migrations/drizzle-tasks}/20260227022417_adr-cognitive-search-fields/migration.sql +0 -0
  118. /package/{drizzle → migrations/drizzle-tasks}/20260227022417_adr-cognitive-search-fields/snapshot.json +0 -0
  119. /package/{drizzle → migrations/drizzle-tasks}/20260227172236_freezing_grey_gargoyle/migration.sql +0 -0
  120. /package/{drizzle → migrations/drizzle-tasks}/20260227172236_freezing_grey_gargoyle/snapshot.json +0 -0
  121. /package/{drizzle → migrations/drizzle-tasks}/20260227183444_fix-orphaned-parent-ids/migration.sql +0 -0
  122. /package/{drizzle → migrations/drizzle-tasks}/20260227183444_fix-orphaned-parent-ids/snapshot.json +0 -0
  123. /package/{drizzle → migrations/drizzle-tasks}/20260227183521_parent-id-on-delete-set-null/migration.sql +0 -0
  124. /package/{drizzle → migrations/drizzle-tasks}/20260227183521_parent-id-on-delete-set-null/snapshot.json +0 -0
  125. /package/{drizzle → migrations/drizzle-tasks}/20260227200430_numerous_mysterio/migration.sql +0 -0
  126. /package/{drizzle → migrations/drizzle-tasks}/20260227200430_numerous_mysterio/snapshot.json +0 -0
  127. /package/{drizzle → migrations/drizzle-tasks}/20260227235745_add-audit-log-dispatch-columns/migration.sql +0 -0
  128. /package/{drizzle → migrations/drizzle-tasks}/20260227235745_add-audit-log-dispatch-columns/snapshot.json +0 -0
  129. /package/{drizzle → migrations/drizzle-tasks}/20260301053344_careless_changeling/migration.sql +0 -0
  130. /package/{drizzle → migrations/drizzle-tasks}/20260301053344_careless_changeling/snapshot.json +0 -0
  131. /package/{drizzle → migrations/drizzle-tasks}/20260301175940_futuristic_eternity/migration.sql +0 -0
  132. /package/{drizzle → migrations/drizzle-tasks}/20260301175940_futuristic_eternity/snapshot.json +0 -0
  133. /package/{drizzle → migrations/drizzle-tasks}/20260301180528_update-task-relations-check-constraint/migration.sql +0 -0
  134. /package/{drizzle → migrations/drizzle-tasks}/20260301180528_update-task-relations-check-constraint/snapshot.json +0 -0
  135. /package/{drizzle → migrations/drizzle-tasks}/20260302163443_free_silk_fever/migration.sql +0 -0
  136. /package/{drizzle → migrations/drizzle-tasks}/20260302163443_free_silk_fever/snapshot.json +0 -0
  137. /package/{drizzle → migrations/drizzle-tasks}/20260302163457_robust_johnny_storm/migration.sql +0 -0
  138. /package/{drizzle → migrations/drizzle-tasks}/20260302163457_robust_johnny_storm/snapshot.json +0 -0
  139. /package/{drizzle → migrations/drizzle-tasks}/20260302163511_late_sphinx/migration.sql +0 -0
  140. /package/{drizzle → migrations/drizzle-tasks}/20260302163511_late_sphinx/snapshot.json +0 -0
  141. /package/{drizzle → migrations/drizzle-tasks}/20260305011924_cheerful_mongu/migration.sql +0 -0
  142. /package/{drizzle → migrations/drizzle-tasks}/20260305011924_cheerful_mongu/snapshot.json +0 -0
  143. /package/{drizzle → migrations/drizzle-tasks}/20260305203927_demonic_storm/migration.sql +0 -0
  144. /package/{drizzle → migrations/drizzle-tasks}/20260305203927_demonic_storm/snapshot.json +0 -0
  145. /package/{drizzle → migrations/drizzle-tasks}/20260306001243_spooky_rage/migration.sql +0 -0
  146. /package/{drizzle → migrations/drizzle-tasks}/20260306001243_spooky_rage/snapshot.json +0 -0
  147. /package/{drizzle → migrations/drizzle-tasks}/20260306193138_young_morbius/migration.sql +0 -0
  148. /package/{drizzle → migrations/drizzle-tasks}/20260306193138_young_morbius/snapshot.json +0 -0
  149. /package/{drizzle → migrations/drizzle-tasks}/20260306194959_sticky_captain_flint/migration.sql +0 -0
  150. /package/{drizzle → migrations/drizzle-tasks}/20260306194959_sticky_captain_flint/snapshot.json +0 -0
@@ -0,0 +1,1574 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>ct-grade — Grade Review</title>
7
+ <link rel="preconnect" href="https://fonts.googleapis.com">
8
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
9
+ <link href="https://fonts.googleapis.com/css2?family=Poppins:wght@500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
10
+ <style>
11
+ :root {
12
+ --bg: #0d1117;
13
+ --surface: #161b22;
14
+ --surface2: #1c2128;
15
+ --border: #30363d;
16
+ --text: #e6edf3;
17
+ --text-muted: #7d8590;
18
+ --accent: #f78166;
19
+ --radius: 6px;
20
+ --mono: 'JetBrains Mono', monospace;
21
+ --grade-a: #3fb950; --grade-a-bg: #0d2818;
22
+ --grade-b: #58a6ff; --grade-b-bg: #0d1f3c;
23
+ --grade-c: #e3b341; --grade-c-bg: #2d1d00;
24
+ --grade-d: #f0883e; --grade-d-bg: #2c1700;
25
+ --grade-f: #f85149; --grade-f-bg: #2d0f0d;
26
+ --mcp: #58a6ff; --cli: #a371f7; --tie: #7d8590;
27
+ --win: #3fb950; --loss: #f85149;
28
+ }
29
+ * { box-sizing: border-box; margin: 0; padding: 0; }
30
+ body { font-family: 'Poppins', sans-serif; background: var(--bg); color: var(--text); height: 100vh; display: flex; flex-direction: column; font-size: 14px; }
31
+
32
+ /* Header */
33
+ .header { background: var(--surface); border-bottom: 1px solid var(--border); padding: .75rem 1.5rem; display: flex; justify-content: space-between; align-items: center; flex-shrink: 0; gap: 1rem; }
34
+ .header-left { display: flex; align-items: center; gap: .75rem; }
35
+ .header-logo { font-size: .65rem; font-weight: 700; letter-spacing: .12em; text-transform: uppercase; background: var(--accent); color: #fff; padding: .2em .5em; border-radius: 3px; }
36
+ .header-title { font-size: 1rem; font-weight: 600; }
37
+ .header-subtitle { font-size: .75rem; color: var(--text-muted); }
38
+ .header-meta { font-size: .7rem; color: var(--text-muted); text-align: right; line-height: 1.6; }
39
+
40
+ /* Tabs */
41
+ .tabs-bar { background: var(--surface); border-bottom: 1px solid var(--border); display: flex; padding: 0 1.5rem; gap: .25rem; flex-shrink: 0; overflow-x: auto; }
42
+ .tab-btn { font-family: 'Poppins', sans-serif; font-size: .75rem; font-weight: 500; padding: .6rem .9rem; border: none; background: transparent; color: var(--text-muted); cursor: pointer; border-bottom: 2px solid transparent; transition: color .15s, border-color .15s; letter-spacing: .02em; white-space: nowrap; }
43
+ .tab-btn:hover { color: var(--text); }
44
+ .tab-btn.active { color: var(--accent); border-bottom-color: var(--accent); }
45
+ .tab-badge { display: inline-block; background: var(--surface2); border-radius: 9px; font-size: .65rem; padding: .1em .45em; margin-left: .3em; color: var(--text-muted); }
46
+ .tab-btn.active .tab-badge { background: rgba(247,129,102,.15); color: var(--accent); }
47
+
48
+ /* Layout */
49
+ .main { display: flex; flex: 1; overflow: hidden; }
50
+ .tab-panel { display: none; width: 100%; overflow: hidden; }
51
+ .tab-panel.active { display: flex; }
52
+ .sidebar { width: 260px; min-width: 200px; border-right: 1px solid var(--border); background: var(--surface); display: flex; flex-direction: column; flex-shrink: 0; overflow-y: auto; }
53
+ .sidebar-header { font-size: .65rem; font-weight: 600; letter-spacing: .08em; text-transform: uppercase; color: var(--text-muted); padding: .75rem 1rem .4rem; }
54
+ .sidebar-item { padding: .55rem 1rem; cursor: pointer; display: flex; align-items: center; gap: .5rem; border-left: 3px solid transparent; transition: background .1s; font-size: .8rem; }
55
+ .sidebar-item:hover { background: var(--surface2); }
56
+ .sidebar-item.active { background: var(--surface2); border-left-color: var(--accent); }
57
+ .item-label { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
58
+ .item-score { font-family: var(--mono); font-size: .7rem; font-weight: 500; }
59
+ .item-scenario { font-size: .6rem; color: var(--text-muted); background: var(--surface2); border: 1px solid var(--border); padding: .05em .35em; border-radius: 3px; margin-left: .25rem; }
60
+ .content { flex: 1; overflow-y: auto; padding: 1.5rem; display: flex; flex-direction: column; gap: 1.25rem; }
61
+ .scroll-panel { width: 100%; padding: 1.5rem; overflow-y: auto; display: flex; flex-direction: column; gap: 1.25rem; }
62
+
63
+ /* Cards */
64
+ .card { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius); padding: 1.25rem; }
65
+ .card-header { display: flex; align-items: flex-start; justify-content: space-between; gap: 1rem; margin-bottom: 1rem; }
66
+ .score-block { display: flex; align-items: center; gap: 1rem; }
67
+ .letter-badge { font-size: 2.5rem; font-weight: 700; width: 64px; height: 64px; display: flex; align-items: center; justify-content: center; border-radius: 8px; }
68
+ .letter-badge.sm { font-size: 1.1rem; width: 36px; height: 36px; border-radius: 5px; }
69
+ .letter-badge.A { background: var(--grade-a-bg); color: var(--grade-a); }
70
+ .letter-badge.B { background: var(--grade-b-bg); color: var(--grade-b); }
71
+ .letter-badge.C { background: var(--grade-c-bg); color: var(--grade-c); }
72
+ .letter-badge.D { background: var(--grade-d-bg); color: var(--grade-d); }
73
+ .letter-badge.F { background: var(--grade-f-bg); color: var(--grade-f); }
74
+ .score-details h2 { font-size: 1.5rem; font-weight: 700; line-height: 1; }
75
+ .score-details .pct { font-size: .85rem; color: var(--text-muted); }
76
+ .session-chip { font-family: var(--mono); font-size: .65rem; color: var(--text-muted); background: var(--surface2); padding: .2em .5em; border-radius: 3px; }
77
+ .section-title { font-size: .75rem; font-weight: 600; text-transform: uppercase; letter-spacing: .08em; color: var(--text-muted); margin-bottom: .75rem; }
78
+
79
+ /* Dimensions */
80
+ .dimensions { display: flex; flex-direction: column; gap: .65rem; }
81
+ .dim-row { display: flex; align-items: center; gap: .75rem; }
82
+ .dim-name { font-size: .7rem; font-weight: 500; width: 155px; flex-shrink: 0; }
83
+ .dim-bar-wrap { flex: 1; height: 8px; background: var(--surface2); border-radius: 4px; overflow: hidden; }
84
+ .dim-bar { height: 100%; border-radius: 4px; transition: width .4s ease; }
85
+ .dim-score { font-family: var(--mono); font-size: .7rem; width: 42px; text-align: right; flex-shrink: 0; }
86
+
87
+ /* Flags */
88
+ .flags-section h3 { font-size: .75rem; font-weight: 600; color: var(--text-muted); text-transform: uppercase; letter-spacing: .07em; margin-bottom: .6rem; }
89
+ .flag-item { display: flex; align-items: flex-start; gap: .5rem; font-size: .78rem; padding: .4rem .6rem; background: var(--grade-f-bg); border-left: 3px solid var(--grade-f); border-radius: 0 var(--radius) var(--radius) 0; line-height: 1.4; margin-bottom: .35rem; }
90
+ .no-flags { font-size: .78rem; color: var(--win); background: var(--grade-a-bg); padding: .4rem .6rem; border-left: 3px solid var(--win); border-radius: 0 var(--radius) var(--radius) 0; }
91
+
92
+ /* Evidence collapsible */
93
+ .evidence-toggle { font-size: .72rem; color: var(--accent); cursor: pointer; background: none; border: none; padding: .25rem 0; }
94
+ .evidence-toggle:hover { text-decoration: underline; }
95
+ .evidence-list { display: none; padding-left: .75rem; }
96
+ .evidence-list.open { display: block; }
97
+ .ev-bullet { font-size: .74rem; padding: .15rem 0; color: var(--text-muted); }
98
+
99
+ /* Token meta */
100
+ .token-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(130px, 1fr)); gap: .6rem; }
101
+ .token-stat { background: var(--surface2); border-radius: var(--radius); padding: .6rem .75rem; }
102
+ .token-stat-label { font-size: .6rem; font-weight: 600; text-transform: uppercase; letter-spacing: .07em; color: var(--text-muted); margin-bottom: .2rem; }
103
+ .token-stat-value { font-family: var(--mono); font-size: .95rem; font-weight: 500; }
104
+ .confidence-badge { display: inline-block; padding: .15em .5em; border-radius: 9px; font-size: .65rem; font-weight: 600; text-transform: uppercase; }
105
+ .confidence-badge.real { background: var(--grade-a-bg); color: var(--grade-a); }
106
+ .confidence-badge.high { background: var(--grade-b-bg); color: var(--grade-b); }
107
+ .confidence-badge.estimated { background: var(--grade-c-bg); color: var(--grade-c); }
108
+ .confidence-badge.coarse { background: var(--grade-d-bg); color: var(--grade-d); }
109
+
110
+ /* Tables */
111
+ table.data-table { width: 100%; border-collapse: collapse; font-size: .76rem; }
112
+ .data-table th { font-size: .65rem; font-weight: 600; text-transform: uppercase; letter-spacing: .07em; color: var(--text-muted); padding: .5rem .75rem; text-align: left; background: var(--surface); border-bottom: 1px solid var(--border); white-space: nowrap; position: sticky; top: 0; z-index: 1; }
113
+ .data-table td { padding: .45rem .75rem; border-bottom: 1px solid var(--border); vertical-align: middle; }
114
+ .data-table tr:hover td { background: var(--surface2); }
115
+ .table-wrap { overflow-x: auto; }
116
+ .op-name { font-family: var(--mono); font-size: .72rem; }
117
+
118
+ /* Pills */
119
+ .pill { display: inline-block; padding: .15em .5em; border-radius: 9px; font-size: .65rem; font-weight: 600; text-transform: uppercase; white-space: nowrap; }
120
+ .pill.mcp { background: rgba(88,166,255,.15); color: var(--mcp); }
121
+ .pill.cli { background: rgba(163,113,247,.15); color: var(--cli); }
122
+ .pill.tie { background: var(--surface2); color: var(--text-muted); }
123
+ .pill.query { background: rgba(88,166,255,.15); color: var(--mcp); }
124
+ .pill.mutate { background: rgba(247,129,102,.15); color: var(--accent); }
125
+ .pill.active { background: var(--grade-a-bg); color: var(--grade-a); }
126
+ .pill.ended { background: var(--surface2); color: var(--text-muted); }
127
+ .pill.pass { background: var(--grade-a-bg); color: var(--grade-a); }
128
+ .pill.fail { background: var(--grade-f-bg); color: var(--grade-f); }
129
+ .pill.not-run { background: var(--surface2); color: var(--text-muted); }
130
+
131
+ /* Delta colors */
132
+ .delta-pos { color: var(--loss); font-family: var(--mono); font-size: .72rem; }
133
+ .delta-neg { color: var(--win); font-family: var(--mono); font-size: .72rem; }
134
+ .delta-zero { color: var(--text-muted); font-family: var(--mono); font-size: .72rem; }
135
+
136
+ /* Bar charts */
137
+ .bar-chart { display: flex; flex-direction: column; gap: .5rem; }
138
+ .bar-row { display: flex; align-items: center; gap: .75rem; }
139
+ .bar-label { font-family: var(--mono); font-size: .72rem; width: 130px; flex-shrink: 0; text-align: right; }
140
+ .bar-track { flex: 1; height: 16px; background: var(--surface2); border-radius: 3px; overflow: hidden; }
141
+ .bar-fill { height: 100%; border-radius: 3px; transition: width .4s ease; }
142
+ .bar-val { font-family: var(--mono); font-size: .7rem; width: 80px; flex-shrink: 0; }
143
+ .chart-card { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius); padding: 1rem 1.25rem; }
144
+ .chart-title { font-size: .8rem; font-weight: 600; margin-bottom: 1rem; }
145
+
146
+ /* Summary grid */
147
+ .stat-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); gap: .75rem; }
148
+ .stat-card { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius); padding: .85rem 1rem; }
149
+ .stat-label { font-size: .65rem; font-weight: 600; text-transform: uppercase; letter-spacing: .07em; color: var(--text-muted); margin-bottom: .3rem; }
150
+ .stat-value { font-family: var(--mono); font-size: 1.3rem; font-weight: 500; }
151
+ .stat-value.win { color: var(--win); }
152
+ .stat-value.loss { color: var(--loss); }
153
+ .stat-value.neutral { color: var(--text-muted); }
154
+
155
+ /* Winner banner */
156
+ .winner-banner { display: flex; align-items: center; gap: .6rem; padding: .6rem 1rem; border-radius: var(--radius); font-size: .8rem; font-weight: 600; }
157
+ .winner-banner.mcp { background: rgba(88,166,255,.1); color: var(--mcp); }
158
+ .winner-banner.cli { background: rgba(163,113,247,.1); color: var(--cli); }
159
+ .winner-banner.tie { background: var(--surface2); color: var(--text-muted); }
160
+
161
+ /* Filter bar */
162
+ .filter-bar { display: flex; gap: .75rem; flex-wrap: wrap; align-items: center; padding: .5rem 0; }
163
+ .filter-bar select, .filter-bar input { font-family: var(--mono); font-size: .72rem; background: var(--surface2); color: var(--text); border: 1px solid var(--border); border-radius: var(--radius); padding: .35rem .5rem; outline: none; }
164
+ .filter-bar select:focus, .filter-bar input:focus { border-color: var(--accent); }
165
+ .filter-bar label { font-size: .68rem; color: var(--text-muted); font-weight: 500; }
166
+
167
+ /* Session row expand */
168
+ .session-row { cursor: pointer; }
169
+ .session-expand { display: none; }
170
+ .session-expand.open { display: table-row; }
171
+ .session-expand td { padding: 1rem; background: var(--surface2); }
172
+ .pulse-dot { display: inline-block; width: 8px; height: 8px; border-radius: 50%; background: var(--grade-a); animation: pulse 1.5s ease-in-out infinite; margin-right: .35rem; vertical-align: middle; }
173
+ @keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: .3; } }
174
+
175
+ /* Session detail expand */
176
+ .session-detail { padding: .25rem 0; }
177
+ .session-detail-header { display: flex; align-items: center; gap: 1.5rem; padding: .5rem 0 .75rem; flex-wrap: wrap; }
178
+ .session-detail-stat { font-size: .72rem; color: var(--text-muted); }
179
+ .session-detail-stat strong { color: var(--text); font-weight: 600; }
180
+ .token-summary { display: flex; gap: 1.25rem; padding: .5rem .75rem; background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius); margin-bottom: .75rem; flex-wrap: wrap; }
181
+ .token-stat { font-size: .72rem; color: var(--text-muted); }
182
+ .token-stat strong { font-family: var(--mono); color: var(--accent); }
183
+ .audit-table-wrap { overflow-x: auto; }
184
+ .audit-table { width: 100%; border-collapse: collapse; font-size: .72rem; }
185
+ .audit-table th { text-align: left; padding: .4rem .6rem; font-size: .65rem; font-weight: 600; text-transform: uppercase; letter-spacing: .07em; color: var(--text-muted); border-bottom: 1px solid var(--border); white-space: nowrap; }
186
+ .audit-table td { padding: .35rem .6rem; border-bottom: 1px solid rgba(48,54,61,.5); vertical-align: middle; white-space: nowrap; }
187
+ .audit-table tr:last-child td { border-bottom: none; }
188
+ .audit-table tr:hover td { background: rgba(255,255,255,.02); }
189
+ .audit-time { font-family: var(--mono); color: var(--text-muted); font-size: .68rem; }
190
+ .audit-domain { font-family: var(--mono); font-size: .72rem; }
191
+ .audit-op { font-family: var(--mono); font-size: .75rem; font-weight: 500; }
192
+ .audit-source { font-size: .68rem; color: var(--text-muted); }
193
+ .audit-ms { font-family: var(--mono); font-size: .7rem; text-align: right; color: var(--text-muted); }
194
+ .audit-ms.slow { color: var(--grade-c); }
195
+ .audit-ms.vslow { color: var(--grade-d); }
196
+ .audit-success { text-align: center; font-size: .85rem; }
197
+ .gw-pill { display: inline-block; padding: .12em .5em; border-radius: 3px; font-size: .65rem; font-weight: 700; letter-spacing: .04em; text-transform: uppercase; }
198
+ .gw-pill.mutate { background: rgba(247,129,102,.15); color: #f78166; border: 1px solid rgba(247,129,102,.3); }
199
+ .gw-pill.query { background: rgba(88,166,255,.15); color: #58a6ff; border: 1px solid rgba(88,166,255,.3); }
200
+ .audit-section-title { font-size: .65rem; font-weight: 600; text-transform: uppercase; letter-spacing: .08em; color: var(--text-muted); padding: .6rem 0 .4rem; }
201
+ .audit-loading { font-size: .75rem; color: var(--text-muted); padding: .5rem 0; font-style: italic; }
202
+ .audit-count-chip { font-family: var(--mono); font-size: .65rem; background: var(--surface); border: 1px solid var(--border); border-radius: 9px; padding: .1em .5em; margin-left: .5rem; color: var(--text-muted); }
203
+ .session-chain { display: flex; align-items: center; gap: .5rem; padding: .4rem 0 .6rem; font-size: .72rem; flex-wrap: wrap; }
204
+ .session-chain-label { color: var(--text-muted); font-size: .65rem; text-transform: uppercase; letter-spacing: .07em; margin-right: .25rem; }
205
+ .chain-sep { color: var(--text-muted); }
206
+ .chain-current { background: var(--accent); color: #fff; }
207
+ .session-scope { font-size: .72rem; color: var(--text-muted); padding: .25rem 0 .5rem; }
208
+ .session-scope strong { color: var(--text); }
209
+ .debrief-card { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius); padding: .6rem .85rem; margin-bottom: .6rem; display: flex; flex-direction: column; gap: .35rem; }
210
+ .debrief-row { font-size: .72rem; display: flex; align-items: center; gap: .5rem; flex-wrap: wrap; }
211
+ .debrief-label { color: var(--text-muted); font-size: .68rem; text-transform: uppercase; letter-spacing: .06em; min-width: 90px; }
212
+ .debrief-note { font-size: .73rem; color: var(--text); font-style: italic; padding: .25rem 0; }
213
+ .git-branch { font-family: var(--mono); font-size: .7rem; color: var(--grade-b); }
214
+ .git-hash { font-family: var(--mono); font-size: .68rem; color: var(--text-muted); }
215
+ .uncommitted-badge { background: rgba(248,81,73,.15); color: var(--grade-f); border: 1px solid rgba(248,81,73,.3); padding: .1em .4em; border-radius: 3px; font-size: .63rem; font-weight: 600; }
216
+ .clean-badge { background: rgba(63,185,80,.15); color: var(--grade-a); border: 1px solid rgba(63,185,80,.3); padding: .1em .4em; border-radius: 3px; font-size: .63rem; }
217
+ .notes-toggle { font-size: .68rem; color: var(--accent); cursor: pointer; user-select: none; }
218
+ .notes-list { display: none; }
219
+ .notes-list.open { display: block; }
220
+ .note-entry { font-size: .7rem; color: var(--text-muted); padding: .2rem 0; border-bottom: 1px solid var(--border); display: flex; gap: .5rem; }
221
+ .note-ts { font-family: var(--mono); color: var(--text-muted); opacity: .6; flex-shrink: 0; font-size: .67rem; }
222
+ .note-text { color: var(--text); }
223
+
224
+ /* Eval cards */
225
+ .eval-card { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius); padding: 1rem 1.25rem; }
226
+ .eval-card-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: .75rem; }
227
+ .eval-card-title { font-weight: 600; font-size: .85rem; }
228
+ .pass-rate-bar { width: 120px; height: 6px; background: var(--surface2); border-radius: 3px; overflow: hidden; }
229
+ .pass-rate-fill { height: 100%; border-radius: 3px; background: var(--grade-a); }
230
+ .expectation-row { display: flex; align-items: flex-start; gap: .5rem; font-size: .76rem; padding: .3rem 0; border-bottom: 1px solid var(--border); }
231
+ .expectation-row:last-child { border-bottom: none; }
232
+ .expect-icon { flex-shrink: 0; font-size: .8rem; }
233
+ .expect-evidence { font-size: .7rem; color: var(--text-muted); margin-top: .15rem; }
234
+
235
+ /* Note block */
236
+ .note-block { font-size: .72rem; color: var(--text-muted); background: var(--grade-c-bg); border-left: 3px solid var(--grade-c); padding: .5rem .75rem; border-radius: 0 4px 4px 0; }
237
+ .api-card { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius); padding: 1rem 1.25rem; display: flex; flex-direction: column; gap: .85rem; }
238
+ .api-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: .75rem; }
239
+ .api-box { background: var(--surface2); border: 1px solid var(--border); border-radius: var(--radius); padding: .75rem .85rem; }
240
+ .api-box-title { font-size: .65rem; font-weight: 600; text-transform: uppercase; letter-spacing: .07em; color: var(--text-muted); margin-bottom: .45rem; }
241
+ .api-list { display: flex; flex-direction: column; gap: .3rem; }
242
+ .api-list code { font-family: var(--mono); font-size: .7rem; color: var(--text); }
243
+
244
+ /* Empty state */
245
+ .empty-state { display: flex; flex-direction: column; align-items: center; justify-content: center; gap: .75rem; padding: 3rem 2rem; color: var(--text-muted); text-align: center; }
246
+ .empty-state h3 { font-size: .9rem; color: var(--text); }
247
+ .empty-state p { font-size: .78rem; max-width: 380px; line-height: 1.6; }
248
+ .empty-cmd { font-family: var(--mono); background: var(--surface2); padding: .25em .5em; border-radius: 3px; font-size: .74rem; display: inline-block; margin-top: .25rem; }
249
+
250
+ /* Scrollbar */
251
+ ::-webkit-scrollbar { width: 6px; height: 6px; }
252
+ ::-webkit-scrollbar-track { background: transparent; }
253
+ ::-webkit-scrollbar-thumb { background: var(--border); border-radius: 3px; }
254
+ ::-webkit-scrollbar-thumb:hover { background: var(--text-muted); }
255
+ </style>
256
+ </head>
257
+ <body>
258
+
259
+ <div class="header">
260
+ <div class="header-left">
261
+ <span class="header-logo">CLEO</span>
262
+ <div>
263
+ <div class="header-title" id="hdr-title">ct-grade — Grade Review</div>
264
+ <div class="header-subtitle" id="hdr-sub"></div>
265
+ </div>
266
+ </div>
267
+ <div class="header-meta" id="hdr-meta"></div>
268
+ </div>
269
+
270
+ <div class="tabs-bar">
271
+ <button class="tab-btn active" data-tab="grades">Grades <span class="tab-badge" id="tb-grades">0</span></button>
272
+ <button class="tab-btn" data-tab="sessions">Sessions <span class="tab-badge" id="tb-sessions">0</span></button>
273
+ <button class="tab-btn" data-tab="ab">A/B <span class="tab-badge" id="tb-ab">0</span></button>
274
+ <button class="tab-btn" data-tab="tokens">Token Economy</button>
275
+ <button class="tab-btn" data-tab="matrix">Operation Matrix <span class="tab-badge" id="tb-matrix">0</span></button>
276
+ <button class="tab-btn" data-tab="evals">Eval Report <span class="tab-badge" id="tb-evals">0</span></button>
277
+ </div>
278
+
279
+ <div class="main">
280
+ <div class="tab-panel active" id="panel-grades">
281
+ <div class="sidebar" id="grade-sidebar"></div>
282
+ <div class="content" id="grade-content"></div>
283
+ </div>
284
+ <div class="tab-panel" id="panel-sessions">
285
+ <div class="scroll-panel" id="sessions-content"></div>
286
+ </div>
287
+ <div class="tab-panel" id="panel-ab">
288
+ <div class="scroll-panel" id="ab-content"></div>
289
+ </div>
290
+ <div class="tab-panel" id="panel-tokens">
291
+ <div class="scroll-panel" id="tokens-content"></div>
292
+ </div>
293
+ <div class="tab-panel" id="panel-matrix">
294
+ <div class="scroll-panel" id="matrix-content"></div>
295
+ </div>
296
+ <div class="tab-panel" id="panel-evals">
297
+ <div class="scroll-panel" id="evals-content"></div>
298
+ </div>
299
+ </div>
300
+
301
+ <script id="grade-data" type="application/json">{{EMBEDDED_DATA}}</script>
302
+ <script>
303
+ // ── Data ──────────────────────────────────────────────────────────────────────
304
+ var DATA = {};
305
+ try { DATA = JSON.parse(document.getElementById('grade-data').textContent || '{}'); } catch(e) { DATA = {}; }
306
+
307
+ // ── Helpers ───────────────────────────────────────────────────────────────────
308
+ function scoreToLetter(score, max) {
309
+ if (score == null) return '?';
310
+ var pct = (score / (max || 100)) * 100;
311
+ if (pct >= 90) return 'A';
312
+ if (pct >= 80) return 'B';
313
+ if (pct >= 70) return 'C';
314
+ if (pct >= 60) return 'D';
315
+ return 'F';
316
+ }
317
+ function gradeColor(letter) {
318
+ var m = { A: 'var(--grade-a)', B: 'var(--grade-b)', C: 'var(--grade-c)', D: 'var(--grade-d)', F: 'var(--grade-f)' };
319
+ return m[letter] || 'var(--text-muted)';
320
+ }
321
+ var DIM_NAMES = ['S1 Session Discipline', 'S2 Discovery Efficiency', 'S3 Task Hygiene', 'S4 Error Protocol', 'S5 Progressive Disclosure'];
322
+ var DIM_KEYS = ['sessionDiscipline', 'discoveryEfficiency', 'taskHygiene', 'errorProtocol', 'disclosureUse'];
323
+ var DIM_COLORS = ['#a371f7', '#58a6ff', '#3fb950', '#e3b341', '#f78166'];
324
+
325
+ function fmtNum(n, fallback) {
326
+ if (n == null) return fallback !== undefined ? fallback : '\u2014';
327
+ return typeof n === 'number' && !Number.isInteger(n) ? n.toFixed(1) : String(n);
328
+ }
329
+ function fmtDate(s) {
330
+ if (!s) return '';
331
+ try { return new Date(s).toLocaleString(undefined, { dateStyle: 'short', timeStyle: 'short' }); } catch(e) { return s; }
332
+ }
333
+ function fmtDuration(ms) {
334
+ if (!ms && ms !== 0) return '\u2014';
335
+ if (ms < 1000) return ms + 'ms';
336
+ var s = Math.round(ms / 1000);
337
+ if (s < 60) return s + 's';
338
+ var m = Math.floor(s / 60);
339
+ return m + 'm ' + (s % 60) + 's';
340
+ }
341
+
342
+ // Safe element builders
343
+ function mk(tag, cls, text) {
344
+ var e = document.createElement(tag);
345
+ if (cls) e.className = cls;
346
+ if (text != null) e.textContent = String(text);
347
+ return e;
348
+ }
349
+ function mks(tag, css, text) {
350
+ var e = document.createElement(tag);
351
+ if (css) e.style.cssText = css;
352
+ if (text != null) e.textContent = String(text);
353
+ return e;
354
+ }
355
+ function app(parent) {
356
+ for (var i = 1; i < arguments.length; i++) {
357
+ if (arguments[i] != null) parent.appendChild(arguments[i]);
358
+ }
359
+ return parent;
360
+ }
361
+ function emptyState(title, desc, cmd) {
362
+ var d = mk('div', 'empty-state');
363
+ app(d, mk('h3', null, title), mk('p', null, desc));
364
+ if (cmd) app(d, mk('span', 'empty-cmd', cmd));
365
+ return d;
366
+ }
367
+
368
+ // ── Tab switching ─────────────────────────────────────────────────────────────
369
+ document.querySelectorAll('.tab-btn').forEach(function(btn) {
370
+ btn.addEventListener('click', function() {
371
+ document.querySelectorAll('.tab-btn').forEach(function(b) { b.classList.remove('active'); });
372
+ document.querySelectorAll('.tab-panel').forEach(function(p) { p.classList.remove('active'); });
373
+ btn.classList.add('active');
374
+ document.getElementById('panel-' + btn.dataset.tab).classList.add('active');
375
+ });
376
+ });
377
+
378
+ // ── Tab 1: Grades ─────────────────────────────────────────────────────────────
379
+ function renderGradesTab(grades) {
380
+ var sidebar = document.getElementById('grade-sidebar');
381
+ var content = document.getElementById('grade-content');
382
+ sidebar.textContent = '';
383
+ content.textContent = '';
384
+ if (!grades || !grades.length) {
385
+ content.appendChild(emptyState('No grade results yet', 'Run a scenario to generate grades.', 'python scripts/run_scenario.py --scenario S1'));
386
+ return;
387
+ }
388
+ document.getElementById('tb-grades').textContent = grades.length;
389
+ app(sidebar, mk('div', 'sidebar-header', 'Sessions'));
390
+ grades.forEach(function(g, i) {
391
+ var score = g.totalScore != null ? g.totalScore : g.score;
392
+ var letter = scoreToLetter(score, g.maxScore);
393
+ var sid = (g.sessionId || '').slice(0, 22) || ('Run ' + (i + 1));
394
+ var item = mk('div', 'sidebar-item' + (i === 0 ? ' active' : ''));
395
+ var ls = mks('span', 'font-weight:700;font-size:.85rem;color:' + gradeColor(letter) + ';flex-shrink:0;width:18px;', letter);
396
+ var lab = mk('span', 'item-label', sid);
397
+ var sc = mk('span', 'item-score');
398
+ sc.textContent = score != null ? (score + '/100') : '\u2014';
399
+ sc.style.color = gradeColor(letter);
400
+ app(item, ls, lab);
401
+ if (g._scenarioId || g.scenario) {
402
+ app(item, mk('span', 'item-scenario', g._scenarioId || g.scenario));
403
+ }
404
+ app(item, sc);
405
+ item.addEventListener('click', (function(g2, item2) {
406
+ return function() {
407
+ sidebar.querySelectorAll('.sidebar-item').forEach(function(x) { x.classList.remove('active'); });
408
+ item2.classList.add('active');
409
+ renderGradeDetail(g2, content);
410
+ };
411
+ })(g, item));
412
+ sidebar.appendChild(item);
413
+ });
414
+ renderGradeDetail(grades[0], content);
415
+ }
416
+
417
+ function renderGradeDetail(g, container) {
418
+ container.textContent = '';
419
+ var score = g.totalScore != null ? g.totalScore : (g.score || 0);
420
+ var max = g.maxScore || 100;
421
+ var letter = scoreToLetter(score, max);
422
+ var pct = Math.round((score / max) * 100);
423
+ var dims = g.dimensions || {};
424
+ var flags = g.flags || [];
425
+ var tmeta = g._tokenMeta || null;
426
+ var scenario = g._scenarioId || g.scenario || '';
427
+
428
+ // Score card
429
+ var card = mk('div', 'card');
430
+ var hdr = mk('div', 'card-header');
431
+ var sb = mk('div', 'score-block');
432
+ var badge = mk('div', 'letter-badge ' + letter, letter);
433
+ var sd = mk('div', 'score-details');
434
+ var h2 = mk('h2'); h2.textContent = score + '/' + max; h2.style.color = gradeColor(letter);
435
+ var pctEl = mk('div', 'pct', pct + '% \u2014 Grade ' + letter + (scenario ? ' \u2014 ' + scenario : ''));
436
+ app(sd, h2, pctEl);
437
+ if (g.timestamp) app(sd, mks('div', 'font-size:.68rem;color:var(--text-muted);margin-top:.25rem;', fmtDate(g.timestamp)));
438
+ app(sb, badge, sd);
439
+ app(hdr, sb);
440
+ if (g.sessionId) app(hdr, mk('code', 'session-chip', g.sessionId));
441
+ card.appendChild(hdr);
442
+
443
+ // Dimension bars
444
+ var dimsWrap = mk('div', 'dimensions');
445
+ DIM_KEYS.forEach(function(key, i) {
446
+ var d = dims[key]; if (!d) return;
447
+ var ds = d.score || 0, dm = d.max || 20, dp = Math.round((ds / dm) * 100);
448
+ var row = mk('div', 'dim-row');
449
+ var bar = mk('div', 'dim-bar');
450
+ bar.style.cssText = 'width:' + dp + '%;background:' + DIM_COLORS[i] + ';';
451
+ var bw = mk('div', 'dim-bar-wrap'); bw.appendChild(bar);
452
+ var sl = mk('span', 'dim-score', ds + '/' + dm);
453
+ sl.style.color = DIM_COLORS[i];
454
+ app(row, mk('span', 'dim-name', DIM_NAMES[i]), bw, sl);
455
+ dimsWrap.appendChild(row);
456
+ });
457
+ card.appendChild(dimsWrap);
458
+ container.appendChild(card);
459
+
460
+ // Flags
461
+ var fs = mk('div', 'card flags-section');
462
+ var ft = mk('h3', null, 'Flags' + (flags.length ? ' (' + flags.length + ')' : ''));
463
+ fs.appendChild(ft);
464
+ if (!flags.length) {
465
+ fs.appendChild(mk('div', 'no-flags', '\u2713 Clean \u2014 all protocol checks passed'));
466
+ } else {
467
+ flags.forEach(function(f) {
468
+ var item = mk('div', 'flag-item');
469
+ app(item, mks('span', 'flex-shrink:0;', '\u26A0'), mk('span', null, f));
470
+ fs.appendChild(item);
471
+ });
472
+ }
473
+ container.appendChild(fs);
474
+
475
+ // Evidence (collapsed, expandable per dimension)
476
+ var hasEvidence = DIM_KEYS.some(function(k) { return dims[k] && dims[k].evidence && dims[k].evidence.length; });
477
+ if (hasEvidence) {
478
+ var ec = mk('div', 'card');
479
+ ec.appendChild(mk('div', 'section-title', 'Evidence'));
480
+ DIM_KEYS.forEach(function(key, i) {
481
+ var d = dims[key]; if (!d || !d.evidence || !d.evidence.length) return;
482
+ var dimLabel = mk('div', 'dim-name', DIM_NAMES[i]);
483
+ dimLabel.style.cssText = 'color:' + DIM_COLORS[i] + ';width:auto;margin-bottom:.25rem;';
484
+ ec.appendChild(dimLabel);
485
+ var toggle = mk('button', 'evidence-toggle', 'Show ' + d.evidence.length + ' evidence item(s)');
486
+ var list = mk('div', 'evidence-list');
487
+ d.evidence.forEach(function(ev) { list.appendChild(mk('div', 'ev-bullet', '\u2022 ' + ev)); });
488
+ toggle.addEventListener('click', function() {
489
+ var open = list.classList.toggle('open');
490
+ toggle.textContent = (open ? 'Hide' : 'Show ' + d.evidence.length) + ' evidence item(s)';
491
+ });
492
+ app(ec, toggle, list);
493
+ });
494
+ container.appendChild(ec);
495
+ }
496
+
497
+ // Token display
498
+ var tokCard = mk('div', 'card');
499
+ tokCard.appendChild(mk('div', 'section-title', 'Token Metadata'));
500
+ var grid = mk('div', 'token-grid');
501
+ function addStat(label, value) {
502
+ var s = mk('div', 'token-stat');
503
+ app(s, mk('div', 'token-stat-label', label), mk('div', 'token-stat-value', fmtNum(value, '\u2014')));
504
+ grid.appendChild(s);
505
+ }
506
+ if (tmeta && tmeta.confidence !== 'heuristic') {
507
+ var conf = (tmeta.confidence || tmeta.estimationMethod || 'estimated').toLowerCase();
508
+ var confBadge = mk('span', 'confidence-badge ' + conf, conf.toUpperCase());
509
+ var confWrap = mk('div', 'token-stat');
510
+ app(confWrap, mk('div', 'token-stat-label', 'Confidence'), confBadge);
511
+ grid.appendChild(confWrap);
512
+ if (tmeta.totalEstimatedTokens || tmeta.total_tokens) addStat('Total Tokens', tmeta.totalEstimatedTokens || tmeta.total_tokens);
513
+ if (tmeta.inputTokens) addStat('Input', tmeta.inputTokens);
514
+ if (tmeta.outputTokens) addStat('Output', tmeta.outputTokens);
515
+ if (tmeta.cacheReadTokens) addStat('Cache Read', tmeta.cacheReadTokens);
516
+ } else if (g.entryCount) {
517
+ var coarseBadge = mk('span', 'confidence-badge coarse', 'COARSE');
518
+ var cWrap = mk('div', 'token-stat');
519
+ app(cWrap, mk('div', 'token-stat-label', 'Confidence'), coarseBadge);
520
+ grid.appendChild(cWrap);
521
+ addStat('Estimate', '~' + (g.entryCount * 150) + 't');
522
+ addStat('Basis', g.entryCount + ' entries x ~150t');
523
+ } else {
524
+ addStat('Note', 'Enable OTEL for token data');
525
+ }
526
+ app(tokCard, grid);
527
+ container.appendChild(tokCard);
528
+ }
529
+
530
+ // ── Tab 2: Sessions ───────────────────────────────────────────────────────────
531
+ function renderSessionsTab(sessions) {
532
+ var container = document.getElementById('sessions-content');
533
+ container.textContent = '';
534
+ if (!sessions || !sessions.length) {
535
+ container.appendChild(emptyState('No sessions found', 'Session data will appear here after graded sessions run.'));
536
+ return;
537
+ }
538
+ document.getElementById('tb-sessions').textContent = sessions.length;
539
+
540
+ // Grade coverage summary
541
+ var summary = DATA.grade_summary || {};
542
+ if (summary.total > 0) {
543
+ var statsRow = mk('div', 'token-summary');
544
+ function gstat(label, val, color) {
545
+ var el = mk('div', 'token-stat');
546
+ var lbl = document.createElement('span'); lbl.textContent = label + ': ';
547
+ var v = document.createElement('strong');
548
+ v.textContent = val != null ? String(val) : '\u2014';
549
+ if (color) v.style.color = color;
550
+ el.appendChild(lbl); el.appendChild(v);
551
+ return el;
552
+ }
553
+ var letter = summary.avgScore != null ? scoreToLetter(summary.avgScore) : null;
554
+ app(statsRow,
555
+ gstat('Total grades', summary.total),
556
+ gstat('Graded sessions', summary.graded),
557
+ gstat('Avg score', summary.avgScore != null ? summary.avgScore + '/100' : null, letter ? gradeColor(letter) : null),
558
+ gstat('Best', summary.maxScore),
559
+ gstat('Worst', summary.minScore)
560
+ );
561
+ // Distribution badges
562
+ if (summary.distribution) {
563
+ var distEl = mk('div', 'token-stat');
564
+ var distLbl = document.createElement('span'); distLbl.textContent = 'Distribution: ';
565
+ distEl.appendChild(distLbl);
566
+ ['A','B','C','D','F'].forEach(function(l) {
567
+ var n = summary.distribution[l];
568
+ if (n) {
569
+ var badge = mk('span', 'pill', l + ':' + n);
570
+ badge.style.cssText = 'background:' + gradeColorBg(l) + ';color:' + gradeColor(l) + ';margin-left:3px;padding:.1em .4em;border-radius:3px;font-size:.65rem;font-weight:700;';
571
+ distEl.appendChild(badge);
572
+ }
573
+ });
574
+ statsRow.appendChild(distEl);
575
+ }
576
+ container.appendChild(statsRow);
577
+ }
578
+
579
+ // Filter bar
580
+ var filterBar = mk('div', 'filter-bar');
581
+ var statusSel = document.createElement('select');
582
+ [['all', 'All'], ['active', 'Active'], ['ended', 'Ended']].forEach(function(opt) {
583
+ var o = document.createElement('option'); o.value = opt[0]; o.textContent = opt[1]; statusSel.appendChild(o);
584
+ });
585
+ var sortSel = document.createElement('select');
586
+ [['newest', 'Newest first'], ['oldest', 'Oldest first'], ['score', 'By score']].forEach(function(opt) {
587
+ var o = document.createElement('option'); o.value = opt[0]; o.textContent = opt[1]; sortSel.appendChild(o);
588
+ });
589
+ var searchInput = document.createElement('input');
590
+ searchInput.type = 'text'; searchInput.placeholder = 'Search sessions...';
591
+ app(filterBar, mk('label', null, 'Status:'), statusSel, mk('label', null, 'Sort:'), sortSel, searchInput);
592
+ container.appendChild(filterBar);
593
+
594
+ var tableWrap = mk('div', 'table-wrap');
595
+ container.appendChild(tableWrap);
596
+
597
+ function renderTable() {
598
+ tableWrap.textContent = '';
599
+ var filtered = sessions.slice();
600
+ var statusVal = statusSel.value;
601
+ if (statusVal !== 'all') {
602
+ filtered = filtered.filter(function(s) {
603
+ var st = (s.status || '').toLowerCase();
604
+ return statusVal === 'active' ? (st === 'active' || st === 'running') : (st === 'ended' || st === 'completed');
605
+ });
606
+ }
607
+ var q = searchInput.value.toLowerCase();
608
+ if (q) {
609
+ filtered = filtered.filter(function(s) {
610
+ return (s.name || '').toLowerCase().indexOf(q) >= 0 ||
611
+ (s.sessionId || '').toLowerCase().indexOf(q) >= 0 ||
612
+ (s.scope || '').toLowerCase().indexOf(q) >= 0;
613
+ });
614
+ }
615
+ var sortVal = sortSel.value;
616
+ filtered.sort(function(a, b) {
617
+ if (sortVal === 'score') return (b.gradeScore || 0) - (a.gradeScore || 0);
618
+ if (sortVal === 'oldest') return new Date(a.startedAt || 0) - new Date(b.startedAt || 0);
619
+ return new Date(b.startedAt || 0) - new Date(a.startedAt || 0);
620
+ });
621
+
622
+ if (!filtered.length) {
623
+ tableWrap.appendChild(emptyState('No matching sessions', 'Try adjusting your filters.'));
624
+ return;
625
+ }
626
+
627
+ var tbl = document.createElement('table'); tbl.className = 'data-table';
628
+ var thead = document.createElement('thead');
629
+ var hrow = document.createElement('tr');
630
+ ['Status', 'Session Name', 'Session ID', 'Scope', 'Started', 'Duration', 'Grade Score', 'Grade Letter', 'Audit Entries', 'MCP Calls', 'CLI Calls'].forEach(function(h) {
631
+ hrow.appendChild(mk('th', null, h));
632
+ });
633
+ thead.appendChild(hrow); tbl.appendChild(thead);
634
+ var tbody = document.createElement('tbody');
635
+
636
+ filtered.forEach(function(s) {
637
+ var row = document.createElement('tr'); row.className = 'session-row';
638
+ var isActive = (s.status || '').toLowerCase() === 'active' || (s.status || '').toLowerCase() === 'running';
639
+ var letter = s.gradeLetter || (s.gradeScore != null ? scoreToLetter(s.gradeScore) : '\u2014');
640
+
641
+ // Status pill
642
+ var statusTd = document.createElement('td');
643
+ if (isActive) {
644
+ var dot = mk('span', 'pulse-dot');
645
+ statusTd.appendChild(dot);
646
+ }
647
+ statusTd.appendChild(mk('span', 'pill ' + (isActive ? 'active' : 'ended'), isActive ? 'Active' : 'Ended'));
648
+ row.appendChild(statusTd);
649
+
650
+ // Name with grade mode badge
651
+ var nameTd = document.createElement('td');
652
+ if (s.gradeMode) {
653
+ var gmBadge = mk('span', 'gw-pill query', '\u{1F4CA}');
654
+ gmBadge.style.cssText = 'font-size:.6rem;padding:.1em .3em;margin-right:.35rem;';
655
+ gmBadge.title = 'Graded session';
656
+ nameTd.appendChild(gmBadge);
657
+ }
658
+ var nameSpan = document.createElement('span');
659
+ nameSpan.textContent = s.name || '\u2014';
660
+ nameTd.appendChild(nameSpan);
661
+ row.appendChild(nameTd);
662
+ // Session ID chip
663
+ var idTd = document.createElement('td');
664
+ idTd.appendChild(mk('code', 'session-chip', (s.sessionId || '').slice(0, 18)));
665
+ row.appendChild(idTd);
666
+ // Scope
667
+ row.appendChild(mk('td', null, s.scope || '\u2014'));
668
+ // Started
669
+ row.appendChild(mk('td', null, fmtDate(s.startedAt)));
670
+ // Duration
671
+ row.appendChild(mk('td', null, s.durationMs ? fmtDuration(s.durationMs) : '\u2014'));
672
+ // Grade Score
673
+ var scoreTd = mk('td', null, s.gradeScore != null ? String(s.gradeScore) : '\u2014');
674
+ if (s.gradeScore != null) scoreTd.style.color = gradeColor(scoreToLetter(s.gradeScore));
675
+ row.appendChild(scoreTd);
676
+ // Grade Letter
677
+ var letterTd = mk('td');
678
+ if (letter !== '\u2014') {
679
+ letterTd.style.fontWeight = '700';
680
+ letterTd.style.color = gradeColor(letter);
681
+ }
682
+ letterTd.textContent = letter;
683
+ row.appendChild(letterTd);
684
+ // Audit entries
685
+ row.appendChild(mk('td', null, fmtNum(s.auditEntries, '0')));
686
+ // MCP Calls
687
+ row.appendChild(mk('td', null, fmtNum(s.mcpCalls, '0')));
688
+ // CLI Calls
689
+ row.appendChild(mk('td', null, fmtNum(s.cliCalls, '0')));
690
+
691
+ tbody.appendChild(row);
692
+
693
+ // Expandable detail row
694
+ var expandRow = document.createElement('tr'); expandRow.className = 'session-expand';
695
+ var expandTd = document.createElement('td'); expandTd.colSpan = 11;
696
+
697
+ row.addEventListener('click', function() {
698
+ var open = expandRow.classList.toggle('open');
699
+ if (open && !expandTd.hasChildNodes()) {
700
+ renderSessionExpand(s, expandTd);
701
+ }
702
+ });
703
+ expandRow.appendChild(expandTd);
704
+ tbody.appendChild(expandRow);
705
+ });
706
+ tbl.appendChild(tbody);
707
+ tableWrap.appendChild(tbl);
708
+ }
709
+
710
+ statusSel.addEventListener('change', renderTable);
711
+ sortSel.addEventListener('change', renderTable);
712
+ searchInput.addEventListener('input', renderTable);
713
+ renderTable();
714
+ }
715
+
716
+ function renderSessionExpand(s, td) {
717
+ var wrap = mk('div', 'session-detail');
718
+ td.appendChild(wrap);
719
+
720
+ // ── Header: session metadata ──────────────────────────────────────────────
721
+ var hdr = mk('div', 'session-detail-header');
722
+ function stat(label, val) {
723
+ var el = mk('div', 'session-detail-stat');
724
+ var lbl = document.createElement('span'); lbl.textContent = label + ': ';
725
+ var v = document.createElement('strong'); v.textContent = val || '\u2014';
726
+ el.appendChild(lbl); el.appendChild(v); return el;
727
+ }
728
+ app(hdr,
729
+ stat('ID', s.sessionId || s.id),
730
+ stat('Scope', s.scope || ((s.scopeType || '') + (s.scopeRootTaskId ? ':' + s.scopeRootTaskId : ''))),
731
+ stat('Started', fmtDate(s.startedAt)),
732
+ stat('Duration', s.durationMs ? fmtDuration(s.durationMs) : (s.endedAt ? fmtDuration(new Date(s.endedAt) - new Date(s.startedAt)) : 'Active')),
733
+ stat('Resumes', s.resumeCount != null ? String(s.resumeCount) : '0'),
734
+ stat('Tasks Done', s.tasksCompleted != null ? String(s.tasksCompleted) : '\u2014')
735
+ );
736
+
737
+ // Grade badge inline
738
+ if (s.gradeScore != null) {
739
+ var letter = s.gradeLetter || scoreToLetter(s.gradeScore);
740
+ var gbadge = mk('div', 'letter-badge sm ' + letter, letter);
741
+ gbadge.style.cssText = 'width:24px;height:24px;font-size:.75rem;display:inline-flex;align-items:center;justify-content:center;border-radius:4px;font-weight:700;background:' + gradeColorBg(letter) + ';color:' + gradeColor(letter) + ';flex-shrink:0;';
742
+ var ginfo = mks('span', 'font-size:.75rem;color:var(--text-muted);margin-left:.4rem;', s.gradeScore + '/100 \u2014 Grade ' + letter);
743
+ var gline = mk('div', 'session-detail-stat');
744
+ gline.style.display = 'flex'; gline.style.alignItems = 'center';
745
+ app(gline, gbadge, ginfo);
746
+ hdr.appendChild(gline);
747
+ }
748
+ wrap.appendChild(hdr);
749
+
750
+ // ── Token summary (if available) ─────────────────────────────────────────
751
+ if (s.totalTokens || s.tokenRecords) {
752
+ var tsumm = mk('div', 'token-summary');
753
+ function tstat(label, val) {
754
+ var el = mk('div', 'token-stat');
755
+ var lbl = document.createElement('span'); lbl.textContent = label + ': ';
756
+ var v = document.createElement('strong'); v.textContent = val != null ? String(val) : '\u2014';
757
+ el.appendChild(lbl); el.appendChild(v); return el;
758
+ }
759
+ app(tsumm,
760
+ tstat('Total tokens', s.totalTokens ? fmtNum(s.totalTokens) : '\u2014'),
761
+ tstat('Token records', s.tokenRecords || 0)
762
+ );
763
+ wrap.appendChild(tsumm);
764
+ }
765
+
766
+ // ── Grade dimension mini-chart (from session-grade join) ────────────────
767
+ var gradeDetails = s.gradeDetails || {};
768
+ var gradeFlags = s.gradeFlags || [];
769
+ if (gradeDetails && typeof gradeDetails === 'object' && Object.keys(gradeDetails).length) {
770
+ var dims = ['sessionDiscipline','discoveryEfficiency','taskHygiene','errorProtocol','disclosureUse'];
771
+ var dimNames = ['S1 Discipline','S2 Efficiency','S3 Hygiene','S4 Protocol','S5 MCP Gateway'];
772
+ var dimColors = ['#a371f7','#58a6ff','#3fb950','#e3b341','#f78166'];
773
+ var dimCard = mk('div', 'token-summary');
774
+ dimCard.style.flexDirection = 'column';
775
+ dims.forEach(function(k, i) {
776
+ var dim = gradeDetails[k] || {};
777
+ var score = dim.score != null ? dim.score : (typeof dim === 'number' ? dim : null);
778
+ var maxS = dim.max != null ? dim.max : 20;
779
+ if (score === null) return;
780
+ var row = mk('div', '');
781
+ row.style.cssText = 'display:flex;align-items:center;gap:.5rem;margin:.2rem 0;';
782
+ var label = mk('span', '');
783
+ label.style.cssText = 'font-size:.68rem;color:var(--text-muted);width:100px;flex-shrink:0;';
784
+ label.textContent = dimNames[i];
785
+ var track = mk('div', '');
786
+ track.style.cssText = 'flex:1;height:6px;background:var(--surface2);border-radius:3px;overflow:hidden;';
787
+ var fill = mk('div', '');
788
+ var pct = maxS > 0 ? Math.min(100, (score / maxS) * 100) : 0;
789
+ fill.style.cssText = 'height:100%;border-radius:3px;background:' + dimColors[i] + ';width:' + pct + '%;';
790
+ track.appendChild(fill);
791
+ var val = mk('span', '');
792
+ val.style.cssText = 'font-family:var(--mono);font-size:.65rem;color:var(--text-muted);width:40px;flex-shrink:0;text-align:right;';
793
+ val.textContent = score + '/' + maxS;
794
+ app(row, label, track, val);
795
+ dimCard.appendChild(row);
796
+ });
797
+ if (gradeFlags.length) {
798
+ var flagRow = mk('div', '');
799
+ flagRow.style.cssText = 'margin-top:.4rem;display:flex;gap:.3rem;flex-wrap:wrap;';
800
+ gradeFlags.forEach(function(f) {
801
+ flagRow.appendChild(mk('span', 'pill', f.replace(/_/g,' ')));
802
+ });
803
+ dimCard.appendChild(flagRow);
804
+ }
805
+ wrap.appendChild(dimCard);
806
+ }
807
+
808
+ // ── Audit log section (fetched from /sessions-data) ───────────────────────
809
+ var auditTitle = mk('div', 'audit-section-title');
810
+ var countChip = mk('span', 'audit-count-chip', s.auditEntries ? fmtNum(s.auditEntries) + ' entries' : '…');
811
+ auditTitle.textContent = 'Audit Log';
812
+ auditTitle.appendChild(countChip);
813
+ wrap.appendChild(auditTitle);
814
+
815
+ var tableWrap = mk('div', 'audit-table-wrap');
816
+ wrap.appendChild(tableWrap);
817
+ var loadingMsg = mk('div', 'audit-loading', 'Loading audit entries…');
818
+ tableWrap.appendChild(loadingMsg);
819
+
820
+ // Fetch full audit data from the server
821
+ var sid = s.sessionId || s.id || '';
822
+ if (!sid) {
823
+ loadingMsg.textContent = 'No session ID available.';
824
+ return;
825
+ }
826
+
827
+ fetch('/sessions-data?sessionId=' + encodeURIComponent(sid))
828
+ .then(function(r) { return r.json(); })
829
+ .then(function(data) {
830
+ tableWrap.textContent = '';
831
+ var entries = (data.entries || []);
832
+ var tokens = data.tokens || {};
833
+
834
+ // Update token summary if richer data available
835
+ if (tokens.total_tokens && !s.totalTokens) {
836
+ var tsumm2 = mk('div', 'token-summary');
837
+ function tstat2(label, val) {
838
+ var el = mk('div', 'token-stat');
839
+ var lbl = document.createElement('span'); lbl.textContent = label + ': ';
840
+ var v = document.createElement('strong'); v.textContent = val != null ? String(val) : '\u2014';
841
+ el.appendChild(lbl); el.appendChild(v); return el;
842
+ }
843
+ app(tsumm2,
844
+ tstat2('Total tokens', fmtNum(tokens.total_tokens)),
845
+ tstat2('Input', fmtNum(tokens.input_tokens || 0)),
846
+ tstat2('Output', fmtNum(tokens.output_tokens || 0)),
847
+ tstat2('Records', tokens.records || 0)
848
+ );
849
+ wrap.insertBefore(tsumm2, auditTitle);
850
+ }
851
+
852
+ // ── Rich session detail sections ────────────────────────────────────
853
+ var sess = data.session || {};
854
+
855
+ // 1. Stats card from session.stats
856
+ var sstats = sess.stats;
857
+ if (sstats) {
858
+ var statsCard = mk('div', 'token-summary');
859
+ function sstat(label, val) {
860
+ var el = mk('div', 'token-stat');
861
+ var lbl = document.createElement('span'); lbl.textContent = label + ': ';
862
+ var v = document.createElement('strong'); v.textContent = val != null ? String(val) : '\u2014';
863
+ el.appendChild(lbl); el.appendChild(v); return el;
864
+ }
865
+ app(statsCard,
866
+ sstat('Tasks Done', sstats.tasksCompleted),
867
+ sstat('Tasks Created', sstats.tasksCreated),
868
+ sstat('Focus Changes', sstats.focusChanges),
869
+ sstat('Active', sstats.totalActiveMinutes != null ? sstats.totalActiveMinutes + ' min' : '\u2014'),
870
+ sstat('Suspends', sstats.suspendCount),
871
+ sstat('Resumes', sess.resumeCount != null ? sess.resumeCount : 0)
872
+ );
873
+ wrap.insertBefore(statsCard, auditTitle);
874
+ }
875
+
876
+ // 2. Session chain (prev / current / next)
877
+ if (sess.previousSessionId || sess.nextSessionId) {
878
+ var chainDiv = mk('div', 'session-chain');
879
+ var chainLabel = mk('span', 'session-chain-label', 'Session Chain');
880
+ chainDiv.appendChild(chainLabel);
881
+
882
+ if (sess.previousSessionId) {
883
+ var prevChip = mk('code', 'session-chip', '\u2190 ' + sess.previousSessionId);
884
+ chainDiv.appendChild(prevChip);
885
+ } else {
886
+ var prevDim = mk('span', 'chain-sep', '\u2014');
887
+ chainDiv.appendChild(prevDim);
888
+ }
889
+
890
+ var sep1 = mk('span', 'chain-sep', '\u2022');
891
+ chainDiv.appendChild(sep1);
892
+
893
+ var curChip = mk('code', 'session-chip chain-current', sess.id || sid);
894
+ chainDiv.appendChild(curChip);
895
+
896
+ var sep2 = mk('span', 'chain-sep', '\u2022');
897
+ chainDiv.appendChild(sep2);
898
+
899
+ if (sess.nextSessionId) {
900
+ var nextChip = mk('code', 'session-chip', sess.nextSessionId + ' \u2192');
901
+ chainDiv.appendChild(nextChip);
902
+ } else {
903
+ var nextDim = mk('span', 'chain-sep', '\u2014');
904
+ chainDiv.appendChild(nextDim);
905
+ }
906
+
907
+ wrap.insertBefore(chainDiv, auditTitle);
908
+ }
909
+
910
+ // 3. Scope section
911
+ var scope = sess.scope;
912
+ if (scope && scope.computedTaskIds && scope.computedTaskIds.length) {
913
+ var scopeDiv = mk('div', 'session-scope');
914
+ var scopeLabel = document.createElement('span');
915
+ scopeLabel.textContent = 'Scope: ';
916
+ var scopeStrong = document.createElement('strong');
917
+ scopeStrong.textContent = (scope.type || 'epic') + ':' + (scope.rootTaskId || '\u2014');
918
+ var scopeCount = document.createElement('span');
919
+ scopeCount.textContent = ' \u2022 ' + scope.computedTaskIds.length + ' tasks in scope';
920
+ scopeDiv.appendChild(scopeLabel);
921
+ scopeDiv.appendChild(scopeStrong);
922
+ scopeDiv.appendChild(scopeCount);
923
+ wrap.insertBefore(scopeDiv, auditTitle);
924
+ }
925
+
926
+ // 4. Debrief section
927
+ var debrief = sess.debrief;
928
+ if (debrief) {
929
+ var ho = debrief.handoff || debrief;
930
+ var debriefCard = mk('div', 'debrief-card');
931
+
932
+ // Note
933
+ if (ho.note) {
934
+ var noteDiv = mk('div', 'debrief-note', ho.note);
935
+ debriefCard.appendChild(noteDiv);
936
+ }
937
+
938
+ // Git state
939
+ var git = ho.gitState || debrief.gitState;
940
+ if (git) {
941
+ var gitRow = mk('div', 'debrief-row');
942
+ var gitLabel = mk('span', 'debrief-label', 'Git');
943
+ gitRow.appendChild(gitLabel);
944
+ if (git.branch) {
945
+ var brSpan = mk('span', 'git-branch', git.branch);
946
+ gitRow.appendChild(brSpan);
947
+ }
948
+ if (git.commit) {
949
+ var hashSpan = mk('span', 'git-hash', String(git.commit).slice(0, 7));
950
+ gitRow.appendChild(hashSpan);
951
+ }
952
+ if (git.uncommittedChanges || git.dirty) {
953
+ var ucBadge = mk('span', 'uncommitted-badge', 'uncommitted changes');
954
+ gitRow.appendChild(ucBadge);
955
+ } else {
956
+ var cleanBadge = mk('span', 'clean-badge', 'clean');
957
+ gitRow.appendChild(cleanBadge);
958
+ }
959
+ debriefCard.appendChild(gitRow);
960
+ }
961
+
962
+ // Chain position
963
+ var chainPos = ho.chainPosition || debrief.chainPosition;
964
+ var chainLen = ho.chainLength || debrief.chainLength;
965
+ if (chainPos != null && chainLen != null) {
966
+ var posRow = mk('div', 'debrief-row');
967
+ var posLabel = mk('span', 'debrief-label', 'Chain');
968
+ posRow.appendChild(posLabel);
969
+ var posVal = document.createElement('span');
970
+ posVal.textContent = 'Position ' + chainPos + ' of ' + chainLen;
971
+ posRow.appendChild(posVal);
972
+ debriefCard.appendChild(posRow);
973
+ }
974
+
975
+ // Next suggested
976
+ var nextSugg = ho.nextSuggested || debrief.nextSuggested;
977
+ if (nextSugg && nextSugg.length) {
978
+ var nsRow = mk('div', 'debrief-row');
979
+ var nsLabel = mk('span', 'debrief-label', 'Next');
980
+ nsRow.appendChild(nsLabel);
981
+ nextSugg.forEach(function(tid) {
982
+ var chip = mk('code', 'session-chip', tid);
983
+ nsRow.appendChild(chip);
984
+ });
985
+ debriefCard.appendChild(nsRow);
986
+ }
987
+
988
+ // Open blockers
989
+ var blockers = ho.openBlockers || debrief.openBlockers;
990
+ if (blockers && blockers.length) {
991
+ var blRow = mk('div', 'debrief-row');
992
+ var blLabel = mk('span', 'debrief-label', 'Blockers');
993
+ blRow.appendChild(blLabel);
994
+ var blCount = document.createElement('span');
995
+ blCount.textContent = blockers.length;
996
+ blCount.style.color = 'var(--grade-d)';
997
+ blCount.style.fontWeight = '600';
998
+ blRow.appendChild(blCount);
999
+ debriefCard.appendChild(blRow);
1000
+ }
1001
+
1002
+ wrap.insertBefore(debriefCard, auditTitle);
1003
+ }
1004
+
1005
+ // 5. Session notes
1006
+ var notes = sess.notes;
1007
+ if (notes && notes.length) {
1008
+ var notesWrap = mk('div', '');
1009
+ var toggle = mk('span', 'notes-toggle', 'Session Notes (' + notes.length + ') \u25B6');
1010
+ var notesList = mk('div', 'notes-list');
1011
+
1012
+ var displayNotes = notes.slice(-5);
1013
+ displayNotes.forEach(function(n) {
1014
+ var entry = mk('div', 'note-entry');
1015
+ var ts = mk('span', 'note-ts', '');
1016
+ var noteText = typeof n === 'string' ? n : (n.text || n.note || '');
1017
+ var noteTs = typeof n === 'object' && n.timestamp ? fmtTime(n.timestamp) : '';
1018
+ ts.textContent = noteTs ? '[' + noteTs + ']' : '';
1019
+ var txt = mk('span', 'note-text', noteText);
1020
+ entry.appendChild(ts);
1021
+ entry.appendChild(txt);
1022
+ notesList.appendChild(entry);
1023
+ });
1024
+
1025
+ toggle.addEventListener('click', function() {
1026
+ var open = notesList.classList.toggle('open');
1027
+ toggle.textContent = 'Session Notes (' + notes.length + ') ' + (open ? '\u25BC' : '\u25B6');
1028
+ });
1029
+
1030
+ notesWrap.appendChild(toggle);
1031
+ notesWrap.appendChild(notesList);
1032
+ wrap.insertBefore(notesWrap, auditTitle);
1033
+ }
1034
+
1035
+ // Update count chip
1036
+ countChip.textContent = fmtNum(entries.length) + ' entries';
1037
+
1038
+ if (!entries.length) {
1039
+ tableWrap.appendChild(mks('div', 'color:var(--text-muted);font-size:.75rem;padding:.5rem 0;', 'No audit entries found for this session.'));
1040
+ return;
1041
+ }
1042
+
1043
+ // Build full audit table
1044
+ var tbl = document.createElement('table'); tbl.className = 'audit-table';
1045
+ var thead = document.createElement('thead');
1046
+ var hrow = document.createElement('tr');
1047
+ ['Time', 'Domain', 'Operation', 'Source', 'Gateway', 'Duration ms', 'Success'].forEach(function(h) {
1048
+ var th = document.createElement('th'); th.textContent = h; hrow.appendChild(th);
1049
+ });
1050
+ thead.appendChild(hrow); tbl.appendChild(thead);
1051
+
1052
+ var tbody = document.createElement('tbody');
1053
+ entries.forEach(function(e) {
1054
+ var tr = document.createElement('tr');
1055
+
1056
+ // Time
1057
+ var timeTd = document.createElement('td');
1058
+ timeTd.className = 'audit-time';
1059
+ timeTd.textContent = fmtTime(e.timestamp);
1060
+ tr.appendChild(timeTd);
1061
+
1062
+ // Domain
1063
+ var domTd = document.createElement('td');
1064
+ domTd.className = 'audit-domain';
1065
+ domTd.textContent = e.domain || '\u2014';
1066
+ tr.appendChild(domTd);
1067
+
1068
+ // Operation
1069
+ var opTd = document.createElement('td');
1070
+ opTd.className = 'audit-op';
1071
+ opTd.textContent = e.operation || '\u2014';
1072
+ tr.appendChild(opTd);
1073
+
1074
+ // Source
1075
+ var srcTd = document.createElement('td');
1076
+ srcTd.className = 'audit-source';
1077
+ srcTd.textContent = e.source || '\u2014';
1078
+ tr.appendChild(srcTd);
1079
+
1080
+ // Gateway pill
1081
+ var gwTd = document.createElement('td');
1082
+ var gw = (e.gateway || '').toUpperCase();
1083
+ var pill = mk('span', 'gw-pill ' + (e.gateway || '').toLowerCase(), gw || '\u2014');
1084
+ gwTd.appendChild(pill);
1085
+ tr.appendChild(gwTd);
1086
+
1087
+ // Duration ms
1088
+ var ms = e.duration_ms != null ? e.duration_ms : null;
1089
+ var msTd = document.createElement('td');
1090
+ msTd.className = 'audit-ms' + (ms > 2000 ? ' vslow' : ms > 500 ? ' slow' : '');
1091
+ msTd.textContent = ms != null ? ms : '\u2014';
1092
+ tr.appendChild(msTd);
1093
+
1094
+ // Success
1095
+ var sucTd = document.createElement('td');
1096
+ sucTd.className = 'audit-success';
1097
+ var sucSpan = document.createElement('span');
1098
+ sucSpan.textContent = e.success ? '\u2713' : '\u2717';
1099
+ sucSpan.style.color = e.success ? 'var(--grade-a)' : 'var(--grade-f)';
1100
+ sucSpan.style.fontWeight = '700';
1101
+ sucTd.appendChild(sucSpan);
1102
+ tr.appendChild(sucTd);
1103
+
1104
+ tbody.appendChild(tr);
1105
+ });
1106
+ tbl.appendChild(tbody);
1107
+ tableWrap.appendChild(tbl);
1108
+ })
1109
+ .catch(function(err) {
1110
+ tableWrap.textContent = '';
1111
+ tableWrap.appendChild(mks('div', 'color:var(--grade-f);font-size:.75rem;padding:.5rem 0;', 'Failed to load audit entries: ' + err.message));
1112
+ });
1113
+ }
1114
+
1115
+ function fmtTime(ts) {
1116
+ if (!ts) return '\u2014';
1117
+ try {
1118
+ var d = new Date(ts);
1119
+ return d.toLocaleTimeString([], {hour: '2-digit', minute: '2-digit', second: '2-digit'});
1120
+ } catch(e) { return String(ts).slice(11, 19) || ts; }
1121
+ }
1122
+
1123
+ function gradeColorBg(l) {
1124
+ var m = {'A':'var(--grade-a-bg)','B':'var(--grade-b-bg)','C':'var(--grade-c-bg)','D':'var(--grade-d-bg)','F':'var(--grade-f-bg)'};
1125
+ return m[l] || 'var(--surface2)';
1126
+ }
1127
+
1128
+ // ── Tab 3: A/B Comparison ─────────────────────────────────────────────────────
1129
+ function renderABTab(abResults, abHistory) {
1130
+ var container = document.getElementById('ab-content');
1131
+ container.textContent = '';
1132
+ var history = abHistory || [];
1133
+ if (!history.length && (!abResults || !abResults.total_runs)) {
1134
+ container.appendChild(emptyState('No A/B results yet', 'Run a blind comparison to generate data.', 'python scripts/run_ab_test.py --domain tasks --runs 3'));
1135
+ return;
1136
+ }
1137
+
1138
+ // Run selector
1139
+ if (history.length) {
1140
+ var selWrap = mk('div', 'filter-bar');
1141
+ selWrap.appendChild(mk('label', null, 'Run:'));
1142
+ var runSel = document.createElement('select');
1143
+ history.sort(function(a, b) { return new Date(b.createdAt || 0) - new Date(a.createdAt || 0); });
1144
+ history.forEach(function(run, i) {
1145
+ var o = document.createElement('option'); o.value = i;
1146
+ o.textContent = (run.name || 'Run ' + (i + 1)) + ' \u2014 ' + fmtDate(run.createdAt);
1147
+ runSel.appendChild(o);
1148
+ });
1149
+ app(selWrap, runSel);
1150
+ container.appendChild(selWrap);
1151
+ }
1152
+
1153
+ renderABContent(abResults, container);
1154
+ document.getElementById('tb-ab').textContent = (abResults && abResults.total_runs) || history.length || 0;
1155
+ }
1156
+
1157
+ function renderABContent(ab, container) {
1158
+ if (!ab || !ab.total_runs) return;
1159
+ var wins = ab.global_wins || {}, wr = ab.global_win_rate || {};
1160
+ var delta = ab.avg_token_delta_mcp_minus_cli;
1161
+ var winner = ab.overall_winner || 'tie';
1162
+
1163
+ // Winner banner
1164
+ var banner = mk('div', 'winner-banner ' + winner);
1165
+ var wlabel = winner === 'mcp' ? 'MCP wins overall' : winner === 'cli' ? 'CLI wins overall' : 'Overall tie';
1166
+ banner.appendChild(mk('span', null, wlabel));
1167
+ if (delta != null) {
1168
+ var sign = delta > 0 ? '+' : '';
1169
+ banner.appendChild(mks('span', 'font-size:.7rem;font-weight:400;opacity:.8;margin-left:.5rem;',
1170
+ 'MCP uses ' + sign + delta.toFixed(1) + ' tokens/op vs CLI'));
1171
+ }
1172
+ container.appendChild(banner);
1173
+
1174
+ // Summary grid
1175
+ container.appendChild(mk('div', 'section-title', 'Global Stats'));
1176
+ var grid = mk('div', 'stat-grid');
1177
+ [
1178
+ ['Total Runs', ab.total_runs, ''],
1179
+ ['Arm A Wins', wins.mcp || 0, 'win'],
1180
+ ['Arm B Wins', wins.cli || 0, winner === 'cli' ? 'win' : ''],
1181
+ ['Ties', wins.tie || 0, 'neutral'],
1182
+ ].forEach(function(row) {
1183
+ var c = mk('div', 'stat-card');
1184
+ app(c, mk('div', 'stat-label', row[0]), mk('div', 'stat-value ' + row[2], String(row[1])));
1185
+ grid.appendChild(c);
1186
+ });
1187
+ container.appendChild(grid);
1188
+
1189
+ // Per-operation table
1190
+ var rawPerOp = ab.per_operation;
1191
+ var perOp = null;
1192
+ if (Array.isArray(rawPerOp)) {
1193
+ perOp = {};
1194
+ rawPerOp.forEach(function(s) { if (s && s.operation) perOp[s.operation] = s; });
1195
+ } else if (rawPerOp && typeof rawPerOp === 'object') {
1196
+ perOp = rawPerOp;
1197
+ }
1198
+ if (perOp && Object.keys(perOp).length) {
1199
+ container.appendChild(mk('div', 'section-title', 'Per-Operation Results'));
1200
+ var wrap = mk('div', 'table-wrap');
1201
+ var tbl = document.createElement('table'); tbl.className = 'data-table';
1202
+ var thead = document.createElement('thead'), hrow = document.createElement('tr');
1203
+ ['Operation', 'Winner', 'MCP Calls', 'CLI Calls', 'Token \u0394', 'MCP ms', 'CLI ms', 'MCP Chars', 'CLI Chars'].forEach(function(h) {
1204
+ hrow.appendChild(mk('th', null, h));
1205
+ });
1206
+ thead.appendChild(hrow); tbl.appendChild(thead);
1207
+ var tbody = document.createElement('tbody');
1208
+ Object.keys(perOp).sort().forEach(function(opKey) {
1209
+ var d = perOp[opKey], row = document.createElement('tr');
1210
+ var dt = d.avg_token_delta != null ? d.avg_token_delta : 0;
1211
+ var dcls = dt > 10 ? 'delta-pos' : dt < -10 ? 'delta-neg' : 'delta-zero';
1212
+ var opW = d.winner || (d.mcp_wins > d.cli_wins ? 'mcp' : d.cli_wins > d.mcp_wins ? 'cli' : 'tie');
1213
+ var cells = [
1214
+ mk('span', 'op-name', opKey),
1215
+ mk('span', 'pill ' + opW, opW.toUpperCase()),
1216
+ document.createTextNode(String(d.mcp_wins || d.mcp_calls || 0)),
1217
+ document.createTextNode(String(d.cli_wins || d.cli_calls || 0)),
1218
+ mk('span', dcls, (dt > 0 ? '+' : '') + dt.toFixed(0) + 't'),
1219
+ document.createTextNode(fmtNum(d.avg_mcp_ms) + 'ms'),
1220
+ document.createTextNode(fmtNum(d.avg_cli_ms) + 'ms'),
1221
+ document.createTextNode(fmtNum(d.avg_mcp_chars)),
1222
+ document.createTextNode(fmtNum(d.avg_cli_chars)),
1223
+ ];
1224
+ cells.forEach(function(c) { var td = document.createElement('td'); td.appendChild(c); row.appendChild(td); });
1225
+ tbody.appendChild(row);
1226
+ });
1227
+ tbl.appendChild(tbody); wrap.appendChild(tbl); container.appendChild(wrap);
1228
+ }
1229
+ }
1230
+
1231
+ // ── Tab 4: Token Economy ──────────────────────────────────────────────────────
1232
+ function renderTokensTab(ta) {
1233
+ var container = document.getElementById('tokens-content');
1234
+ container.textContent = '';
1235
+ if (!ta || (!ta.by_transport && !ta.by_domain)) {
1236
+ container.appendChild(emptyState('No token data yet', 'Token economy data will appear after graded sessions with OTEL enabled.', 'source .cleo/setup-otel.sh'));
1237
+ return;
1238
+ }
1239
+
1240
+ var apiSurface = DATA.api_surface || {};
1241
+ if (apiSurface.canonical || apiSurface.webApi) {
1242
+ var apiCard = mk('div', 'api-card');
1243
+ apiCard.appendChild(mk('div', 'chart-title', 'API Guidance'));
1244
+ var apiGrid = mk('div', 'api-grid');
1245
+
1246
+ function apiBox(title, items) {
1247
+ var box = mk('div', 'api-box');
1248
+ box.appendChild(mk('div', 'api-box-title', title));
1249
+ var list = mk('div', 'api-list');
1250
+ (items || []).forEach(function(item) {
1251
+ var row = document.createElement('code');
1252
+ row.textContent = item;
1253
+ list.appendChild(row);
1254
+ });
1255
+ box.appendChild(list);
1256
+ return box;
1257
+ }
1258
+
1259
+ apiGrid.appendChild(apiBox('Canonical Query Ops', ((apiSurface.canonical || {}).query) || []));
1260
+ apiGrid.appendChild(apiBox('Compatibility Aliases', ((apiSurface.compatibility || {}).query) || []));
1261
+ apiGrid.appendChild(apiBox('Planned Grade Run Reads', apiSurface.planned || []));
1262
+ apiGrid.appendChild(apiBox('Web API', [
1263
+ ((apiSurface.webApi || {}).queryEndpoint) || '/api/query',
1264
+ ((apiSurface.webApi || {}).mutateEndpoint) || '/api/mutate'
1265
+ ].concat(((apiSurface.webApi || {}).lafsHeaders) || [])));
1266
+ apiCard.appendChild(apiGrid);
1267
+ if ((apiSurface.webApi || {}).transportAliasNote) {
1268
+ apiCard.appendChild(mk('div', 'note-block', apiSurface.webApi.transportAliasNote));
1269
+ }
1270
+ container.appendChild(apiCard);
1271
+ }
1272
+
1273
+ // Confidence badge
1274
+ if (ta.confidence) {
1275
+ var conf = ta.confidence.toLowerCase();
1276
+ var confCard = mk('div', 'card');
1277
+ confCard.style.cssText = 'display:flex;align-items:center;gap:.75rem;';
1278
+ confCard.appendChild(mk('div', 'section-title', 'Data Confidence'));
1279
+ confCard.appendChild(mk('span', 'confidence-badge ' + conf, conf.toUpperCase()));
1280
+ container.appendChild(confCard);
1281
+ }
1282
+
1283
+ // By Transport
1284
+ if (ta.by_transport && Object.keys(ta.by_transport).length) {
1285
+ var entries = Object.entries(ta.by_transport).map(function(kv) {
1286
+ return [kv[0], kv[1].mean || kv[1].avg || kv[1].total || 0, kv[1].count || kv[1].samples || 0];
1287
+ });
1288
+ var maxT = Math.max.apply(null, entries.map(function(e) { return e[1]; }).concat([1]));
1289
+ var tColors = { mcp: 'var(--mcp)', cli: 'var(--cli)', agent: 'var(--grade-c)', api: 'var(--accent)' };
1290
+ var card = mk('div', 'chart-card');
1291
+ card.appendChild(mk('div', 'chart-title', 'By Transport'));
1292
+ var chart = mk('div', 'bar-chart');
1293
+ entries.forEach(function(row) {
1294
+ var pct = maxT > 0 ? Math.min((row[1] / maxT) * 90, 90) : 0;
1295
+ var r = mk('div', 'bar-row');
1296
+ var track = mk('div', 'bar-track');
1297
+ var fill = mk('div', 'bar-fill');
1298
+ fill.style.cssText = 'width:' + pct + '%;background:' + (tColors[row[0]] || 'var(--accent)') + ';';
1299
+ track.appendChild(fill);
1300
+ app(r, mk('span', 'bar-label', row[0]), track, mk('span', 'bar-val', fmtNum(row[1]) + 't (n=' + row[2] + ')'));
1301
+ chart.appendChild(r);
1302
+ });
1303
+ card.appendChild(chart);
1304
+ if (entries.some(function(row) { return row[0] === 'http'; })) {
1305
+ card.appendChild(mk('div', 'note-block', 'Transport view normalizes legacy token rows labeled api into http, following CLEO-WEB-API v2.1.'));
1306
+ }
1307
+ container.appendChild(card);
1308
+ }
1309
+
1310
+ // By Domain
1311
+ if (ta.by_domain && Object.keys(ta.by_domain).length) {
1312
+ var dEntries = Object.entries(ta.by_domain).map(function(kv) {
1313
+ return [kv[0], kv[1].mean || kv[1].avg || kv[1].total || 0];
1314
+ });
1315
+ dEntries.sort(function(a, b) { return b[1] - a[1]; });
1316
+ var maxD = Math.max.apply(null, dEntries.map(function(e) { return e[1]; }).concat([1]));
1317
+ var domColors = ['#a371f7', '#58a6ff', '#3fb950', '#e3b341', '#f78166', '#f0883e', '#7d8590', '#3fb950', '#58a6ff', '#a371f7'];
1318
+ var dCard = mk('div', 'chart-card');
1319
+ dCard.appendChild(mk('div', 'chart-title', 'By Domain'));
1320
+ var dChart = mk('div', 'bar-chart');
1321
+ dEntries.forEach(function(row, i) {
1322
+ var pct = maxD > 0 ? Math.min((row[1] / maxD) * 90, 90) : 0;
1323
+ var r = mk('div', 'bar-row');
1324
+ var track = mk('div', 'bar-track');
1325
+ var fill = mk('div', 'bar-fill');
1326
+ fill.style.cssText = 'width:' + pct + '%;background:' + domColors[i % domColors.length] + ';';
1327
+ track.appendChild(fill);
1328
+ app(r, mk('span', 'bar-label', row[0]), track, mk('span', 'bar-val', fmtNum(row[1]) + 't'));
1329
+ dChart.appendChild(r);
1330
+ });
1331
+ dCard.appendChild(dChart);
1332
+ container.appendChild(dCard);
1333
+ }
1334
+ }
1335
+
1336
+ // ── Tab 5: Operation Matrix ───────────────────────────────────────────────────
1337
+ function renderMatrixTab(matrix) {
1338
+ var container = document.getElementById('matrix-content');
1339
+ container.textContent = '';
1340
+ if (!matrix || !matrix.length) {
1341
+ container.appendChild(emptyState('No operation matrix data', 'Operation matrix data is generated from the 202-op registry.'));
1342
+ return;
1343
+ }
1344
+ document.getElementById('tb-matrix').textContent = matrix.length;
1345
+
1346
+ // Collect unique domains and gateways
1347
+ var domains = ['all'];
1348
+ var domainSet = {};
1349
+ matrix.forEach(function(op) {
1350
+ if (op.domain && !domainSet[op.domain]) { domainSet[op.domain] = true; domains.push(op.domain); }
1351
+ });
1352
+
1353
+ // Filter row
1354
+ var filterBar = mk('div', 'filter-bar');
1355
+ var domainSel = document.createElement('select');
1356
+ domains.forEach(function(d) {
1357
+ var o = document.createElement('option'); o.value = d; o.textContent = d === 'all' ? 'All Domains' : d; domainSel.appendChild(o);
1358
+ });
1359
+ var gatewaySel = document.createElement('select');
1360
+ [['all', 'All Gateways'], ['query', 'Query'], ['mutate', 'Mutate']].forEach(function(opt) {
1361
+ var o = document.createElement('option'); o.value = opt[0]; o.textContent = opt[1]; gatewaySel.appendChild(o);
1362
+ });
1363
+ var testedSel = document.createElement('select');
1364
+ [['all', 'All'], ['tested', 'Tested'], ['untested', 'Untested']].forEach(function(opt) {
1365
+ var o = document.createElement('option'); o.value = opt[0]; o.textContent = opt[1]; testedSel.appendChild(o);
1366
+ });
1367
+ app(filterBar, mk('label', null, 'Domain:'), domainSel, mk('label', null, 'Gateway:'), gatewaySel, mk('label', null, 'Tested:'), testedSel);
1368
+ container.appendChild(filterBar);
1369
+
1370
+ var tableWrap = mk('div', 'table-wrap');
1371
+ container.appendChild(tableWrap);
1372
+
1373
+ function renderMatrix() {
1374
+ tableWrap.textContent = '';
1375
+ var filtered = matrix.slice();
1376
+ if (domainSel.value !== 'all') filtered = filtered.filter(function(op) { return op.domain === domainSel.value; });
1377
+ if (gatewaySel.value !== 'all') filtered = filtered.filter(function(op) { return op.gateway === gatewaySel.value; });
1378
+ if (testedSel.value === 'tested') filtered = filtered.filter(function(op) { return op.tested; });
1379
+ if (testedSel.value === 'untested') filtered = filtered.filter(function(op) { return !op.tested; });
1380
+
1381
+ if (!filtered.length) {
1382
+ tableWrap.appendChild(emptyState('No operations match filters', 'Adjust the filters above.'));
1383
+ return;
1384
+ }
1385
+
1386
+ var tbl = document.createElement('table'); tbl.className = 'data-table';
1387
+ var thead = document.createElement('thead');
1388
+ var hrow = document.createElement('tr');
1389
+ ['Operation', 'Domain', 'Gateway', 'Tested', 'MCP Calls', 'CLI Calls', 'Avg MCP ms', 'Avg CLI ms'].forEach(function(h) {
1390
+ hrow.appendChild(mk('th', null, h));
1391
+ });
1392
+ thead.appendChild(hrow); tbl.appendChild(thead);
1393
+ var tbody = document.createElement('tbody');
1394
+
1395
+ filtered.forEach(function(op) {
1396
+ var row = document.createElement('tr');
1397
+ // Operation name
1398
+ row.appendChild((function() { var td = document.createElement('td'); td.appendChild(mk('span', 'op-name', op.operation || op.name || '')); return td; })());
1399
+ // Domain
1400
+ row.appendChild(mk('td', null, op.domain || '\u2014'));
1401
+ // Gateway pill
1402
+ var gwTd = document.createElement('td');
1403
+ gwTd.appendChild(mk('span', 'pill ' + (op.gateway || ''), (op.gateway || '').toUpperCase()));
1404
+ row.appendChild(gwTd);
1405
+ // Tested
1406
+ var testedTd = mk('td');
1407
+ testedTd.textContent = op.tested ? '\u2713' : '\u2014';
1408
+ testedTd.style.color = op.tested ? 'var(--win)' : 'var(--text-muted)';
1409
+ row.appendChild(testedTd);
1410
+ // MCP Calls
1411
+ row.appendChild(mk('td', null, fmtNum(op.mcpCalls || op.mcp_calls, '0')));
1412
+ // CLI Calls
1413
+ row.appendChild(mk('td', null, fmtNum(op.cliCalls || op.cli_calls, '0')));
1414
+ // Avg MCP ms
1415
+ row.appendChild(mk('td', null, op.avgMcpMs || op.avg_mcp_ms ? fmtNum(op.avgMcpMs || op.avg_mcp_ms) + 'ms' : '\u2014'));
1416
+ // Avg CLI ms
1417
+ row.appendChild(mk('td', null, op.avgCliMs || op.avg_cli_ms ? fmtNum(op.avgCliMs || op.avg_cli_ms) + 'ms' : '\u2014'));
1418
+ tbody.appendChild(row);
1419
+ });
1420
+ tbl.appendChild(tbody);
1421
+ tableWrap.appendChild(tbl);
1422
+ }
1423
+
1424
+ domainSel.addEventListener('change', renderMatrix);
1425
+ gatewaySel.addEventListener('change', renderMatrix);
1426
+ testedSel.addEventListener('change', renderMatrix);
1427
+ renderMatrix();
1428
+ }
1429
+
1430
+ // ── Tab 6: Eval Report ────────────────────────────────────────────────────────
1431
+ function renderEvalsTab(evalReport) {
1432
+ var container = document.getElementById('evals-content');
1433
+ container.textContent = '';
1434
+ if (!evalReport || (!evalReport.evals && !evalReport.results)) {
1435
+ container.appendChild(emptyState('No eval report data', 'Eval results will appear after running the eval suite.'));
1436
+ return;
1437
+ }
1438
+
1439
+ var evals = evalReport.evals || evalReport.results || [];
1440
+ if (!Array.isArray(evals)) {
1441
+ // Could be object keyed by eval ID
1442
+ evals = Object.entries(evals).map(function(kv) { var e = kv[1]; e.id = e.id || kv[0]; return e; });
1443
+ }
1444
+ document.getElementById('tb-evals').textContent = evals.length;
1445
+
1446
+ // Sort controls
1447
+ var sortBar = mk('div', 'filter-bar');
1448
+ var sortSel = document.createElement('select');
1449
+ [['id', 'By ID'], ['passRate', 'By Pass Rate'], ['name', 'By Name']].forEach(function(opt) {
1450
+ var o = document.createElement('option'); o.value = opt[0]; o.textContent = opt[1]; sortSel.appendChild(o);
1451
+ });
1452
+ app(sortBar, mk('label', null, 'Sort:'), sortSel);
1453
+ container.appendChild(sortBar);
1454
+
1455
+ var cardsWrap = mk('div', null);
1456
+ cardsWrap.style.cssText = 'display:flex;flex-direction:column;gap:1rem;';
1457
+ container.appendChild(cardsWrap);
1458
+
1459
+ function renderEvals() {
1460
+ cardsWrap.textContent = '';
1461
+ var sorted = evals.slice();
1462
+ var sortVal = sortSel.value;
1463
+ sorted.sort(function(a, b) {
1464
+ if (sortVal === 'passRate') {
1465
+ var aRate = getPassRate(a), bRate = getPassRate(b);
1466
+ return bRate - aRate;
1467
+ }
1468
+ if (sortVal === 'name') return (a.name || '').localeCompare(b.name || '');
1469
+ return (a.id || '').localeCompare(b.id || '');
1470
+ });
1471
+
1472
+ sorted.forEach(function(ev) {
1473
+ var card = mk('div', 'eval-card');
1474
+ var header = mk('div', 'eval-card-header');
1475
+
1476
+ // Title + badge
1477
+ var titleWrap = mk('div');
1478
+ app(titleWrap, mk('span', 'eval-card-title', (ev.id ? ev.id + ': ' : '') + (ev.name || ev.description || 'Eval')));
1479
+ header.appendChild(titleWrap);
1480
+
1481
+ // Status badge
1482
+ var status = getEvalStatus(ev);
1483
+ header.appendChild(mk('span', 'pill ' + status.cls, status.label));
1484
+ card.appendChild(header);
1485
+
1486
+ // Pass rate bar
1487
+ var expectations = ev.expectations || ev.results || [];
1488
+ if (expectations.length) {
1489
+ var passed = expectations.filter(function(e) { return e.pass || e.passed || e.result === 'pass'; }).length;
1490
+ var total = expectations.length;
1491
+ var pct = Math.round((passed / total) * 100);
1492
+
1493
+ var rateRow = mk('div', null);
1494
+ rateRow.style.cssText = 'display:flex;align-items:center;gap:.75rem;margin-bottom:.75rem;';
1495
+ rateRow.appendChild(mks('span', 'font-family:var(--mono);font-size:.72rem;', passed + '/' + total + ' (' + pct + '%)'));
1496
+ var prBar = mk('div', 'pass-rate-bar');
1497
+ var prFill = mk('div', 'pass-rate-fill');
1498
+ prFill.style.width = pct + '%';
1499
+ if (pct < 60) prFill.style.background = 'var(--grade-f)';
1500
+ else if (pct < 80) prFill.style.background = 'var(--grade-c)';
1501
+ prBar.appendChild(prFill);
1502
+ rateRow.appendChild(prBar);
1503
+ card.appendChild(rateRow);
1504
+
1505
+ // Expectation list
1506
+ expectations.forEach(function(exp) {
1507
+ var expRow = mk('div', 'expectation-row');
1508
+ var passed2 = exp.pass || exp.passed || exp.result === 'pass';
1509
+ var icon = mk('span', 'expect-icon');
1510
+ icon.textContent = passed2 ? '\u2713' : '\u2717';
1511
+ icon.style.color = passed2 ? 'var(--win)' : 'var(--loss)';
1512
+ expRow.appendChild(icon);
1513
+ var expContent = mk('div');
1514
+ expContent.appendChild(mk('div', null, exp.name || exp.description || exp.expectation || 'Expectation'));
1515
+ if (exp.evidence) {
1516
+ expContent.appendChild(mk('div', 'expect-evidence', exp.evidence));
1517
+ }
1518
+ expRow.appendChild(expContent);
1519
+ card.appendChild(expRow);
1520
+ });
1521
+ }
1522
+ cardsWrap.appendChild(card);
1523
+ });
1524
+ }
1525
+
1526
+ sortSel.addEventListener('change', renderEvals);
1527
+ renderEvals();
1528
+ }
1529
+
1530
+ function getPassRate(ev) {
1531
+ var expectations = ev.expectations || ev.results || [];
1532
+ if (!expectations.length) return -1;
1533
+ var passed = expectations.filter(function(e) { return e.pass || e.passed || e.result === 'pass'; }).length;
1534
+ return passed / expectations.length;
1535
+ }
1536
+
1537
+ function getEvalStatus(ev) {
1538
+ var expectations = ev.expectations || ev.results || [];
1539
+ if (!expectations.length) return { cls: 'not-run', label: 'NOT RUN' };
1540
+ var passed = expectations.filter(function(e) { return e.pass || e.passed || e.result === 'pass'; }).length;
1541
+ if (passed === expectations.length) return { cls: 'pass', label: 'PASS' };
1542
+ return { cls: 'fail', label: 'FAIL' };
1543
+ }
1544
+
1545
+ // ── Init ──────────────────────────────────────────────────────────────────────
1546
+ (function() {
1547
+ var meta = DATA.metadata || {};
1548
+ document.getElementById('hdr-title').textContent = 'ct-grade v2.1 \u2014 Grade Review';
1549
+ if (meta.workspace) document.getElementById('hdr-sub').textContent = meta.workspace;
1550
+ var hdrMeta = document.getElementById('hdr-meta');
1551
+ if (meta.generated_at) {
1552
+ hdrMeta.appendChild(document.createTextNode('Generated ' + fmtDate(meta.generated_at)));
1553
+ hdrMeta.appendChild(document.createElement('br'));
1554
+ }
1555
+ var grades = DATA.grades || [];
1556
+ var sessions = DATA.sessions || [];
1557
+ hdrMeta.appendChild(document.createTextNode(
1558
+ grades.length + ' grade(s) \u00b7 ' + sessions.length + ' session(s)'
1559
+ ));
1560
+ if (meta.skill_version) {
1561
+ hdrMeta.appendChild(document.createElement('br'));
1562
+ hdrMeta.appendChild(document.createTextNode('v' + meta.skill_version));
1563
+ }
1564
+
1565
+ renderGradesTab(grades);
1566
+ renderSessionsTab(sessions);
1567
+ renderABTab(DATA.ab_results || {}, DATA.ab_history || []);
1568
+ renderTokensTab(DATA.token_analysis || {});
1569
+ renderMatrixTab(DATA.operation_matrix || []);
1570
+ renderEvalsTab(DATA.eval_report || {});
1571
+ })();
1572
+ </script>
1573
+ </body>
1574
+ </html>