cyborg-hunter 0.8.0 → 0.9.1
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.
- package/CHANGELOG.md +48 -0
- package/CITATION.cff +2 -2
- package/README.md +6 -4
- package/dist/cyborg-hunter-replay.js +2 -2
- package/dist/cyborg-hunter.esm.js +3 -1
- package/dist/cyborg-hunter.min.js +2 -2
- package/dist/extension-cyborg-hunter.js +1 -1
- package/package.json +1 -1
- package/src/cli/analyzers/score-weights.js +145 -0
- package/src/cli/analyzers/triage.js +55 -39
- package/src/cli/config.js +15 -0
- package/src/cli/preview-entry.js +6 -0
- package/src/cli/renderers/font-face-css.js +16 -0
- package/src/cli/renderers/fonts/FONTS_MANIFEST.json +69 -0
- package/src/cli/renderers/fonts/majormonodisplay/OFL.txt +93 -0
- package/src/cli/renderers/fonts/majormonodisplay/majormonodisplay-400.woff2 +0 -0
- package/src/cli/renderers/fonts/recursive/OFL.txt +93 -0
- package/src/cli/renderers/fonts/recursive/recursive-300-1000.woff2 +0 -0
- package/src/cli/renderers/fonts/sofiasans/OFL.txt +93 -0
- package/src/cli/renderers/fonts/sofiasans/sofiasans-1-1000.woff2 +0 -0
- package/src/cli/renderers/fonts/sora/OFL.txt +93 -0
- package/src/cli/renderers/fonts/sora/sora-100-800.woff2 +0 -0
- package/src/cli/renderers/fonts/spacegrotesk/OFL.txt +93 -0
- package/src/cli/renderers/fonts/spacegrotesk/spacegrotesk-300-700.woff2 +0 -0
- package/src/cli/renderers/fonts/tomorrow/OFL.txt +93 -0
- package/src/cli/renderers/fonts/tomorrow/tomorrow-400.woff2 +0 -0
- package/src/cli/renderers/html-index-core.js +120 -106
- package/src/cli/renderers/html-index.js +7 -2
- package/src/cli/renderers/replay-styles.js +61 -0
- package/src/cli/renderers/replay-viewer.client.js +8 -6
- package/src/cli/renderers/report-fonts.js +24 -0
- package/src/cli/renderers/score-weights.js +16 -0
- package/src/cli/renderers/summary-csv.js +2 -0
- package/src/cli/renderers/trajectories-core.js +2 -1
- package/src/cli/renderers/triage-md.js +9 -2
- package/src/cli/report.js +10 -3
- package/src/jspsych/extension-cyborg-hunter-replay.js +1 -1
- package/src/jspsych/extension-cyborg-hunter.js +1 -1
- package/src/shared/constants.js +1 -1
- package/src/shared/schema.js +1 -0
- package/src/shared/validation.js +1 -1
|
@@ -21,6 +21,8 @@
|
|
|
21
21
|
|
|
22
22
|
import { VERSION, sanitizeId } from '../../shared/constants.js';
|
|
23
23
|
import { decomposeScore } from '../analyzers/triage.js';
|
|
24
|
+
import { resolveScoreWeights, customWeightsText, formatScore } from '../analyzers/score-weights.js';
|
|
25
|
+
import { REPLAY_STYLES_CSS } from './replay-styles.js';
|
|
24
26
|
import { getByPath } from '../../shared/paths.js';
|
|
25
27
|
import { inferTier } from '../../replay/viewer-model.js';
|
|
26
28
|
import { inlineSafeJson, inlineSafeSrc } from '../../shared/inline-safe.js';
|
|
@@ -43,6 +45,10 @@ export async function renderIndexHtml(summaries, triage, participants, config, v
|
|
|
43
45
|
// was hand-rolled in nine places and missing from exactly this one, which is
|
|
44
46
|
// why the rule now lives in one module (T5 Task 10 + its fix round 3).
|
|
45
47
|
const replayClientSrc = inlineSafeSrc(opts.replayClientSrc);
|
|
48
|
+
// @font-face rules for the report's typefaces (report-fonts.js builds them
|
|
49
|
+
// as base64 data URIs for the CLI). Absent → no faces, and every role
|
|
50
|
+
// renders in its fallback stack (the demo bundle and the snapshot tests).
|
|
51
|
+
const fontFaceCss = opts.fontFaceCss ? String(opts.fontFaceCss) + '\n' : '';
|
|
46
52
|
const visualsUnavailableNote = opts.visualsUnavailableNote
|
|
47
53
|
?? 'Visual renderers not available (install the canvas package).';
|
|
48
54
|
const imageSources = opts.imageSources ?? null; // pid → {typingProfile, sessionTimeline, trajectories} data URIs (null entry = omit that img)
|
|
@@ -104,28 +110,41 @@ export async function renderIndexHtml(summaries, triage, participants, config, v
|
|
|
104
110
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
105
111
|
<title>Cyborg Hunter Report</title>
|
|
106
112
|
<style>
|
|
107
|
-
:root {
|
|
108
|
-
--bg: #fafafa; --surface: #fff; --ink: #
|
|
109
|
-
--dim: #
|
|
113
|
+
${fontFaceCss} :root {
|
|
114
|
+
--bg: #fafafa; --surface: #fff; --ink: #0f0f0f;
|
|
115
|
+
--dim: #4f4a40; --line: #b9b2a2;
|
|
110
116
|
--hard: #d32f2f; --soft: #f57c00; --clean: #388e3c;
|
|
117
|
+
/* Typefaces (embedded by report-fonts.js; picked in the 2026-09 report
|
|
118
|
+
palette). Each stack falls back to the system face when a font is
|
|
119
|
+
absent (demo bundle) or lacks a glyph (≥ ≤ ▶ ⓘ). */
|
|
120
|
+
--ff-space: "Space Grotesk", system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
|
|
121
|
+
--ff-tomorrow: "Tomorrow", system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
|
|
122
|
+
--ff-sofia: "Sofia Sans", system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
|
|
123
|
+
--ff-sora: "Sora", system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
|
|
124
|
+
--ff-recursive: "Recursive", system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
|
|
125
|
+
--ff-majormono: "Major Mono Display", ui-monospace, SFMono-Regular, monospace;
|
|
111
126
|
}
|
|
112
127
|
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
113
128
|
html, body { height: 100%; }
|
|
114
129
|
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
115
130
|
color: var(--ink); background: var(--bg); display: grid; grid-template-rows: auto 1fr; height: 100vh; }
|
|
116
131
|
.topbar { display: flex; align-items: center; gap: 16px; padding: 12px 20px;
|
|
117
|
-
background: var(--surface); border-bottom:
|
|
118
|
-
.topbar h1 { font-size:
|
|
119
|
-
.topbar .meta { color: var(--dim); font-size: 13px; flex: 1; }
|
|
132
|
+
background: var(--surface); border-bottom: 3px double var(--ink); }
|
|
133
|
+
.topbar h1 { font-family: var(--ff-space); font-size: 32.4px; font-weight: 550; letter-spacing: 0.01em; }
|
|
134
|
+
.topbar .meta { font-family: var(--ff-tomorrow); color: var(--dim); font-size: 13px; flex: 1; }
|
|
120
135
|
.topbar button { padding: 6px 12px; border: 1px solid var(--line); background: var(--surface);
|
|
121
|
-
color: var(--ink); border-radius: 4px; cursor: pointer;
|
|
136
|
+
color: var(--ink); border-radius: 4px; cursor: pointer;
|
|
137
|
+
font-family: var(--ff-recursive); font-size: 19.5px; font-weight: 400; letter-spacing: 0; }
|
|
122
138
|
.topbar button:hover { background: var(--bg); }
|
|
123
139
|
.layout { display: grid; grid-template-columns: 360px 1fr; height: 100%; overflow: hidden; }
|
|
124
140
|
.rail { background: var(--surface); border-right: 1px solid var(--line); overflow-y: auto; }
|
|
125
141
|
.detail { overflow-y: auto; padding: 20px; }
|
|
126
|
-
.note { color: var(--dim); font-style: italic; font-size: 13px; }
|
|
142
|
+
.note { font-family: var(--ff-tomorrow); color: var(--dim); font-style: italic; font-size: 13px; }
|
|
143
|
+
/* .mono marks data values (ids, scores, counts, timestamps). It was a
|
|
144
|
+
system monospace; it now defaults to Sora with tabular figures, and the
|
|
145
|
+
roles below override it (timestamps, evidence text). */
|
|
127
146
|
.mono {
|
|
128
|
-
font: 13px/1.3
|
|
147
|
+
font: 13px/1.3 var(--ff-sora);
|
|
129
148
|
font-variant-numeric: tabular-nums;
|
|
130
149
|
}
|
|
131
150
|
[hidden] { display: none !important; }
|
|
@@ -141,34 +160,43 @@ export async function renderIndexHtml(summaries, triage, participants, config, v
|
|
|
141
160
|
padding: 10px 12px; border-top: 1px solid var(--line);
|
|
142
161
|
background: var(--surface); font-size: 12px;
|
|
143
162
|
}
|
|
144
|
-
.totals-title { color: var(--dim); text-transform: uppercase; letter-spacing: 0.04em; margin-bottom: 4px; }
|
|
145
|
-
.totals-row { display: flex; align-items: center; gap: 6px; padding: 2px 0; }
|
|
146
|
-
.totals-row .count { margin-left: auto; }
|
|
163
|
+
.totals-title { font-family: var(--ff-sora); color: var(--dim); text-transform: uppercase; letter-spacing: 0.04em; margin-bottom: 4px; }
|
|
164
|
+
.totals-row { font-family: var(--ff-sora); display: flex; align-items: center; gap: 6px; padding: 2px 0; }
|
|
165
|
+
.totals-row .count { font-family: var(--ff-sora); margin-left: auto; }
|
|
147
166
|
|
|
148
167
|
/* Search */
|
|
149
168
|
.search-wrap input {
|
|
150
169
|
width: 100%; padding: 6px 8px; border: 1px solid var(--line);
|
|
151
|
-
border-radius: 4px;
|
|
170
|
+
border-radius: 4px; background: var(--bg);
|
|
152
171
|
color: var(--ink);
|
|
172
|
+
font-family: var(--ff-recursive); font-size: 19.5px; font-weight: 400; letter-spacing: 0;
|
|
153
173
|
}
|
|
154
174
|
.search-wrap input:focus { outline: 1px solid var(--ink); border-color: var(--ink); }
|
|
155
175
|
|
|
156
|
-
/* Filter chips
|
|
157
|
-
|
|
176
|
+
/* Filter chips — one segmented control: joined buttons, square inner
|
|
177
|
+
edges, outer corners 4px. 15px with 4px 8px padding keeps four 3-digit
|
|
178
|
+
counts on one line in the 360px rail (measured 2026-09-30). */
|
|
179
|
+
.filter-chips { display: flex; flex-wrap: wrap; gap: 0; }
|
|
158
180
|
.filter-chip {
|
|
159
|
-
padding:
|
|
160
|
-
color: var(--ink); border-radius:
|
|
181
|
+
padding: 4px 8px; border: 1px solid var(--line); background: var(--surface);
|
|
182
|
+
color: var(--ink); border-radius: 0; margin-left: -1px; cursor: pointer;
|
|
183
|
+
font-family: var(--ff-recursive); font-size: 15px; font-weight: 400; letter-spacing: 0;
|
|
161
184
|
}
|
|
185
|
+
.filter-chip:first-child { border-radius: 4px 0 0 4px; margin-left: 0; }
|
|
186
|
+
.filter-chip:last-child { border-radius: 0 4px 4px 0; }
|
|
162
187
|
.filter-chip:hover { background: var(--bg); }
|
|
188
|
+
/* Raised so the next chip's -1px overlap can't paint over its ink edge. */
|
|
163
189
|
.filter-chip.active {
|
|
164
190
|
background: var(--ink); color: var(--surface); border-color: var(--ink);
|
|
191
|
+
position: relative; z-index: 1;
|
|
165
192
|
}
|
|
166
193
|
|
|
167
194
|
/* Sort selector */
|
|
168
|
-
.sort-wrap { font-size: 12px; color: var(--dim); }
|
|
195
|
+
.sort-wrap { font-family: var(--ff-tomorrow); font-size: 12px; color: var(--dim); }
|
|
169
196
|
.sort-wrap select {
|
|
170
197
|
background: var(--surface); color: var(--ink); border: 1px solid var(--line);
|
|
171
|
-
border-radius: 4px; padding: 2px 4px;
|
|
198
|
+
border-radius: 4px; padding: 2px 4px;
|
|
199
|
+
font-family: var(--ff-recursive); font-size: 18px; font-weight: 400; letter-spacing: 0;
|
|
172
200
|
}
|
|
173
201
|
|
|
174
202
|
/* Cohort rows */
|
|
@@ -177,23 +205,27 @@ export async function renderIndexHtml(summaries, triage, participants, config, v
|
|
|
177
205
|
cursor: pointer;
|
|
178
206
|
}
|
|
179
207
|
.cohort-row:hover { background: var(--bg); }
|
|
208
|
+
/* Selected row: a 2px inset ink outline (no layout shift, row tone kept). */
|
|
180
209
|
.cohort-row.selected {
|
|
181
|
-
border-left:
|
|
182
|
-
padding-left:
|
|
183
|
-
background:
|
|
210
|
+
border-left: 0;
|
|
211
|
+
padding-left: 12px;
|
|
212
|
+
background: transparent;
|
|
213
|
+
box-shadow: inset 0 0 0 2px var(--ink);
|
|
184
214
|
}
|
|
185
215
|
.cohort-row-top { display: flex; align-items: center; gap: 8px; }
|
|
186
216
|
.cohort-row-top .pid {
|
|
217
|
+
font-family: var(--ff-sora);
|
|
187
218
|
flex: 1; min-width: 0; color: var(--ink);
|
|
188
219
|
overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
|
|
189
220
|
}
|
|
190
|
-
.cohort-row-top .score { color: var(--dim); flex-shrink: 0; }
|
|
221
|
+
.cohort-row-top .score { font-family: var(--ff-sora); color: var(--dim); flex-shrink: 0; }
|
|
191
222
|
.cohort-row.selected .cohort-row-top .score { color: var(--ink); font-weight: 600; }
|
|
192
223
|
.cohort-row-bot {
|
|
193
224
|
display: flex; align-items: flex-start; gap: 8px;
|
|
194
225
|
margin-top: 4px;
|
|
195
226
|
}
|
|
196
227
|
.reason-excerpt {
|
|
228
|
+
font-family: var(--ff-recursive);
|
|
197
229
|
flex: 1; color: var(--dim); font-size: 12px;
|
|
198
230
|
display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
|
|
199
231
|
overflow: hidden; text-overflow: ellipsis;
|
|
@@ -208,9 +240,10 @@ export async function renderIndexHtml(summaries, triage, participants, config, v
|
|
|
208
240
|
.tier-dot[data-tier="soft"] { background: var(--soft); }
|
|
209
241
|
.tier-dot[data-tier="clean"] { background: var(--clean); }
|
|
210
242
|
|
|
211
|
-
/* Tier badge — small
|
|
243
|
+
/* Tier badge — small square tag, filled for hard/soft, outline for clean */
|
|
212
244
|
.tier-badge {
|
|
213
|
-
font-
|
|
245
|
+
font-family: var(--ff-sora);
|
|
246
|
+
font-size: 10px; padding: 2px 6px; border-radius: 0;
|
|
214
247
|
font-weight: 600; letter-spacing: 0.04em; flex-shrink: 0;
|
|
215
248
|
}
|
|
216
249
|
.tier-badge[data-tier="hard"] { background: var(--hard); color: white; }
|
|
@@ -244,17 +277,21 @@ export async function renderIndexHtml(summaries, triage, participants, config, v
|
|
|
244
277
|
|
|
245
278
|
/* Header strip */
|
|
246
279
|
.detail-header {
|
|
247
|
-
border-bottom: 1px solid var(--
|
|
280
|
+
border-bottom: 1px solid var(--ink);
|
|
248
281
|
padding-bottom: 12px; margin-bottom: 16px;
|
|
249
282
|
}
|
|
250
283
|
.detail-header-top { display: flex; align-items: center; gap: 12px; }
|
|
251
|
-
.detail-header-top .pid-full { flex: 1; font-size: 13px; color: var(--ink); }
|
|
252
|
-
.detail-header-top .score-big { font-size: 36px; font-weight: 600; color: var(--ink); }
|
|
253
|
-
.detail-header-sub { color: var(--dim); font-size: 12px; margin-top: 4px; }
|
|
254
|
-
|
|
255
|
-
|
|
284
|
+
.detail-header-top .pid-full { font-family: var(--ff-sora); flex: 1; font-size: 13px; color: var(--ink); }
|
|
285
|
+
.detail-header-top .score-big { font-family: var(--ff-sora); font-size: 36px; font-weight: 600; color: var(--ink); }
|
|
286
|
+
.detail-header-sub { font-family: var(--ff-tomorrow); color: var(--dim); font-size: 12px; margin-top: 4px; }
|
|
287
|
+
/* The optional platform-ID line is an identifier, so it stays in the ID
|
|
288
|
+
face rather than taking the meta-line face by source order. */
|
|
289
|
+
.detail-header-sub.mono { font-family: var(--ff-sora); }
|
|
290
|
+
|
|
291
|
+
/* Tier pill — bigger version of the tier badge in the rail (square tag) */
|
|
256
292
|
.tier-pill {
|
|
257
|
-
font-
|
|
293
|
+
font-family: var(--ff-sora);
|
|
294
|
+
font-size: 11px; padding: 3px 8px; border-radius: 0;
|
|
258
295
|
font-weight: 600; letter-spacing: 0.04em;
|
|
259
296
|
}
|
|
260
297
|
.tier-pill[data-tier="hard"] { background: var(--hard); color: white; }
|
|
@@ -275,7 +312,7 @@ export async function renderIndexHtml(summaries, triage, participants, config, v
|
|
|
275
312
|
}
|
|
276
313
|
.signal-tile {
|
|
277
314
|
border: 1px solid var(--line);
|
|
278
|
-
border-radius:
|
|
315
|
+
border-radius: 0;
|
|
279
316
|
padding: 8px 10px;
|
|
280
317
|
display: flex;
|
|
281
318
|
flex-direction: column;
|
|
@@ -289,8 +326,8 @@ export async function renderIndexHtml(summaries, triage, participants, config, v
|
|
|
289
326
|
.signal-tile.tone-warn { color: var(--soft); border-color: var(--soft); background: rgba(245, 124, 0, 0.04); }
|
|
290
327
|
.signal-tile.tone-muted { color: var(--ink); border-color: var(--ink); background: rgba(26, 24, 20, 0.03); }
|
|
291
328
|
.signal-tile.tone-zero { color: #b8b1a0; border-color: #ebe6d8; background: transparent; }
|
|
292
|
-
.signal-value { font-size: 18px; font-weight: 700; line-height: 1; }
|
|
293
|
-
.signal-label { font-size: 10px; letter-spacing: 0.4px; text-transform: uppercase; opacity: 0.85; }
|
|
329
|
+
.signal-value { font-family: var(--ff-sora); font-size: 18px; font-weight: 700; line-height: 1; }
|
|
330
|
+
.signal-label { font-family: var(--ff-recursive); font-size: 10px; letter-spacing: 0.4px; text-transform: uppercase; opacity: 0.85; }
|
|
294
331
|
|
|
295
332
|
/* Score breakdown — horizontal flex of weighted contributions to t.score,
|
|
296
333
|
ending in "Total: N". Only non-zero terms render; the bar widths are
|
|
@@ -303,21 +340,22 @@ export async function renderIndexHtml(summaries, triage, participants, config, v
|
|
|
303
340
|
display: inline-flex; gap: 6px; align-items: center;
|
|
304
341
|
font-size: 12px;
|
|
305
342
|
}
|
|
306
|
-
.score-term .label { color: var(--dim); }
|
|
307
|
-
.score-term .contrib { color: var(--ink); }
|
|
343
|
+
.score-term .label { font-family: var(--ff-recursive); color: var(--dim); }
|
|
344
|
+
.score-term .contrib { font-family: var(--ff-sora); color: var(--ink); }
|
|
308
345
|
.score-term .bar {
|
|
309
|
-
height: 6px; background: var(--ink); opacity: 0.4; border-radius:
|
|
346
|
+
height: 6px; background: var(--ink); opacity: 0.4; border-radius: 2px;
|
|
310
347
|
}
|
|
311
|
-
.score-total { margin-left: auto; font-weight: 600; color: var(--ink); }
|
|
348
|
+
.score-total { font-family: var(--ff-sora); margin-left: auto; font-weight: 600; color: var(--ink); }
|
|
312
349
|
|
|
313
|
-
/* Reason
|
|
314
|
-
|
|
315
|
-
it shifts cleanly when the palette changes. */
|
|
350
|
+
/* Reason note — a hairline box on the surface (the earlier warm tint and
|
|
351
|
+
3px left rule were dropped in the 2026-09 reshape). */
|
|
316
352
|
.reason {
|
|
317
|
-
|
|
318
|
-
|
|
353
|
+
background: var(--surface);
|
|
354
|
+
border: 1px solid var(--line);
|
|
355
|
+
border-radius: 4px;
|
|
319
356
|
padding: 10px 14px;
|
|
320
357
|
margin: 0 0 16px;
|
|
358
|
+
font-family: var(--ff-tomorrow);
|
|
321
359
|
font-size: 14px;
|
|
322
360
|
color: var(--ink);
|
|
323
361
|
}
|
|
@@ -325,6 +363,7 @@ export async function renderIndexHtml(summaries, triage, participants, config, v
|
|
|
325
363
|
/* Session-level signals — small cards in a responsive grid. The grid
|
|
326
364
|
collapses from 3 cells to 1 in narrow detail panes via auto-fit. */
|
|
327
365
|
.section-heading {
|
|
366
|
+
font-family: var(--ff-sofia);
|
|
328
367
|
font-size: 13px; color: var(--dim);
|
|
329
368
|
text-transform: uppercase; letter-spacing: 0.04em;
|
|
330
369
|
margin: 18px 0 8px;
|
|
@@ -336,14 +375,16 @@ export async function renderIndexHtml(summaries, triage, participants, config, v
|
|
|
336
375
|
}
|
|
337
376
|
.sig-cell {
|
|
338
377
|
background: var(--surface); border: 1px solid var(--line);
|
|
339
|
-
border-radius:
|
|
378
|
+
border-radius: 0; padding: 10px;
|
|
379
|
+
box-shadow: 0 1px 2px rgba(0,0,0,0.06), 0 2px 8px rgba(0,0,0,0.05);
|
|
340
380
|
}
|
|
341
381
|
.sig-cell-title {
|
|
382
|
+
font-family: var(--ff-sofia);
|
|
342
383
|
font-size: 11px; color: var(--dim);
|
|
343
384
|
text-transform: uppercase; letter-spacing: 0.04em;
|
|
344
385
|
}
|
|
345
386
|
.sig-cell ul { list-style: none; margin-top: 4px; font-size: 12px; padding: 0; }
|
|
346
|
-
.sig-cell li { padding: 2px 0; word-break: break-word; }
|
|
387
|
+
.sig-cell li { font-family: var(--ff-recursive); padding: 2px 0; word-break: break-word; }
|
|
347
388
|
.muted { color: var(--dim); }
|
|
348
389
|
|
|
349
390
|
/* Paste evidence — list of expandable entries */
|
|
@@ -358,10 +399,11 @@ export async function renderIndexHtml(summaries, triage, participants, config, v
|
|
|
358
399
|
}
|
|
359
400
|
.paste-toggle:disabled { cursor: default; }
|
|
360
401
|
.paste-toggle:hover:not(:disabled) { color: var(--ink); }
|
|
361
|
-
.paste-trial { color: var(--dim); flex-shrink: 0; }
|
|
402
|
+
.paste-trial { font-family: var(--ff-majormono); color: var(--dim); flex-shrink: 0; }
|
|
362
403
|
.paste-preview, .paste-full {
|
|
363
|
-
background:
|
|
364
|
-
padding: 4px 8px; border-radius:
|
|
404
|
+
background: var(--surface); border: 1px solid var(--line); /* hairline box, like .reason */
|
|
405
|
+
padding: 4px 8px; border-radius: 0;
|
|
406
|
+
font-family: var(--ff-tomorrow);
|
|
365
407
|
font-size: 12px;
|
|
366
408
|
word-break: break-word; white-space: pre-wrap;
|
|
367
409
|
flex: 1; min-width: 0; /* allow flex item to shrink and wrap correctly */
|
|
@@ -369,7 +411,7 @@ export async function renderIndexHtml(summaries, triage, participants, config, v
|
|
|
369
411
|
/* When expanded by Task 11's JS: hide preview, show full text */
|
|
370
412
|
.paste-entry.expanded .paste-preview { display: none; }
|
|
371
413
|
.paste-entry:not(.expanded) .paste-full { display: none; }
|
|
372
|
-
.paste-overflow { font-size: 12px; margin-top: 4px; }
|
|
414
|
+
.paste-overflow { font-family: var(--ff-tomorrow); font-size: 12px; margin-top: 4px; }
|
|
373
415
|
|
|
374
416
|
/* Image blocks — wrapper containers so a missing PNG hides its heading too.
|
|
375
417
|
Each .image-block contains a <h4 class="section-heading"> + a zoomable
|
|
@@ -382,7 +424,8 @@ export async function renderIndexHtml(summaries, triage, participants, config, v
|
|
|
382
424
|
.image-block .section-heading { margin: 0 0 6px; }
|
|
383
425
|
.detail img {
|
|
384
426
|
max-width: 100%; display: block; margin: 6px 0 0;
|
|
385
|
-
border: 1px solid var(--line); border-radius:
|
|
427
|
+
border: 1px solid var(--line); border-radius: 0;
|
|
428
|
+
box-shadow: 0 1px 2px rgba(0,0,0,0.06), 0 2px 8px rgba(0,0,0,0.05);
|
|
386
429
|
}
|
|
387
430
|
.zoomable {
|
|
388
431
|
display: block; cursor: zoom-in;
|
|
@@ -422,7 +465,7 @@ export async function renderIndexHtml(summaries, triage, participants, config, v
|
|
|
422
465
|
max-width: 560px; max-height: calc(100vh - 16vh);
|
|
423
466
|
margin: 8vh auto 0;
|
|
424
467
|
padding: 20px 24px;
|
|
425
|
-
border-radius:
|
|
468
|
+
border-radius: 0;
|
|
426
469
|
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.25);
|
|
427
470
|
overflow-y: auto;
|
|
428
471
|
}
|
|
@@ -435,6 +478,7 @@ export async function renderIndexHtml(summaries, triage, participants, config, v
|
|
|
435
478
|
}
|
|
436
479
|
.modal-close:hover { color: var(--ink); }
|
|
437
480
|
.modal-card h3 {
|
|
481
|
+
font-family: var(--ff-space);
|
|
438
482
|
font-size: 16px; margin-bottom: 12px;
|
|
439
483
|
}
|
|
440
484
|
.legend-table {
|
|
@@ -446,7 +490,9 @@ export async function renderIndexHtml(summaries, triage, participants, config, v
|
|
|
446
490
|
border-bottom: 1px solid var(--line);
|
|
447
491
|
vertical-align: top;
|
|
448
492
|
}
|
|
493
|
+
.legend-table td { font-family: var(--ff-recursive); }
|
|
449
494
|
.legend-table th {
|
|
495
|
+
font-family: var(--ff-sofia);
|
|
450
496
|
color: var(--dim); font-weight: 500; font-size: 12px;
|
|
451
497
|
text-transform: uppercase; letter-spacing: 0.04em;
|
|
452
498
|
}
|
|
@@ -459,7 +505,7 @@ export async function renderIndexHtml(summaries, triage, participants, config, v
|
|
|
459
505
|
<body data-filter="all">
|
|
460
506
|
<header class="topbar">
|
|
461
507
|
<h1>Cyborg Hunter Report</h1>
|
|
462
|
-
<span class="meta">${triage.length} participants · v${VERSION}</span>
|
|
508
|
+
<span class="meta">${triage.length} participants · v${VERSION}${scoreWeightsNote(config)}</span>
|
|
463
509
|
<button class="legend-btn" type="button" aria-haspopup="dialog" aria-controls="legend-modal">Legend ⓘ</button>
|
|
464
510
|
</header>
|
|
465
511
|
<div class="layout">
|
|
@@ -728,50 +774,7 @@ export async function renderIndexHtml(summaries, triage, participants, config, v
|
|
|
728
774
|
})();
|
|
729
775
|
</script>
|
|
730
776
|
<style>
|
|
731
|
-
|
|
732
|
-
.replay-header { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; margin: 8px 0; }
|
|
733
|
-
.replay-badge { font-size: 11px; font-weight: 600; padding: 2px 8px; border-radius: 10px;
|
|
734
|
-
background: var(--ink); color: var(--surface); }
|
|
735
|
-
.replay-badge[data-tier="trace"] { background: var(--surface); color: var(--ink);
|
|
736
|
-
border: 1px solid var(--line); }
|
|
737
|
-
.replay-stage { position: relative; overflow: hidden; background: var(--surface);
|
|
738
|
-
border: 1px solid var(--line); border-radius: 4px; }
|
|
739
|
-
.replay-frame { position: absolute; top: 0; left: 0; border: 0; }
|
|
740
|
-
.replay-overlay { position: absolute; top: 0; left: 0; pointer-events: none; }
|
|
741
|
-
.replay-neutral { background: #e8e6e0; }
|
|
742
|
-
.replay-neutral-label { position: absolute; top: 50%; left: 50%; transform: translate(-50%,-50%);
|
|
743
|
-
color: var(--dim); font-size: 13px; }
|
|
744
|
-
.replay-lane { display: block; margin-top: 6px; border-radius: 2px; }
|
|
745
|
-
.replay-scrub { display: block; margin: 2px 0 4px; }
|
|
746
|
-
.replay-ticker { font: 12px/1.3 ui-monospace, SFMono-Regular, 'IBM Plex Mono', monospace;
|
|
747
|
-
color: var(--dim); font-variant-numeric: tabular-nums;
|
|
748
|
-
height: 1.4em; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; }
|
|
749
|
-
.replay-note { font-size: 12px; color: var(--dim); }
|
|
750
|
-
.replay-warn { color: var(--hard); }
|
|
751
|
-
/* Controls share the report's flat, line-bordered button style */
|
|
752
|
-
.replay-play, .replay-load-btn, .replay-css-btn, .replay-segment-select, .replay-speed {
|
|
753
|
-
padding: 4px 10px; border: 1px solid var(--line); background: var(--surface);
|
|
754
|
-
color: var(--ink); border-radius: 4px; cursor: pointer; font-size: 13px; }
|
|
755
|
-
.replay-play:hover, .replay-load-btn:hover, .replay-css-btn:hover { background: var(--bg); }
|
|
756
|
-
.replay-play:disabled, .replay-load-btn:disabled { opacity: 0.6; cursor: default; }
|
|
757
|
-
.replay-fetch-css-label { margin-left: 10px; font-size: 12px; color: #555; }
|
|
758
|
-
.replay-unstyled { position: absolute; left: 0; right: 0; top: 0; z-index: 3; background: #fff4dd; color: #7a4b00; border-bottom: 1px solid #e8b24a; padding: 6px 10px; font-size: 12px; line-height: 1.4; }
|
|
759
|
-
.replay-css-btn { font-size: 12px; padding: 2px 8px; }
|
|
760
|
-
.replay-clock { font: 12px/1.3 ui-monospace, SFMono-Regular, 'IBM Plex Mono', monospace;
|
|
761
|
-
font-variant-numeric: tabular-nums; }
|
|
762
|
-
.replay-keycast { position: absolute; left: 0; right: 0; bottom: 0; display: flex;
|
|
763
|
-
gap: 4px; padding: 5px 6px; pointer-events: none; flex-wrap: wrap-reverse; }
|
|
764
|
-
.replay-key-chip { font: 11px/1.2 ui-monospace, SFMono-Regular, 'IBM Plex Mono', monospace;
|
|
765
|
-
background: rgba(0,0,0,0.72); color: #fff; padding: 2px 7px;
|
|
766
|
-
border-radius: 3px; white-space: nowrap; }
|
|
767
|
-
.replay-key-chip--redacted { background: rgba(0,0,0,0.5); font-style: italic; }
|
|
768
|
-
/* Media is reported as state, never played (design §7): the badges say what
|
|
769
|
-
the recorded element was doing, in the stage's top-left. */
|
|
770
|
-
.replay-media { position: absolute; left: 0; top: 0; display: flex; gap: 4px;
|
|
771
|
-
padding: 5px 6px; pointer-events: none; flex-wrap: wrap; }
|
|
772
|
-
.replay-media-badge { font: 11px/1.2 ui-monospace, SFMono-Regular, 'IBM Plex Mono', monospace;
|
|
773
|
-
background: rgba(0,137,123,0.85); color: #fff; padding: 2px 7px;
|
|
774
|
-
border-radius: 3px; white-space: nowrap; }
|
|
777
|
+
${REPLAY_STYLES_CSS}
|
|
775
778
|
</style>
|
|
776
779
|
${preloadedReplayScript}<script>
|
|
777
780
|
${replayClientSrc}
|
|
@@ -846,7 +849,8 @@ function tierOf(t) {
|
|
|
846
849
|
// drop, long tab-aways, AI extensions detected).
|
|
847
850
|
// warn — soft-score-contributing signals that compound with others.
|
|
848
851
|
// muted — diagnostic signals that are noise alone but corroborate.
|
|
849
|
-
// Tone classes track
|
|
852
|
+
// Tone classes track the DEFAULT score weights (score-weights.js); custom
|
|
853
|
+
// config.scoreWeights do not retone the tiles. If the defaults change, audit this.
|
|
850
854
|
// `aiExtensionsCount` and `edgeExitCount` are derived (not on summary directly);
|
|
851
855
|
// renderSignalGrid handles that mapping.
|
|
852
856
|
const SIGNALS = [
|
|
@@ -960,7 +964,7 @@ function renderCohortRow(t) {
|
|
|
960
964
|
<div class="cohort-row-top">
|
|
961
965
|
<span class="tier-dot" data-tier="${tier}"></span>
|
|
962
966
|
<span class="mono pid" title="${esc(pid)}">${esc(pid)}</span>
|
|
963
|
-
<span class="mono score">${score}</span>
|
|
967
|
+
<span class="mono score">${formatScore(score)}</span>
|
|
964
968
|
</div>
|
|
965
969
|
<div class="cohort-row-bot">
|
|
966
970
|
<span class="reason-excerpt">${esc(reason)}</span>
|
|
@@ -1034,7 +1038,7 @@ function renderDetail(t, participant, config, visualsRendered, visualsUnavailabl
|
|
|
1034
1038
|
return `<section class="participant" id="p-${sanitized}"${defaultVisible ? '' : ' hidden'}>
|
|
1035
1039
|
${renderDetailHeader(t, tier, participant, config)}
|
|
1036
1040
|
${renderSignalGrid(s, t)}
|
|
1037
|
-
${renderScoreBreakdown(t)}
|
|
1041
|
+
${renderScoreBreakdown(t, config)}
|
|
1038
1042
|
${renderReasonQuote(t)}
|
|
1039
1043
|
${renderSessionBlock(s, participant)}
|
|
1040
1044
|
${renderPasteEvidence(participant)}
|
|
@@ -1238,7 +1242,7 @@ function renderDetailHeader(t, tier, participant, config) {
|
|
|
1238
1242
|
<div class="detail-header-top">
|
|
1239
1243
|
<span class="mono pid-full">${esc(t.participantId)}</span>
|
|
1240
1244
|
<span class="tier-pill" data-tier="${tier}">${tierLabel}</span>
|
|
1241
|
-
<span class="mono score-big">${t.score}</span>
|
|
1245
|
+
<span class="mono score-big">${formatScore(t.score)}</span>
|
|
1242
1246
|
</div>
|
|
1243
1247
|
<div class="detail-header-sub">
|
|
1244
1248
|
${s.trialCount ?? 0} trials · ${cps} cps · ${offTask} off-task
|
|
@@ -1247,9 +1251,12 @@ function renderDetailHeader(t, tier, participant, config) {
|
|
|
1247
1251
|
}
|
|
1248
1252
|
|
|
1249
1253
|
// Horizontal score breakdown — only non-zero contributions, ending in Total:N.
|
|
1250
|
-
//
|
|
1251
|
-
|
|
1252
|
-
|
|
1254
|
+
// Draws the terms rankTriage actually summed (t.terms). Rows built elsewhere
|
|
1255
|
+
// without terms (hand-built callers) are decomposed here with the CONFIGURED
|
|
1256
|
+
// weights, so the bars never disagree with a custom-weights header.
|
|
1257
|
+
function renderScoreBreakdown(t, config) {
|
|
1258
|
+
const terms = (t.terms ?? decomposeScore(t.summary || {}, t.edgeExitCount, t.hardTriggered,
|
|
1259
|
+
resolveScoreWeights(config?.scoreWeights).weights))
|
|
1253
1260
|
.filter(([, n]) => n > 0);
|
|
1254
1261
|
|
|
1255
1262
|
const total = t.score;
|
|
@@ -1259,17 +1266,24 @@ function renderScoreBreakdown(t) {
|
|
|
1259
1266
|
const termHtml = terms.map(([label, n]) =>
|
|
1260
1267
|
`<span class="score-term">
|
|
1261
1268
|
<span class="label">${label}</span>
|
|
1262
|
-
<span class="mono contrib">+${n}</span>
|
|
1269
|
+
<span class="mono contrib">+${formatScore(n)}</span>
|
|
1263
1270
|
<span class="bar" style="width:${barFor(n)}px"></span>
|
|
1264
1271
|
</span>`
|
|
1265
1272
|
).join('');
|
|
1266
1273
|
|
|
1267
1274
|
return `<div class="score-breakdown">
|
|
1268
1275
|
${termHtml}
|
|
1269
|
-
<span class="score-total mono">Total: ${total}</span>
|
|
1276
|
+
<span class="score-total mono">Total: ${formatScore(total)}</span>
|
|
1270
1277
|
</div>`;
|
|
1271
1278
|
}
|
|
1272
1279
|
|
|
1280
|
+
// Top-bar note, present only when config.scoreWeights changes the score from
|
|
1281
|
+
// the defaults: " · custom score weights: copy 5 max 3, synthetic 1".
|
|
1282
|
+
function scoreWeightsNote(config) {
|
|
1283
|
+
const { weights, isDefault } = resolveScoreWeights(config?.scoreWeights);
|
|
1284
|
+
return isDefault ? '' : ` · custom score weights: ${esc(customWeightsText(weights))}`;
|
|
1285
|
+
}
|
|
1286
|
+
|
|
1273
1287
|
// Format milliseconds as "Xs" / "Xm Ys".
|
|
1274
1288
|
function formatDuration(ms) {
|
|
1275
1289
|
if (!ms || ms < 1000) return '0s';
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
// src/cli/renderers/html-index.js
|
|
2
2
|
// Thin Node wrapper around the pure render core (html-index-core.js):
|
|
3
|
-
// reads the replay viewer client from disk, writes
|
|
3
|
+
// reads the replay viewer client and the embedded fonts from disk, writes
|
|
4
|
+
// index.html to outputDir.
|
|
4
5
|
// Public API unchanged — report.js and adopters keep calling renderHtmlIndex.
|
|
5
6
|
import { writeFileSync } from 'fs';
|
|
6
7
|
import { join } from 'path';
|
|
7
8
|
import { renderIndexHtml } from './html-index-core.js';
|
|
8
9
|
import { readReplayClientSrc } from './replay-client-source.js';
|
|
10
|
+
import { buildFontFaceCss } from './report-fonts.js';
|
|
9
11
|
|
|
10
12
|
// The replay viewer client is developed as real JS files (linted, syntax-
|
|
11
13
|
// highlighted) and embedded verbatim at render time — same file://-safe
|
|
@@ -13,10 +15,13 @@ import { readReplayClientSrc } from './replay-client-source.js';
|
|
|
13
15
|
// CONCATENATION: the client calls into src/replay/dom-instantiate.js, which
|
|
14
16
|
// ships ahead of it in the same script (see replay-client-source.js).
|
|
15
17
|
const REPLAY_CLIENT_SRC = readReplayClientSrc();
|
|
18
|
+
// The report's six typefaces as base64 @font-face rules (report-fonts.js),
|
|
19
|
+
// built once per process like the replay client.
|
|
20
|
+
const FONT_FACE_CSS = buildFontFaceCss();
|
|
16
21
|
|
|
17
22
|
export async function renderHtmlIndex(summaries, triage, participants, config, visualsRendered) {
|
|
18
23
|
const html = await renderIndexHtml(summaries, triage, participants, config,
|
|
19
|
-
visualsRendered, { replayClientSrc: REPLAY_CLIENT_SRC });
|
|
24
|
+
visualsRendered, { replayClientSrc: REPLAY_CLIENT_SRC, fontFaceCss: FONT_FACE_CSS });
|
|
20
25
|
writeFileSync(join(config.outputDir, 'index.html'), html);
|
|
21
26
|
console.log(' index.html — report page');
|
|
22
27
|
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
// src/cli/renderers/replay-styles.js
|
|
2
|
+
// The replay viewer's CSS (.replay-* rules), shared by the CLI report
|
|
3
|
+
// (html-index-core.js puts it in the report's second <style>) and the demo's
|
|
4
|
+
// replay-host iframe (demo/replay-host.js, which receives it from results.js
|
|
5
|
+
// through the preview-core bundle). One copy: the demo used to keep a
|
|
6
|
+
// hand-synced duplicate, which had drifted. Rules reference the report's
|
|
7
|
+
// tokens (--ink, --surface, --line, --bg, --dim, --hard and the --ff-* font
|
|
8
|
+
// stacks); a host document must declare them.
|
|
9
|
+
|
|
10
|
+
export const REPLAY_STYLES_CSS = ` /* Replay viewer (see replay-viewer.client.js) — matches report house style */
|
|
11
|
+
.replay-header { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; margin: 8px 0; }
|
|
12
|
+
.replay-badge { font-family: var(--ff-sora); font-size: 11px; font-weight: 600; padding: 2px 8px; border-radius: 0;
|
|
13
|
+
background: var(--ink); color: var(--surface); }
|
|
14
|
+
.replay-badge[data-tier="trace"] { background: var(--surface); color: var(--ink);
|
|
15
|
+
border: 1px solid var(--line); }
|
|
16
|
+
.replay-stage { position: relative; overflow: hidden; background: var(--surface);
|
|
17
|
+
border: 1px solid var(--line); border-radius: 0; }
|
|
18
|
+
.replay-frame { position: absolute; top: 0; left: 0; border: 0; }
|
|
19
|
+
.replay-overlay { position: absolute; top: 0; left: 0; pointer-events: none; }
|
|
20
|
+
.replay-neutral { background: #e8e6e0; }
|
|
21
|
+
.replay-neutral-label { position: absolute; top: 50%; left: 50%; transform: translate(-50%,-50%);
|
|
22
|
+
font-family: var(--ff-recursive); color: var(--dim); font-size: 13px; }
|
|
23
|
+
.replay-lane { display: block; margin-top: 6px; border-radius: 2px; }
|
|
24
|
+
.replay-scrub { display: block; margin: 2px 0 4px; }
|
|
25
|
+
.replay-ticker { font: 12px/1.3 var(--ff-majormono);
|
|
26
|
+
color: var(--dim); font-variant-numeric: tabular-nums;
|
|
27
|
+
height: 1.4em; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; }
|
|
28
|
+
.replay-note { font-family: var(--ff-recursive); font-size: 12px; color: var(--dim); }
|
|
29
|
+
.replay-warn { color: var(--hard); }
|
|
30
|
+
.replay-pause-toggle { font-family: var(--ff-recursive); }
|
|
31
|
+
/* Controls share the report's flat, line-bordered button style */
|
|
32
|
+
.replay-play, .replay-load-btn, .replay-css-btn, .replay-segment-select, .replay-speed {
|
|
33
|
+
padding: 4px 10px; border: 1px solid var(--line); background: var(--surface);
|
|
34
|
+
color: var(--ink); border-radius: 4px; cursor: pointer;
|
|
35
|
+
font-family: var(--ff-recursive); font-size: 13px; }
|
|
36
|
+
/* Play is the transport's primary action: a round ink button. Its hover
|
|
37
|
+
is its own (darker ink); the light shared hover below would leave a
|
|
38
|
+
white ▶ on a light ground. */
|
|
39
|
+
.replay-play { background: var(--ink); color: var(--surface); border-color: var(--ink);
|
|
40
|
+
width: 36px; height: 30px; padding: 0; border-radius: 50%; }
|
|
41
|
+
.replay-play:hover { background: #2b2b2b; }
|
|
42
|
+
.replay-load-btn:hover, .replay-css-btn:hover { background: var(--bg); }
|
|
43
|
+
.replay-play:disabled, .replay-load-btn:disabled { opacity: 0.6; cursor: default; }
|
|
44
|
+
.replay-fetch-css-label { font-family: var(--ff-recursive); margin-left: 10px; font-size: 12px; color: #555; }
|
|
45
|
+
.replay-unstyled { position: absolute; left: 0; right: 0; top: 0; z-index: 3; background: #fff4dd; color: #7a4b00; border-bottom: 1px solid #e8b24a; padding: 6px 10px; font-family: var(--ff-recursive); font-size: 12px; line-height: 1.4; }
|
|
46
|
+
.replay-css-btn { font-size: 12px; padding: 2px 8px; }
|
|
47
|
+
.replay-clock { font: 12px/1.3 var(--ff-majormono);
|
|
48
|
+
font-variant-numeric: tabular-nums; }
|
|
49
|
+
.replay-keycast { position: absolute; left: 0; right: 0; bottom: 0; display: flex;
|
|
50
|
+
gap: 4px; padding: 5px 6px; pointer-events: none; flex-wrap: wrap-reverse; }
|
|
51
|
+
.replay-key-chip { font: 400 13px/1.2 var(--ff-tomorrow);
|
|
52
|
+
background: rgba(0,0,0,0.72); color: #fff; padding: 3px 9px;
|
|
53
|
+
border-radius: 2px; white-space: nowrap; }
|
|
54
|
+
.replay-key-chip--redacted { background: rgba(0,0,0,0.5); font-style: italic; }
|
|
55
|
+
/* Media is reported as state, never played (design §7): the badges say what
|
|
56
|
+
the recorded element was doing, in the stage's top-left. */
|
|
57
|
+
.replay-media { position: absolute; left: 0; top: 0; display: flex; gap: 4px;
|
|
58
|
+
padding: 5px 6px; pointer-events: none; flex-wrap: wrap; }
|
|
59
|
+
.replay-media-badge { font: 11px/1.2 var(--ff-tomorrow);
|
|
60
|
+
background: rgba(0,137,123,0.85); color: #fff; padding: 2px 7px;
|
|
61
|
+
border-radius: 3px; white-space: nowrap; }`;
|
|
@@ -1881,16 +1881,18 @@
|
|
|
1881
1881
|
if (e.t >= playhead - 600) ripples.push(q);
|
|
1882
1882
|
}
|
|
1883
1883
|
}
|
|
1884
|
-
// Cursor trail:
|
|
1884
|
+
// Cursor trail: breadcrumb dots, one per sampled point (after a
|
|
1885
|
+
// segment's first), fading with age. Gaps between dots show pointer
|
|
1886
|
+
// speed; a severed segment simply has no dot bridging the break. Drawn
|
|
1887
|
+
// FIRST, and never at radius 2, 5 or 7: the alignment battery reads the
|
|
1888
|
+
// cursor glyph off the final arcs (r=5 confident, r=7 then r=2 uncertain).
|
|
1885
1889
|
segs.forEach(function (line) {
|
|
1886
1890
|
for (var s = 1; s < line.length; s++) {
|
|
1887
1891
|
var age = (playhead - line[s].t) / 2500;
|
|
1888
|
-
ctx.
|
|
1889
|
-
ctx.lineWidth = 2;
|
|
1892
|
+
ctx.fillStyle = 'rgba(211,47,47,' + (0.85 * (1 - age)).toFixed(3) + ')';
|
|
1890
1893
|
ctx.beginPath();
|
|
1891
|
-
ctx.
|
|
1892
|
-
ctx.
|
|
1893
|
-
ctx.stroke();
|
|
1894
|
+
ctx.arc(line[s].x, line[s].y, 2.2, 0, Math.PI * 2);
|
|
1895
|
+
ctx.fill();
|
|
1894
1896
|
}
|
|
1895
1897
|
});
|
|
1896
1898
|
// Click ripples: expanding rings over 600ms
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
// src/cli/renderers/report-fonts.js
|
|
2
|
+
// The report's typefaces, embedded so the report stays a single offline file.
|
|
3
|
+
// Reads the committed WOFF2 files listed in fonts/FONTS_MANIFEST.json and
|
|
4
|
+
// returns one @font-face block of base64 data URIs (≈227 KB for the six
|
|
5
|
+
// families). html-index.js passes it to renderIndexHtml as opts.fontFaceCss,
|
|
6
|
+
// the same way it passes the replay viewer source, so html-index-core.js
|
|
7
|
+
// stays pure (no fs) and can be bundled for the browser demo.
|
|
8
|
+
//
|
|
9
|
+
// Node only: keep this module out of anything src/cli/preview-entry.js
|
|
10
|
+
// imports (npm run demo:preview fails the build if it leaks in).
|
|
11
|
+
|
|
12
|
+
import { readFileSync } from 'fs';
|
|
13
|
+
import { join, dirname } from 'path';
|
|
14
|
+
import { fileURLToPath } from 'url';
|
|
15
|
+
import { fontFaceCss } from './font-face-css.js';
|
|
16
|
+
|
|
17
|
+
export const FONTS_DIR = join(dirname(fileURLToPath(import.meta.url)), 'fonts');
|
|
18
|
+
|
|
19
|
+
// The rule format lives in font-face-css.js (shared with the browser demo);
|
|
20
|
+
// this only supplies the bytes from disk.
|
|
21
|
+
export function buildFontFaceCss(dir = FONTS_DIR) {
|
|
22
|
+
const manifest = JSON.parse(readFileSync(join(dir, 'FONTS_MANIFEST.json'), 'utf8'));
|
|
23
|
+
return fontFaceCss(manifest.files, p => readFileSync(join(dir, p)).toString('base64'));
|
|
24
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// src/cli/renderers/score-weights.js
|
|
2
|
+
// Writes score-weights.json — the weights this report's triage score was built
|
|
3
|
+
// with (config.scoreWeights merged onto the defaults). Always written, so two
|
|
4
|
+
// reports can be checked for comparability without their config files.
|
|
5
|
+
// Warnings stay on the console (config.js); this file holds weights only.
|
|
6
|
+
|
|
7
|
+
import { writeFileSync } from 'fs';
|
|
8
|
+
import { join } from 'path';
|
|
9
|
+
import { resolveScoreWeights } from '../analyzers/score-weights.js';
|
|
10
|
+
|
|
11
|
+
export function renderScoreWeights(config) {
|
|
12
|
+
const { weights, isDefault } = resolveScoreWeights(config?.scoreWeights);
|
|
13
|
+
writeFileSync(join(config.outputDir, 'score-weights.json'),
|
|
14
|
+
JSON.stringify({ isDefault, weights }, null, 2) + '\n');
|
|
15
|
+
console.log(` score-weights.json — ${isDefault ? 'default' : 'custom'} weights`);
|
|
16
|
+
}
|
|
@@ -9,6 +9,8 @@ import { join } from 'path';
|
|
|
9
9
|
const COLUMNS = [
|
|
10
10
|
['participantId', (s, t) => s.participantId],
|
|
11
11
|
['trialCount', (s, t) => s.trialCount],
|
|
12
|
+
// The exact score rankTriage sorted on (not the one-decimal display form),
|
|
13
|
+
// so the analysis file never disagrees with the ranking.
|
|
12
14
|
['triageScore', (s, t) => t.score],
|
|
13
15
|
['hardTriggered', (s, t) => t.hardTriggered ? 'YES' : 'no'],
|
|
14
16
|
['triageReason', (s, t) => t.reason],
|