@cleocode/skills 2026.5.81 → 2026.5.83

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