miriad-viz 0.1.0

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 (41) hide show
  1. package/README.md +139 -0
  2. package/bin/miriad-viz.mjs +45 -0
  3. package/dist-lib/frame-state-eeHrDxl-.d.cts +228 -0
  4. package/dist-lib/frame-state-eeHrDxl-.d.ts +228 -0
  5. package/dist-lib/index.cjs +2369 -0
  6. package/dist-lib/index.cjs.map +1 -0
  7. package/dist-lib/index.d.cts +707 -0
  8. package/dist-lib/index.d.ts +707 -0
  9. package/dist-lib/index.js +2352 -0
  10. package/dist-lib/index.js.map +1 -0
  11. package/dist-lib/layout-Cc23UM-j.d.cts +353 -0
  12. package/dist-lib/layout-Cc23UM-j.d.ts +353 -0
  13. package/dist-lib/renderer/index.cjs +3697 -0
  14. package/dist-lib/renderer/index.cjs.map +1 -0
  15. package/dist-lib/renderer/index.d.cts +205 -0
  16. package/dist-lib/renderer/index.d.ts +205 -0
  17. package/dist-lib/renderer/index.js +3668 -0
  18. package/dist-lib/renderer/index.js.map +1 -0
  19. package/dist-lib/viewer/exports.cjs +10572 -0
  20. package/dist-lib/viewer/exports.cjs.map +1 -0
  21. package/dist-lib/viewer/exports.d.cts +130 -0
  22. package/dist-lib/viewer/exports.d.ts +130 -0
  23. package/dist-lib/viewer/exports.js +10541 -0
  24. package/dist-lib/viewer/exports.js.map +1 -0
  25. package/package.json +108 -0
  26. package/template/remotion/README.md +139 -0
  27. package/template/remotion/package.json +29 -0
  28. package/template/remotion/pnpm-lock.yaml +2816 -0
  29. package/template/remotion/public/data/.gitkeep +0 -0
  30. package/template/remotion/remotion.config.ts +5 -0
  31. package/template/remotion/src/MiriadViz.tsx +220 -0
  32. package/template/remotion/src/Root.tsx +39 -0
  33. package/template/remotion/tsconfig.json +16 -0
  34. package/template/viewer/README.md +13 -0
  35. package/template/viewer/index.html +17 -0
  36. package/template/viewer/package.json +27 -0
  37. package/template/viewer/public/data/.gitkeep +0 -0
  38. package/template/viewer/src/App.tsx +313 -0
  39. package/template/viewer/src/main.tsx +4 -0
  40. package/template/viewer/tsconfig.json +16 -0
  41. package/template/viewer/vite.config.ts +12 -0
@@ -0,0 +1,2369 @@
1
+ 'use strict';
2
+
3
+ // src/shared-constants.ts
4
+ var SIM_HOURS_PER_SECOND = 0.6;
5
+ var AGENT_SHADER_NODE_RADIUS = 0.15;
6
+ var GLOW_RADIUS_RATIO = 3;
7
+ var PILL_GAP_RATIO = 0.2;
8
+ var PILL_GAP_BASE = 0;
9
+
10
+ // src/engine/pack-rows.ts
11
+ var MIN_BAR_WIDTH_NORM = 3e-3;
12
+ var ROW_GAP_NORM = 1e-3;
13
+ var MAX_BAR_WIDTH_NORM = 0.95;
14
+ function packArtifactRows(bars, progress, minGapNorm, minBarWidthNorm) {
15
+ const gap = Math.max(ROW_GAP_NORM, minGapNorm ?? ROW_GAP_NORM);
16
+ const effectiveBarWidth = Math.max(MIN_BAR_WIDTH_NORM, minBarWidthNorm ?? MIN_BAR_WIDTH_NORM);
17
+ const rowEnds = [];
18
+ for (const bar of bars) {
19
+ const start = bar.normalizedTime;
20
+ if (start < 0 || start > 1) {
21
+ bar.row = 0;
22
+ continue;
23
+ }
24
+ const rawEnd = start + effectiveBarWidth;
25
+ const end = Math.min(rawEnd, progress);
26
+ let row = 0;
27
+ for (row = 0; row < rowEnds.length; row++) {
28
+ if (rowEnds[row] <= start) break;
29
+ }
30
+ if (row >= rowEnds.length) {
31
+ rowEnds.push(-1);
32
+ }
33
+ rowEnds[row] = end + gap;
34
+ bar.row = row;
35
+ }
36
+ return bars;
37
+ }
38
+ function packPRRows(bars, progress, minGapNorm, minBarWidthNorm) {
39
+ const gap = Math.max(ROW_GAP_NORM, minGapNorm ?? ROW_GAP_NORM);
40
+ const minWidth = Math.max(MIN_BAR_WIDTH_NORM, minBarWidthNorm ?? MIN_BAR_WIDTH_NORM);
41
+ bars.sort((a, b) => a.openedNormalized - b.openedNormalized);
42
+ const rowEnds = [];
43
+ for (const bar of bars) {
44
+ const start = bar.openedNormalized;
45
+ if (start < 0 || start > 1) {
46
+ bar.row = 0;
47
+ continue;
48
+ }
49
+ const rawEnd = bar.mergedNormalized ?? progress;
50
+ const width = Math.min(MAX_BAR_WIDTH_NORM, Math.max(minWidth, rawEnd - start));
51
+ const end = Math.min(start + width, progress);
52
+ let row = 0;
53
+ for (row = 0; row < rowEnds.length; row++) {
54
+ if (rowEnds[row] <= start) break;
55
+ }
56
+ if (row >= rowEnds.length) {
57
+ rowEnds.push(-1);
58
+ }
59
+ rowEnds[row] = end + gap;
60
+ bar.row = row;
61
+ }
62
+ return bars;
63
+ }
64
+
65
+ // src/engine/get-frame-state.ts
66
+ var AGENT_FADE_DURATION = 0.01;
67
+ var BOUNCE_DURATION = 0.015;
68
+ var MIN_PARTICLE_SECONDS = 3;
69
+ var ACTIVITY_WINDOW = 0.01;
70
+ var ACTIVITY_RADIUS_BOOST = 1.5;
71
+ var WORK_WINDOW = 0.042;
72
+ var WORK_WEIGHTS = {
73
+ commit: 3,
74
+ pr_created: 5,
75
+ pr_merged: 5,
76
+ message: 1,
77
+ artifact: 2
78
+ };
79
+ var WORK_INTENSITY_MAX = 25;
80
+ var MIN_PILL_SECONDS = 5;
81
+ var DENSE_WINDOW_RATIO = 0.625;
82
+ var CLUSTER_THRESHOLD_RATIO = 2;
83
+ function secondsToNormalized(realSeconds, durationMs) {
84
+ const totalHours = durationMs / 36e5;
85
+ const playbackSeconds = totalHours / SIM_HOURS_PER_SECOND;
86
+ return realSeconds / playbackSeconds;
87
+ }
88
+ function computeTimeWindows(durationMs) {
89
+ const narrationWindow = secondsToNormalized(MIN_PILL_SECONDS, durationMs);
90
+ const narrationWindowDense = narrationWindow * DENSE_WINDOW_RATIO;
91
+ const narrationClusterThreshold = narrationWindow * CLUSTER_THRESHOLD_RATIO;
92
+ const particleWindow = secondsToNormalized(MIN_PARTICLE_SECONDS, durationMs);
93
+ return { narrationWindow, narrationWindowDense, narrationClusterThreshold, particleWindow };
94
+ }
95
+ var NARRATION_FADE_IN = 0.05;
96
+ var NARRATION_FADE_OUT = 0.08;
97
+ var EDITORIAL_FADE = 0;
98
+ var EDGE_SATURATION = 8;
99
+ var EDGE_MIN_STRENGTH = 0.01;
100
+ var NODE_RADIUS_REF = AGENT_SHADER_NODE_RADIUS;
101
+ var HUMAN_SIZE_MULTIPLIER = 1.5;
102
+ var MAX_WORK_GROWTH = 1;
103
+ var ACTIVITY_PULSE = 0.3;
104
+ var LABEL_GAP_RATIO = 0.65;
105
+ function getAgentAppearTime(agentId, events) {
106
+ for (const e of events) {
107
+ if (e.agent === agentId || e.targetAgent === agentId) {
108
+ return e.normalizedTime;
109
+ }
110
+ }
111
+ return null;
112
+ }
113
+ function computeBounceScale(timeSinceJoin) {
114
+ if (timeSinceJoin < 0) return 0;
115
+ const t = timeSinceJoin / BOUNCE_DURATION;
116
+ if (t >= 1) return 1;
117
+ if (t < 1 / 3) {
118
+ const p2 = t * 3;
119
+ return 1.2 * (1 - (1 - p2) ** 3);
120
+ }
121
+ if (t < 2 / 3) {
122
+ const p2 = (t - 1 / 3) * 3;
123
+ return 1.2 - 0.25 * p2;
124
+ }
125
+ const p = (t - 2 / 3) * 3;
126
+ return 0.95 + 0.05 * p;
127
+ }
128
+ function computeWorkIntensity(agentId, progress, events) {
129
+ const windowStart = progress - WORK_WINDOW;
130
+ let totalWork = 0;
131
+ for (const event of events) {
132
+ if (event.normalizedTime > progress) break;
133
+ if (event.normalizedTime < windowStart) continue;
134
+ if (event.agent !== agentId) continue;
135
+ const weight = WORK_WEIGHTS[event.type];
136
+ if (weight === void 0) continue;
137
+ const age = progress - event.normalizedTime;
138
+ const decayed = weight * Math.exp(-50 * age);
139
+ totalWork += decayed;
140
+ }
141
+ for (const event of events) {
142
+ if (event.normalizedTime > progress) break;
143
+ if (event.normalizedTime < windowStart) continue;
144
+ if (event.targetAgent !== agentId) continue;
145
+ const weight = WORK_WEIGHTS[event.type];
146
+ if (weight === void 0) continue;
147
+ const age = progress - event.normalizedTime;
148
+ const decayed = weight * Math.exp(-50 * age);
149
+ totalWork += decayed;
150
+ }
151
+ return Math.min(1, totalWork / WORK_INTENSITY_MAX);
152
+ }
153
+ var TOTAL_WORK_EVENT_TYPES = /* @__PURE__ */ new Set(["commit", "pr_created", "pr_merged"]);
154
+ function computeTotalWork(agentId, progress, events) {
155
+ let count = 0;
156
+ for (const event of events) {
157
+ if (event.normalizedTime > progress) break;
158
+ if (event.agent !== agentId) continue;
159
+ if (TOTAL_WORK_EVENT_TYPES.has(event.type)) {
160
+ count++;
161
+ }
162
+ }
163
+ return count;
164
+ }
165
+ function computeAgentState(agent, progress, events, maxTotalWork, layoutContext) {
166
+ const appearTime = getAgentAppearTime(agent.id, events);
167
+ if (appearTime === null || progress < appearTime) {
168
+ return null;
169
+ }
170
+ const timeSinceJoin = progress - appearTime;
171
+ const opacity = Math.min(1, timeSinceJoin / AGENT_FADE_DURATION);
172
+ const scale = computeBounceScale(timeSinceJoin);
173
+ const recentEvents = events.filter(
174
+ (e) => e.agent === agent.id && e.normalizedTime <= progress && e.normalizedTime > progress - ACTIVITY_WINDOW
175
+ );
176
+ const activityFactor = Math.min(1, recentEvents.length / 5);
177
+ const radiusMultiplier = 1 + activityFactor * (ACTIVITY_RADIUS_BOOST - 1);
178
+ const workIntensity = computeWorkIntensity(agent.id, progress, events);
179
+ const totalWork = computeTotalWork(agent.id, progress, events);
180
+ const isHuman = agent.role === "human";
181
+ const humanMul = isHuman ? HUMAN_SIZE_MULTIPLIER : 1;
182
+ const growthFactor = totalWork / maxTotalWork;
183
+ const activityPulse = 1 + workIntensity * ACTIVITY_PULSE;
184
+ const nodeRadius = layoutContext?.nodeRadius ?? NODE_RADIUS_REF;
185
+ const layoutScale = nodeRadius / NODE_RADIUS_REF;
186
+ const coreScale = humanMul * (1 + growthFactor * MAX_WORK_GROWTH) * activityPulse * scale * layoutScale;
187
+ const labelHeight = layoutContext?.labelHeight ?? nodeRadius * 1.6;
188
+ const visualRadius = nodeRadius * coreScale;
189
+ const labelGap = visualRadius * LABEL_GAP_RATIO;
190
+ const labelOffsetY = -(visualRadius + labelGap + labelHeight / 2);
191
+ return {
192
+ id: agent.id,
193
+ x: agent.position.x,
194
+ y: agent.position.y,
195
+ radius: agent.radius * radiusMultiplier,
196
+ color: agent.color,
197
+ opacity,
198
+ glowIntensity: activityFactor,
199
+ label: agent.label,
200
+ labelOpacity: opacity,
201
+ scale,
202
+ workIntensity,
203
+ totalWork,
204
+ isHuman,
205
+ role: agent.role,
206
+ coreScale,
207
+ labelOffsetY
208
+ };
209
+ }
210
+ var PARTICLE_TYPES = /* @__PURE__ */ new Set(["message"]);
211
+ var PARTICLE_TRAVEL_FRACTION = 1;
212
+ function computeParticles(agents, events, progress, particleWindow) {
213
+ const agentMap = new Map(agents.map((a) => [a.id, a]));
214
+ const particles = [];
215
+ for (const event of events) {
216
+ if (event.normalizedTime > progress) break;
217
+ if (!PARTICLE_TYPES.has(event.type)) continue;
218
+ const age = progress - event.normalizedTime;
219
+ if (age > particleWindow) continue;
220
+ const sender = agentMap.get(event.agent);
221
+ if (!sender) continue;
222
+ const receiver = event.targetAgent ? agentMap.get(event.targetAgent) : null;
223
+ if (!receiver) continue;
224
+ const t = age / particleWindow;
225
+ const travel = Math.min(t / PARTICLE_TRAVEL_FRACTION, 1);
226
+ const fade = t <= PARTICLE_TRAVEL_FRACTION ? 1 : 1 - (t - PARTICLE_TRAVEL_FRACTION) / (1 - PARTICLE_TRAVEL_FRACTION);
227
+ const px = sender.position.x + (receiver.position.x - sender.position.x) * travel;
228
+ const py = sender.position.y + (receiver.position.y - sender.position.y) * travel;
229
+ particles.push({
230
+ id: `particle-${event.type}-${event.normalizedTime}-${event.agent}`,
231
+ type: "message",
232
+ x: px,
233
+ y: py,
234
+ opacity: fade,
235
+ size: 4,
236
+ color: sender.color
237
+ });
238
+ }
239
+ return particles;
240
+ }
241
+ var NARRATION_ABOVE_RATIO = PILL_GAP_RATIO;
242
+ var NARRATION_ABOVE_BASE = PILL_GAP_BASE;
243
+ var PILL_QUOTE_FONT_PX = 16;
244
+ var PILL_SPEAKER_FONT_RATIO = 1.2;
245
+ var PILL_MAX_VIEWPORT_FRACTION = 0.75;
246
+ var PILL_MIN_HEIGHT_RATIO = 2;
247
+ var PILL_BG_DARKEN = 0.3;
248
+ var PILL_BG_MIN_BRIGHTNESS = 25;
249
+ var CHAR_WIDTH_RATIO = 0.75;
250
+ var LINE_HEIGHT_RATIO = 1.4;
251
+ function darkenHexForPill(hex, factor) {
252
+ const h = hex.replace("#", "");
253
+ const r = Math.max(
254
+ PILL_BG_MIN_BRIGHTNESS,
255
+ Math.round(Number.parseInt(h.slice(0, 2), 16) * factor)
256
+ );
257
+ const g = Math.max(
258
+ PILL_BG_MIN_BRIGHTNESS,
259
+ Math.round(Number.parseInt(h.slice(2, 4), 16) * factor)
260
+ );
261
+ const b = Math.max(
262
+ PILL_BG_MIN_BRIGHTNESS,
263
+ Math.round(Number.parseInt(h.slice(4, 6), 16) * factor)
264
+ );
265
+ return `#${r.toString(16).padStart(2, "0")}${g.toString(16).padStart(2, "0")}${b.toString(16).padStart(2, "0")}`;
266
+ }
267
+ function computePillLayout(speakerLabel, text, speakerColor, layoutContext) {
268
+ const nodeRadius = layoutContext?.nodeRadius ?? NODE_RADIUS_REF;
269
+ const ppu = layoutContext ? layoutContext.viewportPxWidth / layoutContext.worldWidth : 128;
270
+ const padX = layoutContext?.narrationPadX ?? 0.25;
271
+ const padY = layoutContext?.narrationPadY ?? 0.14;
272
+ const maxWidth = layoutContext?.narrationMaxWidth ?? 4;
273
+ const fontSize = PILL_QUOTE_FONT_PX;
274
+ const speakerFontSize = Math.round(fontSize * PILL_SPEAKER_FONT_RATIO);
275
+ const fontWorldUnits = fontSize / ppu;
276
+ const viewportMaxWorld = layoutContext ? layoutContext.viewportPxWidth * PILL_MAX_VIEWPORT_FRACTION / ppu : maxWidth;
277
+ const maxPillWidth = Math.min(maxWidth, viewportMaxWorld);
278
+ const textAreaWidth = maxPillWidth - padX * 2;
279
+ const speakerPrefix = "";
280
+ const speakerWidthWorld = speakerPrefix.length * (speakerFontSize / ppu) * CHAR_WIDTH_RATIO;
281
+ const fullText = text;
282
+ const fullTextWidthWorld = fullText.length * fontWorldUnits * CHAR_WIDTH_RATIO;
283
+ const totalTextWidth = speakerWidthWorld + fullTextWidthWorld;
284
+ let lines;
285
+ const charWidthWorld = fontWorldUnits * CHAR_WIDTH_RATIO;
286
+ if (totalTextWidth <= textAreaWidth) {
287
+ lines = [fullText];
288
+ } else {
289
+ const firstLineAvail = textAreaWidth - speakerWidthWorld;
290
+ const charsPerLine = Math.max(10, Math.floor(textAreaWidth / charWidthWorld));
291
+ const charsFirstLine = Math.max(5, Math.floor(firstLineAvail / charWidthWorld));
292
+ lines = [];
293
+ const words = fullText.split(" ");
294
+ let currentLine = "";
295
+ let currentChars = 0;
296
+ for (const word of words) {
297
+ const limit = lines.length === 0 ? charsFirstLine : charsPerLine;
298
+ if (currentLine && currentChars + 1 + word.length > limit) {
299
+ lines.push(currentLine);
300
+ currentLine = word;
301
+ currentChars = word.length;
302
+ } else {
303
+ currentLine = currentLine ? `${currentLine} ${word}` : word;
304
+ currentChars = currentLine.length;
305
+ }
306
+ }
307
+ if (currentLine) lines.push(currentLine);
308
+ if (lines.length > 3) {
309
+ lines = lines.slice(0, 3);
310
+ lines[2] = `${lines[2].slice(0, -3)}...`;
311
+ }
312
+ }
313
+ const lineHeightWorld = fontWorldUnits * LINE_HEIGHT_RATIO;
314
+ const textHeight = lineHeightWorld * lines.length;
315
+ const computedHeight = textHeight + padY * 2;
316
+ const minHeight = nodeRadius * PILL_MIN_HEIGHT_RATIO;
317
+ const estimatedHeight = Math.max(computedHeight, minHeight);
318
+ const estimatedWidth = lines.length === 1 ? Math.min(totalTextWidth + padX * 2, maxPillWidth) : maxPillWidth;
319
+ const pillBgColor = darkenHexForPill(speakerColor, PILL_BG_DARKEN);
320
+ return {
321
+ lines,
322
+ padX,
323
+ padY,
324
+ maxPillWidth,
325
+ estimatedWidth,
326
+ estimatedHeight,
327
+ speakerLabel,
328
+ speakerColor,
329
+ pillBgColor
330
+ };
331
+ }
332
+ function computeQuotePillPosition(quote, visibleAgents, agents, layoutContext, estimatedHeight, estimatedWidth) {
333
+ const nodeRadius = layoutContext?.nodeRadius ?? NODE_RADIUS_REF;
334
+ const gap = nodeRadius * NARRATION_ABOVE_RATIO + NARRATION_ABOVE_BASE;
335
+ const speaker = visibleAgents.find((a) => a.id === quote.speaker);
336
+ const speakerAgent = agents.find((a) => a.id === quote.speaker);
337
+ if (speaker && speakerAgent) {
338
+ const speakerCoreScale = speaker.coreScale ?? 1;
339
+ const coreRadius = nodeRadius * speakerCoreScale;
340
+ let yOffset = coreRadius + gap + estimatedHeight / 2;
341
+ if (layoutContext?.agentToWorld && layoutContext.agentsBandTop !== void 0) {
342
+ const agentWorldY = layoutContext.agentToWorld(
343
+ speakerAgent.position.x,
344
+ speakerAgent.position.y
345
+ ).y;
346
+ const pillTop = agentWorldY + yOffset + estimatedHeight / 2;
347
+ const margin = 0.02;
348
+ const maxPillTop = layoutContext.agentsBandTop - margin;
349
+ if (pillTop > maxPillTop) {
350
+ yOffset -= pillTop - maxPillTop;
351
+ }
352
+ }
353
+ let xOffset = 0;
354
+ if (layoutContext?.agentToWorld) {
355
+ const agentWorldX = layoutContext.agentToWorld(
356
+ speakerAgent.position.x,
357
+ speakerAgent.position.y
358
+ ).x;
359
+ const margin = 0.02;
360
+ const minLeft = layoutContext.worldLeft + margin;
361
+ const maxRight = layoutContext.worldRight - margin;
362
+ const clampWidth = estimatedWidth * 1.1;
363
+ const pillLeft = agentWorldX - clampWidth / 2;
364
+ const pillRight = agentWorldX + clampWidth / 2;
365
+ if (pillLeft < minLeft) {
366
+ xOffset = minLeft - pillLeft;
367
+ } else if (pillRight > maxRight) {
368
+ xOffset = maxRight - pillRight;
369
+ }
370
+ }
371
+ return {
372
+ speakerNormX: speakerAgent.position.x,
373
+ speakerNormY: speakerAgent.position.y,
374
+ yOffset,
375
+ xOffset
376
+ };
377
+ }
378
+ return { speakerNormX: 0.5, speakerNormY: 0.5, yOffset: 0, xOffset: 0 };
379
+ }
380
+ function computePillSchedule(quotes, narrationWindow, narrationWindowDense, narrationClusterThreshold) {
381
+ const schedule = /* @__PURE__ */ new Map();
382
+ const bySpeaker = /* @__PURE__ */ new Map();
383
+ for (let i = 0; i < quotes.length; i++) {
384
+ const q = quotes[i];
385
+ let group = bySpeaker.get(q.speaker);
386
+ if (!group) {
387
+ group = [];
388
+ bySpeaker.set(q.speaker, group);
389
+ }
390
+ group.push({ idx: i, quote: q });
391
+ }
392
+ for (const group of bySpeaker.values()) {
393
+ group.sort((a, b) => a.quote.normalizedTime - b.quote.normalizedTime);
394
+ let lastEnd = -1;
395
+ for (let gi = 0; gi < group.length; gi++) {
396
+ const { idx, quote } = group[gi];
397
+ const naturalStart = quote.normalizedTime;
398
+ const nextQuote = gi + 1 < group.length ? group[gi + 1].quote : null;
399
+ const isDense = nextQuote !== null && nextQuote.normalizedTime - naturalStart < narrationClusterThreshold;
400
+ const window = isDense ? narrationWindowDense : narrationWindow;
401
+ let effectiveStart = Math.max(naturalStart, lastEnd);
402
+ const minWindow = window * 0.5;
403
+ if (effectiveStart + minWindow > 1) {
404
+ effectiveStart = Math.min(effectiveStart, 1 - minWindow);
405
+ }
406
+ const effectiveWindow = Math.min(window, 1 - effectiveStart);
407
+ schedule.set(idx, { effectiveStart, window: Math.max(effectiveWindow, minWindow) });
408
+ lastEnd = effectiveStart + Math.max(effectiveWindow, minWindow);
409
+ }
410
+ }
411
+ return schedule;
412
+ }
413
+ function computeQuotePills(quotes, progress, agents, visibleAgents, layoutContext, narrationWindow, narrationWindowDense, narrationClusterThreshold) {
414
+ const result = [];
415
+ const schedule = computePillSchedule(
416
+ quotes,
417
+ narrationWindow,
418
+ narrationWindowDense,
419
+ narrationClusterThreshold
420
+ );
421
+ for (let i = 0; i < quotes.length; i++) {
422
+ const quote = quotes[i];
423
+ const timing = schedule.get(i);
424
+ if (!timing) continue;
425
+ const { effectiveStart, window } = timing;
426
+ const end = effectiveStart + window;
427
+ if (progress < effectiveStart || progress > end) continue;
428
+ const elapsed = progress - effectiveStart;
429
+ const fadeIn = window * NARRATION_FADE_IN;
430
+ const fadeOut = window * NARRATION_FADE_OUT;
431
+ let opacity;
432
+ if (elapsed < fadeIn) {
433
+ opacity = elapsed / fadeIn;
434
+ } else if (elapsed > window - fadeOut) {
435
+ opacity = (window - elapsed) / fadeOut;
436
+ } else {
437
+ opacity = 1;
438
+ }
439
+ const speakerAgent = visibleAgents.find((a) => a.id === quote.speaker);
440
+ const speakerColor = speakerAgent?.color ?? "#3b82f6";
441
+ const speakerLabel = "";
442
+ const pill = computePillLayout(speakerLabel, quote.text, speakerColor, layoutContext);
443
+ const { speakerNormX, speakerNormY, yOffset, xOffset } = computeQuotePillPosition(
444
+ quote,
445
+ visibleAgents,
446
+ agents,
447
+ layoutContext,
448
+ pill.estimatedHeight,
449
+ pill.estimatedWidth
450
+ );
451
+ result.push({
452
+ text: quote.text,
453
+ speaker: quote.speaker,
454
+ opacity: Math.max(0, Math.min(1, opacity)),
455
+ speakerNormX,
456
+ speakerNormY,
457
+ yOffset,
458
+ xOffset,
459
+ lines: pill.lines,
460
+ padX: pill.padX,
461
+ padY: pill.padY,
462
+ maxPillWidth: pill.maxPillWidth,
463
+ estimatedWidth: pill.estimatedWidth,
464
+ speakerLabel: pill.speakerLabel,
465
+ speakerColor: pill.speakerColor,
466
+ pillBgColor: pill.pillBgColor,
467
+ ...quote.emoji && { emoji: quote.emoji },
468
+ ...quote.mood && { mood: quote.mood }
469
+ });
470
+ }
471
+ return result;
472
+ }
473
+ function computeEditorialNarration(queue, progress) {
474
+ if (queue.length === 0) return null;
475
+ let lo = 0;
476
+ let hi = queue.length - 1;
477
+ let idx = -1;
478
+ while (lo <= hi) {
479
+ const mid = lo + hi >>> 1;
480
+ if (queue[mid].normalizedTime <= progress) {
481
+ idx = mid;
482
+ lo = mid + 1;
483
+ } else {
484
+ hi = mid - 1;
485
+ }
486
+ }
487
+ if (idx < 0) return null;
488
+ const entry = queue[idx];
489
+ const nextTime = idx < queue.length - 1 ? queue[idx + 1].normalizedTime : 1;
490
+ const entryDuration = nextTime - entry.normalizedTime;
491
+ const elapsed = progress - entry.normalizedTime;
492
+ const fadeIn = Math.min(EDITORIAL_FADE, entryDuration * 0.3);
493
+ const fadeOut = Math.min(EDITORIAL_FADE, entryDuration * 0.3);
494
+ let opacity;
495
+ if (elapsed < fadeIn) {
496
+ opacity = fadeIn > 0 ? elapsed / fadeIn : 1;
497
+ } else if (elapsed > entryDuration - fadeOut) {
498
+ opacity = fadeOut > 0 ? (entryDuration - elapsed) / fadeOut : 1;
499
+ } else {
500
+ opacity = 1;
501
+ }
502
+ return {
503
+ phaseTitle: entry.phaseTitle,
504
+ phaseColor: entry.phaseColor,
505
+ text: entry.text,
506
+ opacity: Math.max(0, Math.min(1, opacity))
507
+ };
508
+ }
509
+ function computePhase(phases, progress) {
510
+ for (const phase of phases) {
511
+ if (progress >= phase.startNormalized && progress <= phase.endNormalized) {
512
+ const phaseProgress = (progress - phase.startNormalized) / (phase.endNormalized - phase.startNormalized);
513
+ return {
514
+ id: phase.id,
515
+ name: phase.name,
516
+ progress: Math.max(0, Math.min(1, phaseProgress)),
517
+ color: phase.color
518
+ };
519
+ }
520
+ }
521
+ const lastPhase = phases[phases.length - 1];
522
+ if (lastPhase) {
523
+ return {
524
+ id: lastPhase.id,
525
+ name: lastPhase.name,
526
+ progress: progress >= lastPhase.endNormalized ? 1 : 0,
527
+ color: lastPhase.color
528
+ };
529
+ }
530
+ return { id: "unknown", name: "Unknown", progress: 0, color: "#888888" };
531
+ }
532
+ function computeStats(events, agents, progress) {
533
+ let commitsToDate = 0;
534
+ let prsToDate = 0;
535
+ let messagesToDate = 0;
536
+ for (const e of events) {
537
+ if (e.normalizedTime > progress) break;
538
+ if (e.type === "commit") commitsToDate++;
539
+ else if (e.type === "pr_merged") prsToDate++;
540
+ else if (e.type === "message") messagesToDate++;
541
+ }
542
+ return {
543
+ commitsToDate,
544
+ prsToDate,
545
+ messagesToDate,
546
+ activeAgents: agents.length
547
+ };
548
+ }
549
+ var COMMIT_TYPE_COLORS = {
550
+ feat: "#00e5cc",
551
+ // cyan — new features
552
+ fix: "#ffaa33",
553
+ // orange — bug fixes
554
+ hotfix: "#ffaa33",
555
+ // orange — urgent fixes
556
+ rework: "#ffaa33",
557
+ // orange — rework
558
+ infra: "#4488ff",
559
+ // blue — infrastructure
560
+ ci: "#4488ff",
561
+ // blue — CI/CD
562
+ refactor: "#4488ff",
563
+ // blue — refactoring
564
+ scaffold: "#4488ff",
565
+ // blue — scaffolding
566
+ test: "#aa66ff",
567
+ // purple — tests
568
+ perf: "#00e5cc",
569
+ // cyan — performance
570
+ docs: "#6bcb77",
571
+ // green — documentation
572
+ chore: "#555555",
573
+ // dim — chores
574
+ tweak: "#555555",
575
+ // dim — tweaks
576
+ merge: "#333333",
577
+ // very dim — merges
578
+ other: "#555555"
579
+ // dim — unclassified
580
+ };
581
+ var DEFAULT_COMMIT_COLOR = "#555555";
582
+ function computeCommitDots(events, progress) {
583
+ const rawDots = [];
584
+ let maxSize = 0;
585
+ for (const event of events) {
586
+ if (event.normalizedTime > progress) break;
587
+ if (event.type !== "commit") continue;
588
+ const insertions = event.metadata.insertions ?? 0;
589
+ const deletions = event.metadata.deletions ?? 0;
590
+ const commitType = event.metadata.commitType ?? "other";
591
+ const size = insertions + deletions;
592
+ if (size > maxSize) maxSize = size;
593
+ rawDots.push({
594
+ normalizedTime: event.normalizedTime,
595
+ insertions,
596
+ deletions,
597
+ agent: event.agent,
598
+ commitType,
599
+ color: COMMIT_TYPE_COLORS[commitType] ?? DEFAULT_COMMIT_COLOR,
600
+ size
601
+ });
602
+ }
603
+ const sqrtMax = Math.sqrt(maxSize);
604
+ const dots = rawDots.map((d) => ({
605
+ normalizedTime: d.normalizedTime,
606
+ insertions: d.insertions,
607
+ deletions: d.deletions,
608
+ agent: d.agent,
609
+ commitType: d.commitType,
610
+ color: d.color,
611
+ heightNorm: sqrtMax > 0 ? Math.sqrt(d.size) / sqrtMax : 0
612
+ }));
613
+ return dots;
614
+ }
615
+ function computePRBars(agents, events, progress) {
616
+ const agentMap = new Map(agents.map((a) => [a.id, a]));
617
+ const createdMap = /* @__PURE__ */ new Map();
618
+ const mergedMap = /* @__PURE__ */ new Map();
619
+ for (const event of events) {
620
+ if (event.normalizedTime > progress) break;
621
+ const prNumber = event.metadata.prNumber;
622
+ if (prNumber === void 0) continue;
623
+ if (event.type === "pr_created") {
624
+ createdMap.set(prNumber, event);
625
+ } else if (event.type === "pr_merged") {
626
+ mergedMap.set(prNumber, event);
627
+ }
628
+ }
629
+ const bars = [];
630
+ for (const [prNumber, created] of createdMap) {
631
+ const agent = agentMap.get(created.agent);
632
+ if (!agent) continue;
633
+ const merged = mergedMap.get(prNumber);
634
+ const additions = created.metadata.additions ?? 0;
635
+ const deletions = created.metadata.deletions ?? 0;
636
+ bars.push({
637
+ prNumber,
638
+ openedNormalized: created.normalizedTime,
639
+ mergedNormalized: merged ? merged.normalizedTime : null,
640
+ agent: created.agent,
641
+ color: agent.color,
642
+ additions,
643
+ deletions,
644
+ row: 0
645
+ // populated by packPRRows()
646
+ });
647
+ }
648
+ return bars;
649
+ }
650
+ var ARTIFACT_TYPE_COLORS = {
651
+ task: "#00e5cc",
652
+ decision: "#ffd93d",
653
+ doc: "#4d96ff",
654
+ code: "#c084fc",
655
+ asset: "#ff8c42"
656
+ };
657
+ var DEFAULT_ARTIFACT_COLOR = "#4d96ff";
658
+ function computeArtifactBars(_agents, events, progress) {
659
+ const bars = [];
660
+ for (const event of events) {
661
+ if (event.normalizedTime > progress) break;
662
+ if (event.type !== "artifact") continue;
663
+ const artifactType = event.metadata.type ?? "doc";
664
+ bars.push({
665
+ normalizedTime: event.normalizedTime,
666
+ type: artifactType,
667
+ agent: event.agent,
668
+ color: ARTIFACT_TYPE_COLORS[artifactType] ?? DEFAULT_ARTIFACT_COLOR,
669
+ row: 0
670
+ // populated by packArtifactRows()
671
+ });
672
+ }
673
+ return bars;
674
+ }
675
+ function computeEdges(agents, events, progress) {
676
+ const agentMap = new Map(agents.map((a) => [a.id, a]));
677
+ const edgeStrengths = /* @__PURE__ */ new Map();
678
+ for (const event of events) {
679
+ if (event.normalizedTime > progress) break;
680
+ if (event.type !== "message" || !event.targetAgent) continue;
681
+ if (event.agent === event.targetAgent) continue;
682
+ const pair = event.agent < event.targetAgent ? `${event.agent}:${event.targetAgent}` : `${event.targetAgent}:${event.agent}`;
683
+ const age = progress - event.normalizedTime;
684
+ const decayedStrength = Math.exp(-5 * age);
685
+ edgeStrengths.set(pair, (edgeStrengths.get(pair) ?? 0) + decayedStrength);
686
+ }
687
+ if (edgeStrengths.size === 0) return [];
688
+ const maxRaw = Math.max(...edgeStrengths.values(), 1);
689
+ const edges = [];
690
+ for (const [pair, raw] of edgeStrengths) {
691
+ const strength = Math.min(raw / maxRaw, raw / EDGE_SATURATION);
692
+ if (strength < EDGE_MIN_STRENGTH) continue;
693
+ const [fromId, toId] = pair.split(":");
694
+ const fromAgent = agentMap.get(fromId);
695
+ const toAgent = agentMap.get(toId);
696
+ if (!fromAgent) continue;
697
+ const fromColor = fromAgent.color;
698
+ const toColor = toAgent?.color ?? fromColor;
699
+ edges.push({
700
+ fromId,
701
+ toId,
702
+ strength,
703
+ color: fromColor,
704
+ fromColor,
705
+ toColor
706
+ });
707
+ }
708
+ return edges;
709
+ }
710
+ function computeMilestones(vizData, progress) {
711
+ return vizData.milestones.map((m) => ({
712
+ normalizedTime: m.normalizedTime,
713
+ label: m.label,
714
+ visible: progress >= m.normalizedTime
715
+ }));
716
+ }
717
+ var LABEL_FADE_ZONE = 5e-3;
718
+ function computePhaseLabelOpacity(progress, startNormalized, endNormalized, isLast = false) {
719
+ const upperBound = isLast ? 1 : endNormalized;
720
+ if (progress < startNormalized || progress > upperBound) return 0;
721
+ const fadeIn = Math.min((progress - startNormalized) / LABEL_FADE_ZONE, 1);
722
+ const fadeOut = isLast ? 1 : Math.min((endNormalized - progress) / LABEL_FADE_ZONE, 1);
723
+ return Math.min(fadeIn, fadeOut);
724
+ }
725
+ function computeAllPhases(phases, progress) {
726
+ return phases.map((p, i) => ({
727
+ id: p.id,
728
+ name: p.name,
729
+ startNormalized: p.startNormalized,
730
+ endNormalized: p.endNormalized,
731
+ color: p.color,
732
+ labelOpacity: computePhaseLabelOpacity(
733
+ progress,
734
+ p.startNormalized,
735
+ p.endNormalized,
736
+ i === phases.length - 1
737
+ )
738
+ }));
739
+ }
740
+ function smoothstep(t) {
741
+ const x = Math.max(0, Math.min(1, t));
742
+ return x * x * (3 - 2 * x);
743
+ }
744
+ function computeTrustLevel(keyframes, progress) {
745
+ if (keyframes.length === 0) return 0.5;
746
+ if (progress <= keyframes[0].normalizedTime) {
747
+ return keyframes[0].level;
748
+ }
749
+ if (progress >= keyframes[keyframes.length - 1].normalizedTime) {
750
+ return keyframes[keyframes.length - 1].level;
751
+ }
752
+ for (let i = 0; i < keyframes.length - 1; i++) {
753
+ const a = keyframes[i];
754
+ const b = keyframes[i + 1];
755
+ if (progress >= a.normalizedTime && progress <= b.normalizedTime) {
756
+ const t = (progress - a.normalizedTime) / (b.normalizedTime - a.normalizedTime);
757
+ return a.level + (b.level - a.level) * smoothstep(t);
758
+ }
759
+ }
760
+ return keyframes[keyframes.length - 1].level;
761
+ }
762
+ function computeMaxTotalWork(agents, progress, events) {
763
+ let max = 1;
764
+ for (const agent of agents) {
765
+ const work = computeTotalWork(agent.id, progress, events);
766
+ if (work > max) max = work;
767
+ }
768
+ return max;
769
+ }
770
+ function getFrameState(progress, vizData, layoutContext) {
771
+ const p = Math.max(0, Math.min(1, progress));
772
+ const tw = computeTimeWindows(vizData.timeRange.durationMs);
773
+ const maxTotalWork = computeMaxTotalWork(vizData.agents, p, vizData.events);
774
+ const agents = [];
775
+ for (const agent of vizData.agents) {
776
+ const state = computeAgentState(agent, p, vizData.events, maxTotalWork, layoutContext);
777
+ if (state) agents.push(state);
778
+ }
779
+ const particles = computeParticles(vizData.agents, vizData.events, p, tw.particleWindow);
780
+ const quotePills = computeQuotePills(
781
+ vizData.quotes,
782
+ p,
783
+ vizData.agents,
784
+ agents,
785
+ layoutContext,
786
+ tw.narrationWindow,
787
+ tw.narrationWindowDense,
788
+ tw.narrationClusterThreshold
789
+ );
790
+ const phase = computePhase(vizData.phases, p);
791
+ const stats = computeStats(vizData.events, agents, p);
792
+ const commitDots = computeCommitDots(vizData.events, p);
793
+ const MIN_GAP_PX = 3;
794
+ const timeWidth = layoutContext?.timeWidth;
795
+ const minGapNorm = layoutContext && layoutContext.viewportPxWidth > 0 && timeWidth && timeWidth > 0 ? MIN_GAP_PX / (layoutContext.viewportPxWidth / layoutContext.worldWidth * timeWidth) : void 0;
796
+ const minBarWidthNorm = layoutContext?.minBarWidthNorm;
797
+ const prBars = packPRRows(
798
+ computePRBars(vizData.agents, vizData.events, p),
799
+ p,
800
+ minGapNorm,
801
+ minBarWidthNorm
802
+ );
803
+ const artifactBars = packArtifactRows(
804
+ computeArtifactBars(vizData.agents, vizData.events, p),
805
+ p,
806
+ minGapNorm,
807
+ minBarWidthNorm
808
+ );
809
+ const edges = computeEdges(vizData.agents, vizData.events, p);
810
+ const visibleMilestones = computeMilestones(vizData, p);
811
+ const allPhases = computeAllPhases(vizData.phases, p);
812
+ const editorialNarration = computeEditorialNarration(vizData.editorialNarration ?? [], p);
813
+ const trustLevel = computeTrustLevel(vizData.trustKeyframes ?? [], p);
814
+ const totalHours = vizData.timeRange.durationMs / (1e3 * 60 * 60);
815
+ const currentHours = totalHours * p;
816
+ return {
817
+ projectTitle: vizData.projectTitle ?? "miriad-viz",
818
+ progress: p,
819
+ agents,
820
+ particles,
821
+ quotePills,
822
+ editorialNarration,
823
+ phase,
824
+ stats,
825
+ commitDots,
826
+ prBars,
827
+ artifactBars,
828
+ edges,
829
+ visibleMilestones,
830
+ allPhases,
831
+ trustLevel,
832
+ totalHours,
833
+ currentHours
834
+ };
835
+ }
836
+
837
+ // src/processing/process-data.ts
838
+ function deriveCommitType(message) {
839
+ if (!message) return "other";
840
+ const match = message.match(/^(\w+)(?:\([^)]*\))?:/);
841
+ return match ? match[1].toLowerCase() : "other";
842
+ }
843
+ var TIMELINE_TAIL_FRACTION = 0.02;
844
+ function deriveTimeRange(rawData, curationData, options) {
845
+ const timestamps = [];
846
+ for (const c of rawData.commits) timestamps.push(c.timestamp);
847
+ for (const p of rawData.prs) {
848
+ timestamps.push(p.createdAt);
849
+ if (p.mergedAt) timestamps.push(p.mergedAt);
850
+ }
851
+ for (const m of rawData.messages) timestamps.push(m.timestamp);
852
+ for (const a of rawData.agents) timestamps.push(a.joinedAt);
853
+ for (const p of curationData.phases) {
854
+ timestamps.push(p.startTime);
855
+ timestamps.push(p.endTime);
856
+ }
857
+ for (const m of curationData.milestones) timestamps.push(m.timestamp);
858
+ for (const q of curationData.quotes) timestamps.push(q.timestamp);
859
+ if (timestamps.length === 0) {
860
+ throw new Error("processData: no timestamps found in data \u2014 cannot derive time range");
861
+ }
862
+ const dataStart = Math.min(...timestamps);
863
+ const dataEnd = Math.max(...timestamps);
864
+ const padStartMs = options?.padStartMs ?? 0;
865
+ const padEndMs = options?.padEndMs ?? 0;
866
+ const start = dataStart - padStartMs;
867
+ const end = dataEnd + padEndMs;
868
+ const rawDuration = end - start;
869
+ const ONE_HOUR_MS = 36e5;
870
+ const effectiveDuration = rawDuration > 0 ? rawDuration : ONE_HOUR_MS;
871
+ const durationMs = effectiveDuration * (1 + TIMELINE_TAIL_FRACTION);
872
+ return { start, end, durationMs };
873
+ }
874
+ function buildAgents(rawData) {
875
+ return rawData.agents.map((agent) => ({
876
+ id: agent.id,
877
+ role: agent.role,
878
+ joinedAt: agent.joinedAt,
879
+ label: `@${agent.id}`
880
+ }));
881
+ }
882
+ function mergeEvents(rawData, curationData) {
883
+ const events = [];
884
+ for (const agent of rawData.agents) {
885
+ events.push({
886
+ timestamp: agent.joinedAt,
887
+ type: "agent_join",
888
+ agent: agent.id,
889
+ metadata: { role: agent.role }
890
+ });
891
+ }
892
+ for (const commit of rawData.commits) {
893
+ events.push({
894
+ timestamp: commit.timestamp,
895
+ type: "commit",
896
+ agent: commit.agent,
897
+ metadata: {
898
+ sha: commit.sha,
899
+ filesChanged: commit.filesChanged,
900
+ insertions: commit.insertions,
901
+ deletions: commit.deletions,
902
+ prNumber: commit.prNumber,
903
+ commitType: deriveCommitType(commit.message)
904
+ }
905
+ });
906
+ }
907
+ for (const pr of rawData.prs) {
908
+ events.push({
909
+ timestamp: pr.createdAt,
910
+ type: "pr_created",
911
+ agent: pr.agent,
912
+ metadata: {
913
+ prNumber: pr.number,
914
+ title: pr.title,
915
+ additions: pr.additions,
916
+ deletions: pr.deletions
917
+ }
918
+ });
919
+ if (pr.mergedAt) {
920
+ events.push({
921
+ timestamp: pr.mergedAt,
922
+ type: "pr_merged",
923
+ agent: pr.agent,
924
+ metadata: {
925
+ prNumber: pr.number,
926
+ title: pr.title
927
+ }
928
+ });
929
+ }
930
+ }
931
+ for (const msg of rawData.messages) {
932
+ events.push({
933
+ timestamp: msg.timestamp,
934
+ type: "message",
935
+ agent: msg.sender,
936
+ targetAgent: msg.mentions[0],
937
+ // primary target
938
+ metadata: {
939
+ mentions: msg.mentions
940
+ }
941
+ });
942
+ }
943
+ for (const milestone of curationData.milestones) {
944
+ events.push({
945
+ timestamp: milestone.timestamp,
946
+ type: "milestone",
947
+ agent: "",
948
+ // milestones aren't agent-specific
949
+ metadata: {
950
+ label: milestone.label,
951
+ phase: milestone.phase
952
+ }
953
+ });
954
+ }
955
+ for (const artifact of rawData.artifacts) {
956
+ events.push({
957
+ timestamp: artifact.createdAt,
958
+ type: "artifact",
959
+ agent: artifact.agent ?? "",
960
+ metadata: {
961
+ slug: artifact.slug,
962
+ type: artifact.type
963
+ }
964
+ });
965
+ }
966
+ events.sort((a, b) => a.timestamp - b.timestamp);
967
+ return events;
968
+ }
969
+ var PHASE_COLORS = [
970
+ "#3b82f6",
971
+ // blue
972
+ "#ef4444",
973
+ // red
974
+ "#f59e0b",
975
+ // amber
976
+ "#8b5cf6",
977
+ // purple
978
+ "#10b981",
979
+ // emerald
980
+ "#ec4899",
981
+ // pink
982
+ "#f97316",
983
+ // orange
984
+ "#06b6d4"
985
+ // cyan
986
+ ];
987
+ function mapPhases(curationData, timeRange) {
988
+ const phases = curationData.phases;
989
+ if (phases.length === 0) return [];
990
+ return phases.map((p, i) => ({
991
+ id: p.id,
992
+ name: p.name,
993
+ startTime: i === 0 ? Math.min(p.startTime, timeRange.start) : p.startTime,
994
+ endTime: i === phases.length - 1 ? Math.max(p.endTime, timeRange.end) : p.endTime,
995
+ color: PHASE_COLORS[i % PHASE_COLORS.length]
996
+ }));
997
+ }
998
+ function mapQuotes(curationData) {
999
+ return curationData.quotes.map((q) => ({
1000
+ text: q.text,
1001
+ speaker: q.speaker,
1002
+ timestamp: q.timestamp,
1003
+ phase: q.phase,
1004
+ ...q.emoji && { emoji: q.emoji },
1005
+ ...q.mood && { mood: q.mood }
1006
+ }));
1007
+ }
1008
+ function mapMilestones(curationData) {
1009
+ return curationData.milestones.map((m) => ({
1010
+ label: m.label,
1011
+ timestamp: m.timestamp,
1012
+ phase: m.phase
1013
+ }));
1014
+ }
1015
+ function mapTrustKeyframes(curationData) {
1016
+ return curationData.trustKeyframes.map((k) => ({
1017
+ timestamp: k.timestamp,
1018
+ level: k.level,
1019
+ label: k.label
1020
+ }));
1021
+ }
1022
+ var DEFAULT_PHASE_COLOR = "#6b7280";
1023
+ function buildEditorialNarration(durationMs, curationData) {
1024
+ if (durationMs <= 0) return [];
1025
+ if (curationData.editorialNarration && curationData.editorialNarration.length > 0) {
1026
+ const timeRangeStart = deriveTimeRangeStart(curationData);
1027
+ return curationData.editorialNarration.map((entry) => ({
1028
+ normalizedTime: Math.max(0, Math.min(1, (entry.timestamp - timeRangeStart) / durationMs)),
1029
+ phaseTitle: entry.phaseTitle,
1030
+ phaseColor: entry.phaseColor,
1031
+ text: entry.text,
1032
+ type: entry.type
1033
+ })).sort((a, b) => a.normalizedTime - b.normalizedTime);
1034
+ }
1035
+ return buildGenericNarration(durationMs, curationData);
1036
+ }
1037
+ function deriveTimeRangeStart(curationData) {
1038
+ if (curationData.phases.length === 0) return 0;
1039
+ return Math.min(...curationData.phases.map((p) => p.startTime));
1040
+ }
1041
+ function buildGenericNarration(durationMs, curationData) {
1042
+ if (curationData.phases.length === 0) return [];
1043
+ const timeRangeStart = deriveTimeRangeStart(curationData);
1044
+ return curationData.phases.map((phase) => ({
1045
+ normalizedTime: Math.max(0, Math.min(1, (phase.startTime - timeRangeStart) / durationMs)),
1046
+ phaseTitle: phase.name.toUpperCase(),
1047
+ phaseColor: DEFAULT_PHASE_COLOR,
1048
+ text: phase.name,
1049
+ type: "phase"
1050
+ })).sort((a, b) => a.normalizedTime - b.normalizedTime);
1051
+ }
1052
+ function processData(rawData, curationData, options) {
1053
+ const timeRange = deriveTimeRange(rawData, curationData, options);
1054
+ const agents = buildAgents(rawData);
1055
+ const events = mergeEvents(rawData, curationData);
1056
+ const phases = mapPhases(curationData, timeRange);
1057
+ const quotes = mapQuotes(curationData);
1058
+ const milestones = mapMilestones(curationData);
1059
+ const trustKeyframes = mapTrustKeyframes(curationData);
1060
+ const editorialNarration = buildEditorialNarration(timeRange.durationMs, curationData);
1061
+ return {
1062
+ projectTitle: rawData.project.name,
1063
+ agents,
1064
+ events,
1065
+ phases,
1066
+ quotes,
1067
+ milestones,
1068
+ trustKeyframes,
1069
+ editorialNarration,
1070
+ timeRange,
1071
+ stats: {
1072
+ totalCommits: rawData.commits.length,
1073
+ totalPRs: rawData.prs.length,
1074
+ totalMessages: rawData.messages.length,
1075
+ totalAgents: rawData.agents.length
1076
+ }
1077
+ };
1078
+ }
1079
+
1080
+ // src/processing/agent-layout.ts
1081
+ var MARGIN = 0.04;
1082
+ var MIN_Y_GAP = 0.08;
1083
+ var MIN_X_GAP = 0.12;
1084
+ var DEFAULT_BAND_ASPECT = 5;
1085
+ var PHI = (1 + Math.sqrt(5)) / 2;
1086
+ var JITTER_AMP = 3;
1087
+ function computeAgentPositions(agents, messages, optsOrMinYGap) {
1088
+ const opts = typeof optsOrMinYGap === "number" ? { minYGapNorm: optsOrMinYGap } : optsOrMinYGap ?? {};
1089
+ if (agents.length === 0) return /* @__PURE__ */ new Map();
1090
+ if (agents.length === 1) {
1091
+ return /* @__PURE__ */ new Map([[agents[0].id, { x: 0.5, y: 0.5 }]]);
1092
+ }
1093
+ if (agents.length === 2) {
1094
+ const [a, b] = agents;
1095
+ const humanFirst = a.role === "human" ? [a, b] : b.role === "human" ? [b, a] : [a, b];
1096
+ return /* @__PURE__ */ new Map([
1097
+ [humanFirst[0].id, { x: 0.35, y: 0.45 }],
1098
+ [humanFirst[1].id, { x: 0.65, y: 0.55 }]
1099
+ ]);
1100
+ }
1101
+ const bandAspect = opts.bandAspect ?? DEFAULT_BAND_ASPECT;
1102
+ const minYGap = Math.max(MIN_Y_GAP, opts.minYGapNorm ?? MIN_Y_GAP);
1103
+ const minXGap = opts.minXGapNorm ?? Math.max(MIN_X_GAP, minYGap / bandAspect);
1104
+ const adjacency = buildAdjacency(agents, messages);
1105
+ const ordered = orderAgents(agents, adjacency);
1106
+ const { cols, rows } = computeGridDims(ordered.length, minXGap, minYGap);
1107
+ const positions = assignGridPositions(ordered, cols, rows, minXGap, minYGap);
1108
+ return positions;
1109
+ }
1110
+ function orderAgents(agents, adjacency) {
1111
+ const humans = [];
1112
+ const coordination = [];
1113
+ const support = [];
1114
+ const builders = [];
1115
+ for (const agent of agents) {
1116
+ switch (agent.role) {
1117
+ case "human":
1118
+ humans.push(agent);
1119
+ break;
1120
+ case "lead":
1121
+ case "pm":
1122
+ case "reviewer":
1123
+ coordination.push(agent);
1124
+ break;
1125
+ case "ideation":
1126
+ case "researcher":
1127
+ case "tester":
1128
+ case "auditor":
1129
+ support.push(agent);
1130
+ break;
1131
+ default:
1132
+ builders.push(agent);
1133
+ break;
1134
+ }
1135
+ }
1136
+ const orderedCoord = orderByCliques(coordination, adjacency);
1137
+ const orderedSupport = orderByCliques(support, adjacency);
1138
+ const orderedBuilders = orderByCliques(builders, adjacency);
1139
+ return [...humans, ...orderedCoord, ...orderedSupport, ...orderedBuilders];
1140
+ }
1141
+ function orderByCliques(agents, adjacency) {
1142
+ if (agents.length === 0) return [];
1143
+ const cliques = findCliques(agents, adjacency);
1144
+ return cliques.flat();
1145
+ }
1146
+ function computeGridDims(n, minXGap, minYGap) {
1147
+ const usable = 1 - 2 * MARGIN;
1148
+ const maxCols = Math.max(1, Math.floor(usable / minXGap) + 1);
1149
+ const maxRows = Math.max(1, Math.floor(usable / minYGap) + 1);
1150
+ let bestCols = Math.min(maxCols, n);
1151
+ let bestRows = 1;
1152
+ let bestScore = Number.NEGATIVE_INFINITY;
1153
+ for (let r = 1; r <= Math.min(maxRows, n); r++) {
1154
+ const c = Math.ceil(n / r);
1155
+ if (c > maxCols) continue;
1156
+ if (c * r < n) continue;
1157
+ const waste = c * r - n;
1158
+ const oddRowBonus = r % 2 === 1 ? 5 : 0;
1159
+ const ratio = c / r;
1160
+ const balanceScore = -Math.abs(Math.log(ratio)) * 5;
1161
+ const score = oddRowBonus + balanceScore - waste;
1162
+ if (score > bestScore) {
1163
+ bestScore = score;
1164
+ bestCols = c;
1165
+ bestRows = r;
1166
+ }
1167
+ }
1168
+ return { cols: bestCols, rows: bestRows };
1169
+ }
1170
+ function assignGridPositions(agents, cols, rows, minXGap, minYGap) {
1171
+ const positions = /* @__PURE__ */ new Map();
1172
+ const usable = 1 - 2 * MARGIN;
1173
+ const slots = spiralOrder(cols, rows);
1174
+ const COMPRESS_PAD = 1.4;
1175
+ const effectiveCols = cols > 1 && rows > 1 ? cols - 0.5 : Math.max(1, cols - 1);
1176
+ const fullSpreadX = cols > 1 ? usable / effectiveCols : 0;
1177
+ const fullSpreadY = rows > 1 ? usable / (rows - 1) : 0;
1178
+ const xStep = cols > 1 ? Math.min(fullSpreadX, minXGap * COMPRESS_PAD) : 0;
1179
+ const yStep = rows > 1 ? Math.min(fullSpreadY, minYGap * COMPRESS_PAD) : 0;
1180
+ const useHex = cols > 1 && rows > 1;
1181
+ const staggerX = useHex ? xStep * 0.5 : 0;
1182
+ const gridWidth = cols > 1 ? (cols - 1) * xStep + staggerX : 0;
1183
+ const gridHeight = rows > 1 ? (rows - 1) * yStep : 0;
1184
+ const xOrigin = Math.max(MARGIN, 0.5 - gridWidth / 2);
1185
+ const yOrigin = Math.max(MARGIN, 0.5 - gridHeight / 2);
1186
+ const maxJitterX = cols > 1 ? Math.max(0, (xStep - minXGap) * 0.6) : 0;
1187
+ const maxJitterY = rows > 1 ? Math.max(0, (yStep - minYGap) * 0.6) : 0;
1188
+ for (let i = 0; i < agents.length && i < slots.length; i++) {
1189
+ const { col, row } = slots[i];
1190
+ let x = cols > 1 ? xOrigin + col * xStep : 0.5;
1191
+ let y = rows > 1 ? yOrigin + row * yStep : 0.5;
1192
+ if (row % 2 === 0 && cols > 1) {
1193
+ x += staggerX;
1194
+ }
1195
+ const EDGE_ZONE = 0.15;
1196
+ const edgeDampX = Math.min((x - MARGIN) / EDGE_ZONE, (1 - MARGIN - x) / EDGE_ZONE, 1);
1197
+ const edgeDampY = Math.min((y - MARGIN) / EDGE_ZONE, (1 - MARGIN - y) / EDGE_ZONE, 1);
1198
+ const dampX = Math.max(0, edgeDampX);
1199
+ const dampY = Math.max(0, edgeDampY);
1200
+ const jitterX = (i * PHI % 1 - 0.5) * 2 * maxJitterX * JITTER_AMP * dampX;
1201
+ const jitterY = (i * PHI * PHI % 1 - 0.5) * 2 * maxJitterY * JITTER_AMP * dampY;
1202
+ x += jitterX;
1203
+ y += jitterY;
1204
+ x = Math.max(MARGIN, Math.min(1 - MARGIN, x));
1205
+ y = Math.max(MARGIN, Math.min(1 - MARGIN, y));
1206
+ positions.set(agents[i].id, { x, y });
1207
+ }
1208
+ return positions;
1209
+ }
1210
+ function spiralOrder(cols, rows) {
1211
+ const centerCol = (cols - 1) / 2;
1212
+ const centerRow = (rows - 1) / 2;
1213
+ const cells = [];
1214
+ for (let r = 0; r < rows; r++) {
1215
+ for (let c = 0; c < cols; c++) {
1216
+ const dx = c - centerCol;
1217
+ const dy = r - centerRow;
1218
+ const dist = Math.sqrt(dx * dx + dy * dy);
1219
+ const angle = Math.atan2(dy, dx);
1220
+ cells.push({ col: c, row: r, dist, angle });
1221
+ }
1222
+ }
1223
+ cells.sort((a, b) => {
1224
+ if (Math.abs(a.dist - b.dist) > 0.01) return a.dist - b.dist;
1225
+ return a.angle - b.angle;
1226
+ });
1227
+ return cells;
1228
+ }
1229
+ function buildAdjacency(agents, messages) {
1230
+ const agentIds = new Set(agents.map((a) => a.id));
1231
+ const adj = /* @__PURE__ */ new Map();
1232
+ for (const agent of agents) {
1233
+ adj.set(agent.id, /* @__PURE__ */ new Map());
1234
+ }
1235
+ for (const msg of messages) {
1236
+ if (!agentIds.has(msg.sender)) continue;
1237
+ for (const mention of msg.mentions) {
1238
+ if (!agentIds.has(mention)) continue;
1239
+ if (mention === msg.sender) continue;
1240
+ const senderMap = adj.get(msg.sender);
1241
+ senderMap.set(mention, (senderMap.get(mention) ?? 0) + 1);
1242
+ const mentionMap = adj.get(mention);
1243
+ mentionMap.set(msg.sender, (mentionMap.get(msg.sender) ?? 0) + 1);
1244
+ }
1245
+ }
1246
+ return adj;
1247
+ }
1248
+ function findCliques(agents, adjacency) {
1249
+ const agentSet = new Set(agents.map((a) => a.id));
1250
+ const visited = /* @__PURE__ */ new Set();
1251
+ const cliques = [];
1252
+ const agentMap = new Map(agents.map((a) => [a.id, a]));
1253
+ const sorted = [...agents].sort((a, b) => a.joinedAt - b.joinedAt);
1254
+ for (const agent of sorted) {
1255
+ if (visited.has(agent.id)) continue;
1256
+ const clique = [];
1257
+ const queue = [agent.id];
1258
+ visited.add(agent.id);
1259
+ while (queue.length > 0) {
1260
+ const current = queue.shift();
1261
+ clique.push(agentMap.get(current));
1262
+ const neighbors = adjacency.get(current);
1263
+ if (!neighbors) continue;
1264
+ for (const [neighborId] of neighbors) {
1265
+ if (!agentSet.has(neighborId)) continue;
1266
+ if (visited.has(neighborId)) continue;
1267
+ visited.add(neighborId);
1268
+ queue.push(neighborId);
1269
+ }
1270
+ }
1271
+ cliques.push(clique);
1272
+ }
1273
+ cliques.sort((a, b) => {
1274
+ if (b.length !== a.length) return b.length - a.length;
1275
+ return Math.min(...a.map((x) => x.joinedAt)) - Math.min(...b.map((x) => x.joinedAt));
1276
+ });
1277
+ for (const clique of cliques) {
1278
+ clique.sort((a, b) => {
1279
+ const wA = totalWeight(a.id, adjacency);
1280
+ const wB = totalWeight(b.id, adjacency);
1281
+ if (wB !== wA) return wB - wA;
1282
+ return a.joinedAt - b.joinedAt;
1283
+ });
1284
+ }
1285
+ return cliques;
1286
+ }
1287
+ function totalWeight(agentId, adjacency) {
1288
+ const neighbors = adjacency.get(agentId);
1289
+ if (!neighbors) return 0;
1290
+ let total = 0;
1291
+ for (const [, weight] of neighbors) {
1292
+ total += weight;
1293
+ }
1294
+ return total;
1295
+ }
1296
+ function computeLayoutPositions(raw, optsOrMinYGap) {
1297
+ const agents = raw.agents.map((a) => ({
1298
+ id: a.id,
1299
+ role: a.role,
1300
+ joinedAt: a.joinedAt
1301
+ }));
1302
+ const messages = raw.events.filter((e) => e.type === "message" && e.targetAgent).map((e) => ({
1303
+ sender: e.agent,
1304
+ mentions: [e.targetAgent]
1305
+ }));
1306
+ return computeAgentPositions(agents, messages, optsOrMinYGap);
1307
+ }
1308
+
1309
+ // src/processing/enrich.ts
1310
+ var HUMAN_COLOR = "#ff922b";
1311
+ var AGENT_PALETTE = [
1312
+ "#ff6b6b",
1313
+ // coral red
1314
+ "#ff4da6",
1315
+ // hot pink
1316
+ "#ffd93d",
1317
+ // gold
1318
+ "#4d96ff",
1319
+ // sky blue
1320
+ "#ff8c42",
1321
+ // tangerine
1322
+ "#00e5cc",
1323
+ // cyan
1324
+ "#7c5cfc",
1325
+ // purple
1326
+ "#22d3ee",
1327
+ // light cyan
1328
+ "#10b981",
1329
+ // emerald
1330
+ "#a78bfa",
1331
+ // lavender
1332
+ "#6bcb77",
1333
+ // green
1334
+ "#c084fc",
1335
+ // violet
1336
+ "#f472b6"
1337
+ // pink
1338
+ ];
1339
+ function getAgentColor(_id, role, index) {
1340
+ if (role === "human") return HUMAN_COLOR;
1341
+ return AGENT_PALETTE[index % AGENT_PALETTE.length];
1342
+ }
1343
+ function getAgentRadius(role) {
1344
+ return role === "human" ? 0.04 : 0.03;
1345
+ }
1346
+ function normalize(timestamp, start, durationMs) {
1347
+ return (timestamp - start) / durationMs;
1348
+ }
1349
+ function enrichVizData(raw, layoutOpts) {
1350
+ const { start, end } = raw.timeRange;
1351
+ const rawDuration = end - start;
1352
+ const expectedDuration = rawDuration * (1 + TIMELINE_TAIL_FRACTION);
1353
+ const durationMs = raw.timeRange.durationMs < expectedDuration * 0.99 ? expectedDuration : raw.timeRange.durationMs;
1354
+ const positions = computeLayoutPositions(raw, layoutOpts);
1355
+ const agents = raw.agents.map((a, i) => ({
1356
+ id: a.id,
1357
+ role: a.role,
1358
+ color: getAgentColor(a.id, a.role, i),
1359
+ position: positions.get(a.id) ?? { x: 0.5, y: 0.5 },
1360
+ radius: getAgentRadius(a.role),
1361
+ label: a.label
1362
+ }));
1363
+ const events = raw.events.map((e) => ({
1364
+ ...e,
1365
+ normalizedTime: normalize(e.timestamp, start, durationMs)
1366
+ }));
1367
+ const phases = raw.phases.map((p) => ({
1368
+ id: p.id,
1369
+ name: p.name,
1370
+ startNormalized: normalize(p.startTime, start, durationMs),
1371
+ endNormalized: normalize(p.endTime, start, durationMs),
1372
+ color: p.color
1373
+ }));
1374
+ const quotes = raw.quotes.map((q) => ({
1375
+ text: q.text,
1376
+ speaker: q.speaker,
1377
+ normalizedTime: normalize(q.timestamp, start, durationMs),
1378
+ phase: q.phase,
1379
+ ...q.emoji && { emoji: q.emoji },
1380
+ ...q.mood && { mood: q.mood }
1381
+ }));
1382
+ const milestones = raw.milestones.map((m) => ({
1383
+ label: m.label,
1384
+ normalizedTime: normalize(m.timestamp, start, durationMs),
1385
+ phase: m.phase
1386
+ }));
1387
+ const trustKeyframes = raw.trustKeyframes.map((k) => ({
1388
+ normalizedTime: normalize(k.timestamp, start, durationMs),
1389
+ level: k.level,
1390
+ label: k.label
1391
+ }));
1392
+ const durationChanged = durationMs !== raw.timeRange.durationMs;
1393
+ const editorialNarration = durationChanged && raw.editorialNarration ? raw.editorialNarration.map((entry) => ({
1394
+ ...entry,
1395
+ normalizedTime: entry.normalizedTime * (raw.timeRange.durationMs / durationMs)
1396
+ })) : raw.editorialNarration ?? [];
1397
+ return {
1398
+ projectTitle: raw.projectTitle ?? "miriad-viz",
1399
+ agents,
1400
+ events,
1401
+ phases,
1402
+ quotes,
1403
+ milestones,
1404
+ trustKeyframes,
1405
+ editorialNarration,
1406
+ timeRange: { start, end, durationMs },
1407
+ stats: raw.stats
1408
+ };
1409
+ }
1410
+
1411
+ // src/transform/transform-raw.ts
1412
+ function inferRole(roleString) {
1413
+ const s = roleString.toLowerCase();
1414
+ if (s.includes("human") || s === "user") return "human";
1415
+ if (s.includes("lead")) return "lead";
1416
+ if (s.includes("pm")) return "pm";
1417
+ if (s.includes("tester") || s.includes("qa")) return "tester";
1418
+ if (s.includes("reviewer") || s.includes("review")) return "reviewer";
1419
+ if (s.includes("auditor") || s.includes("audit")) return "auditor";
1420
+ if (s.includes("challenger") || s.includes("challenge")) return "challenger";
1421
+ if (s.includes("scout")) return "scout";
1422
+ if (s.includes("ideation")) return "ideation";
1423
+ if (s.includes("research")) return "researcher";
1424
+ if (s.includes("builder") || s.includes("build")) return "builder";
1425
+ return "builder";
1426
+ }
1427
+ function transformToRawData(sources) {
1428
+ const defaultAgent = sources.retroPageData.teamAssembly[0]?.agent.replace("@", "") ?? "unknown";
1429
+ return {
1430
+ project: {
1431
+ name: sources.retroPageData.meta.title,
1432
+ description: sources.retroPageData.meta.subtitle
1433
+ },
1434
+ agents: transformAgents(
1435
+ sources.retroPageData,
1436
+ sources.chatActivity.agentTotals,
1437
+ sources.timelineEvents
1438
+ ),
1439
+ commits: transformCommits(sources.commits, sources.prAgentMap, defaultAgent),
1440
+ prs: transformPRs(sources.prs, sources.prAgentMap),
1441
+ messages: transformMessages(sources.timelineEvents),
1442
+ artifacts: sources.artifactData ? transformArtifacts(
1443
+ sources.artifactData,
1444
+ sources.retroPageData,
1445
+ sources.prs,
1446
+ sources.prAgentMap
1447
+ ) : []
1448
+ };
1449
+ }
1450
+ function buildFirstMessageTimeMap(timelineEvents) {
1451
+ const map = /* @__PURE__ */ new Map();
1452
+ if (!timelineEvents) return map;
1453
+ for (const evt of timelineEvents.events) {
1454
+ if (evt.type !== "message" && evt.type !== "beam") continue;
1455
+ const sender = evt.from;
1456
+ const ts = new Date(evt.t).getTime();
1457
+ const existing = map.get(sender);
1458
+ if (existing === void 0 || ts < existing) {
1459
+ map.set(sender, ts);
1460
+ }
1461
+ }
1462
+ return map;
1463
+ }
1464
+ function transformAgents(retroData, agentTotals, timelineEvents, agentRoles) {
1465
+ const agents = [];
1466
+ const seen = /* @__PURE__ */ new Set();
1467
+ const firstMessageTime = buildFirstMessageTimeMap(timelineEvents);
1468
+ const assemblyRoles = /* @__PURE__ */ new Map();
1469
+ for (const entry of retroData.teamAssembly) {
1470
+ const id = entry.agent.replace("@", "");
1471
+ assemblyRoles.set(id, entry.role);
1472
+ }
1473
+ function resolveRole(id) {
1474
+ const assemblyRole = assemblyRoles.get(id);
1475
+ if (assemblyRole) {
1476
+ return inferRole(assemblyRole);
1477
+ }
1478
+ return "builder";
1479
+ }
1480
+ function resolveJoinedAt(id, assemblyTime) {
1481
+ const msgTime = firstMessageTime.get(id);
1482
+ if (msgTime !== void 0 && assemblyTime !== void 0) {
1483
+ return Math.min(msgTime, assemblyTime);
1484
+ }
1485
+ if (msgTime !== void 0) return msgTime;
1486
+ if (assemblyTime !== void 0) return assemblyTime;
1487
+ return new Date(retroData.meta.startDate).getTime();
1488
+ }
1489
+ for (const entry of retroData.teamAssembly) {
1490
+ const id = entry.agent.replace("@", "");
1491
+ if (seen.has(id)) continue;
1492
+ seen.add(id);
1493
+ agents.push({
1494
+ id,
1495
+ role: resolveRole(id),
1496
+ joinedAt: resolveJoinedAt(id, new Date(entry.time).getTime())
1497
+ });
1498
+ }
1499
+ for (const id of Object.keys(agentTotals)) {
1500
+ if (seen.has(id)) continue;
1501
+ agents.push({
1502
+ id,
1503
+ role: resolveRole(id),
1504
+ joinedAt: resolveJoinedAt(id)
1505
+ });
1506
+ seen.add(id);
1507
+ }
1508
+ return agents;
1509
+ }
1510
+ function transformCommits(commits, prAgentMap, defaultAgent = "unknown") {
1511
+ return commits.map((c) => {
1512
+ const agent = resolveCommitAgent(c, prAgentMap, defaultAgent);
1513
+ return {
1514
+ sha: c.hash,
1515
+ timestamp: new Date(c.date).getTime(),
1516
+ agent,
1517
+ message: c.subject,
1518
+ filesChanged: c.files.length,
1519
+ insertions: c.totalInsertions,
1520
+ deletions: c.totalDeletions,
1521
+ prNumber: c.prNumber ?? void 0
1522
+ };
1523
+ });
1524
+ }
1525
+ function resolveCommitAgent(commit, prAgentMap, defaultAgent = "unknown") {
1526
+ if (commit.prNumber != null) {
1527
+ const agent = prAgentMap[String(commit.prNumber)];
1528
+ if (agent) return agent;
1529
+ }
1530
+ const mergeMatch = commit.subject.match(/^Merge pull request #(\d+)/);
1531
+ if (mergeMatch) {
1532
+ const agent = prAgentMap[mergeMatch[1]];
1533
+ if (agent) return agent;
1534
+ }
1535
+ const suffixMatch = commit.subject.match(/\(#(\d+)\)\s*$/);
1536
+ if (suffixMatch) {
1537
+ const agent = prAgentMap[suffixMatch[1]];
1538
+ if (agent) return agent;
1539
+ }
1540
+ return defaultAgent;
1541
+ }
1542
+ function transformPRs(prs, prAgentMap) {
1543
+ return prs.map((pr) => {
1544
+ const agent = prAgentMap[String(pr.number)] ?? "unknown";
1545
+ return {
1546
+ number: pr.number,
1547
+ title: pr.title,
1548
+ agent,
1549
+ createdAt: new Date(pr.createdAt).getTime(),
1550
+ mergedAt: pr.mergedAt ? new Date(pr.mergedAt).getTime() : null,
1551
+ additions: pr.additions,
1552
+ deletions: pr.deletions,
1553
+ branch: pr.headRefName
1554
+ };
1555
+ });
1556
+ }
1557
+ function inferArtifactType(slug) {
1558
+ if (slug.includes(".app.") || slug.endsWith(".ts") || slug.endsWith(".js")) return "code";
1559
+ if (slug.includes("screenshot") || slug.includes("qa-")) return "asset";
1560
+ if (slug.includes("decision")) return "decision";
1561
+ if (slug.includes("task") || slug.includes("phase-")) return "task";
1562
+ if (slug.startsWith("spec-") || slug.startsWith("spec")) return "doc";
1563
+ if (slug.includes("concept")) return "doc";
1564
+ return "doc";
1565
+ }
1566
+ function resolveArtifactAgent(slug, prAgentMap, createdBy, defaultAgent) {
1567
+ if (createdBy) return createdBy;
1568
+ const prMatch = slug.match(/#?(\d+)/);
1569
+ if (prMatch) {
1570
+ const agent = prAgentMap[prMatch[1]];
1571
+ if (agent) return agent;
1572
+ }
1573
+ return defaultAgent;
1574
+ }
1575
+ var CATEGORY_TYPE_MAP = {
1576
+ qaScreenshots: "asset",
1577
+ transitionSpecs: "doc",
1578
+ transitionConcepts: "doc",
1579
+ scenes: "doc",
1580
+ vizConcepts: "code",
1581
+ teamPrinciples: "doc",
1582
+ retroData: "doc",
1583
+ decisions: "decision"
1584
+ };
1585
+ function distributeTimestamps(count, bounds, prTimestamps) {
1586
+ const phasePRTimes = prTimestamps.filter((t) => t >= bounds.start && t <= bounds.end);
1587
+ const timestamps = [];
1588
+ for (let i = 0; i < count; i++) {
1589
+ let timestamp;
1590
+ if (phasePRTimes.length > 0) {
1591
+ const prIdx = i % phasePRTimes.length;
1592
+ timestamp = phasePRTimes[prIdx] + i * 6e4;
1593
+ } else {
1594
+ const fraction = count > 1 ? i / (count - 1) : 0.5;
1595
+ timestamp = bounds.start + fraction * (bounds.end - bounds.start);
1596
+ }
1597
+ timestamps.push(Math.round(Math.max(bounds.start, Math.min(bounds.end, timestamp))));
1598
+ }
1599
+ return timestamps;
1600
+ }
1601
+ function buildCategoryDistribution(categories) {
1602
+ const entries = [];
1603
+ let total = 0;
1604
+ for (const [categoryName, categoryData] of Object.entries(categories)) {
1605
+ if (categoryData.count === 0) continue;
1606
+ entries.push({
1607
+ type: CATEGORY_TYPE_MAP[categoryName] ?? "doc",
1608
+ count: categoryData.count
1609
+ });
1610
+ total += categoryData.count;
1611
+ }
1612
+ if (total === 0) return [{ type: "doc", weight: 1 }];
1613
+ return entries.sort((a, b) => b.count - a.count).map((e) => ({ type: e.type, weight: e.count / total }));
1614
+ }
1615
+ function buildCreatedByLookup(categories) {
1616
+ const lookup = /* @__PURE__ */ new Map();
1617
+ for (const categoryData of Object.values(categories)) {
1618
+ for (const item of categoryData.items ?? []) {
1619
+ if (typeof item === "object" && item.createdBy) {
1620
+ lookup.set(item.slug, item.createdBy);
1621
+ }
1622
+ }
1623
+ }
1624
+ return lookup;
1625
+ }
1626
+ function transformArtifacts(artifactData, retroData, prs, prAgentMap) {
1627
+ const artifacts = [];
1628
+ const agentIds = retroData.teamAssembly.map((e) => e.agent.replace("@", ""));
1629
+ const defaultAgent = agentIds[0] ?? "unknown";
1630
+ const phaseBoundaries = retroData.phases.map((p) => ({
1631
+ start: new Date(p.start).getTime(),
1632
+ end: new Date(p.end).getTime()
1633
+ }));
1634
+ const prTimestamps = prs.map((pr) => new Date(pr.createdAt).getTime()).sort((a, b) => a - b);
1635
+ const distribution = buildCategoryDistribution(artifactData.categories);
1636
+ const createdByLookup = buildCreatedByLookup(artifactData.categories);
1637
+ let phaseIndex = 0;
1638
+ let globalBulkIndex = 0;
1639
+ for (const [_phaseName, phaseData] of Object.entries(artifactData.phases)) {
1640
+ if (phaseIndex >= phaseBoundaries.length) break;
1641
+ const bounds = phaseBoundaries[phaseIndex];
1642
+ phaseIndex++;
1643
+ const totalCount = phaseData.count;
1644
+ const slugs = phaseData.artifacts;
1645
+ const bulkCount = Math.max(0, totalCount - slugs.length);
1646
+ const timestamps = distributeTimestamps(totalCount, bounds, prTimestamps);
1647
+ let tsIdx = 0;
1648
+ for (const slug of slugs) {
1649
+ artifacts.push({
1650
+ slug,
1651
+ type: inferArtifactType(slug),
1652
+ createdAt: timestamps[tsIdx],
1653
+ updatedAt: timestamps[tsIdx],
1654
+ agent: resolveArtifactAgent(slug, prAgentMap, createdByLookup.get(slug), defaultAgent)
1655
+ });
1656
+ tsIdx++;
1657
+ }
1658
+ let distIdx = 0;
1659
+ let distBudget = Math.round(distribution[0].weight * bulkCount);
1660
+ for (let i = 0; i < bulkCount; i++) {
1661
+ while (distBudget <= 0 && distIdx < distribution.length - 1) {
1662
+ distIdx++;
1663
+ distBudget = Math.round(distribution[distIdx].weight * bulkCount);
1664
+ }
1665
+ distBudget--;
1666
+ const entry = distribution[distIdx];
1667
+ const agent = agentIds.length > 0 ? agentIds[globalBulkIndex % agentIds.length] : defaultAgent;
1668
+ artifacts.push({
1669
+ slug: `phase${phaseIndex}-${entry.type}-${String(globalBulkIndex + 1).padStart(3, "0")}`,
1670
+ type: entry.type,
1671
+ createdAt: timestamps[tsIdx],
1672
+ updatedAt: timestamps[tsIdx],
1673
+ agent
1674
+ });
1675
+ tsIdx++;
1676
+ globalBulkIndex++;
1677
+ }
1678
+ }
1679
+ artifacts.sort((a, b) => a.createdAt - b.createdAt);
1680
+ return artifacts;
1681
+ }
1682
+ function transformMessages(timelineEvents) {
1683
+ const messageEvents = timelineEvents.events.filter(
1684
+ (e) => e.type === "message" || e.type === "beam"
1685
+ );
1686
+ return messageEvents.map((evt, i) => ({
1687
+ id: `msg-${String(i).padStart(4, "0")}`,
1688
+ timestamp: new Date(evt.t).getTime(),
1689
+ sender: evt.from,
1690
+ mentions: [evt.to]
1691
+ }));
1692
+ }
1693
+
1694
+ // src/transform/transform-curation.ts
1695
+ function transformToCurationData(sources) {
1696
+ const narration = transformNarration(sources.retroPageData);
1697
+ return {
1698
+ phases: transformPhases(sources.retroPageData),
1699
+ quotes: transformQuotes(sources.retroPageData, sources.retroPageQuotes),
1700
+ milestones: transformMilestones(sources.retroPageData),
1701
+ trustKeyframes: transformTrustKeyframes(sources.retroPageData),
1702
+ ...narration.length > 0 ? { editorialNarration: narration } : {}
1703
+ };
1704
+ }
1705
+ function transformPhases(retroData) {
1706
+ return retroData.phases.map((p) => ({
1707
+ id: p.id,
1708
+ name: p.name,
1709
+ startTime: new Date(p.start).getTime(),
1710
+ endTime: new Date(p.end).getTime()
1711
+ }));
1712
+ }
1713
+ function transformMilestones(retroData) {
1714
+ const phases = retroData.phases;
1715
+ return retroData.milestones.map((m) => {
1716
+ const milestoneTime = new Date(m.time).getTime();
1717
+ const phase = findPhaseForTimestamp(milestoneTime, phases);
1718
+ return {
1719
+ label: m.label,
1720
+ timestamp: milestoneTime,
1721
+ phase: phase?.id ?? "unknown"
1722
+ };
1723
+ });
1724
+ }
1725
+ var CATEGORY_MOOD = {
1726
+ theGood: "good",
1727
+ theBad: "bad",
1728
+ theUgly: "angry",
1729
+ theFunny: "good"
1730
+ };
1731
+ var ADDITION_MOOD = {
1732
+ good: "good",
1733
+ bad: "bad",
1734
+ ugly: "angry"
1735
+ };
1736
+ function transformQuotes(retroData, retroQuotes) {
1737
+ const phases = retroData.phases;
1738
+ const quotes = [];
1739
+ const firstPhaseStart = phases[0]?.start;
1740
+ const referenceYear = firstPhaseStart ? new Date(firstPhaseStart).getUTCFullYear() : (/* @__PURE__ */ new Date()).getUTCFullYear();
1741
+ for (const [category, entries] of Object.entries(retroData.quotes)) {
1742
+ if (!Array.isArray(entries)) continue;
1743
+ const mood = CATEGORY_MOOD[category] ?? "good";
1744
+ for (const entry of entries) {
1745
+ const timestamp = parseInformalTimestamp(entry.time, referenceYear);
1746
+ if (timestamp == null) continue;
1747
+ const phase = findPhaseForTimestamp(timestamp, phases);
1748
+ quotes.push({
1749
+ text: entry.text,
1750
+ speaker: entry.author.replace(/^@/, ""),
1751
+ timestamp,
1752
+ phase: phase?.id ?? "unknown",
1753
+ mood
1754
+ });
1755
+ }
1756
+ }
1757
+ for (const [category, entries] of Object.entries(retroQuotes)) {
1758
+ if (!Array.isArray(entries)) continue;
1759
+ const mood = ADDITION_MOOD[category] ?? "good";
1760
+ for (const entry of entries) {
1761
+ const timestamp = parseInformalTimestamp(entry.timestamp, referenceYear);
1762
+ if (timestamp == null) continue;
1763
+ const phase = findPhaseForTimestamp(timestamp, phases);
1764
+ quotes.push({
1765
+ text: entry.text,
1766
+ speaker: entry.author.replace(/^@/, ""),
1767
+ timestamp,
1768
+ phase: phase?.id ?? "unknown",
1769
+ mood
1770
+ });
1771
+ }
1772
+ }
1773
+ quotes.sort((a, b) => a.timestamp - b.timestamp);
1774
+ return quotes;
1775
+ }
1776
+ function transformTrustKeyframes(retroData) {
1777
+ if (!retroData.trustArc || retroData.trustArc.length === 0) return [];
1778
+ return retroData.trustArc.map((entry) => ({
1779
+ timestamp: new Date(entry.time).getTime(),
1780
+ level: entry.level / 100,
1781
+ // normalize 0-100 → 0-1
1782
+ label: entry.label
1783
+ })).sort((a, b) => a.timestamp - b.timestamp);
1784
+ }
1785
+ var MONTH_ABBREVS = {
1786
+ jan: 0,
1787
+ feb: 1,
1788
+ mar: 2,
1789
+ apr: 3,
1790
+ may: 4,
1791
+ jun: 5,
1792
+ jul: 6,
1793
+ aug: 7,
1794
+ sep: 8,
1795
+ oct: 9,
1796
+ nov: 10,
1797
+ dec: 11
1798
+ };
1799
+ function transformNarration(retroData) {
1800
+ if (!retroData.editorialNarration || retroData.editorialNarration.length === 0) return [];
1801
+ return retroData.editorialNarration.map((entry) => ({
1802
+ timestamp: new Date(entry.time).getTime(),
1803
+ phaseTitle: entry.phaseTitle,
1804
+ phaseColor: entry.phaseColor,
1805
+ text: entry.text,
1806
+ type: entry.type
1807
+ })).sort((a, b) => a.timestamp - b.timestamp);
1808
+ }
1809
+ function parseInformalTimestamp(timeStr, referenceYear) {
1810
+ if (!timeStr) return null;
1811
+ const isoDate = new Date(timeStr);
1812
+ if (!Number.isNaN(isoDate.getTime()) && timeStr.includes("-")) {
1813
+ return isoDate.getTime();
1814
+ }
1815
+ const cleaned = timeStr.replace(/~/g, "").replace(/\(.*?\)/g, "").trim();
1816
+ const match = cleaned.match(/([A-Za-z]{3})\s+(\d{1,2})(?:\s+(\d{1,2}):(\d{2}))?/);
1817
+ if (!match) return null;
1818
+ const monthIdx = MONTH_ABBREVS[match[1].toLowerCase()];
1819
+ if (monthIdx === void 0) return null;
1820
+ const day = Number.parseInt(match[2], 10);
1821
+ const hour = match[3] ? Number.parseInt(match[3], 10) : 12;
1822
+ const minute = match[4] ? Number.parseInt(match[4], 10) : 0;
1823
+ const date = new Date(Date.UTC(referenceYear, monthIdx, day, hour, minute, 0));
1824
+ return date.getTime();
1825
+ }
1826
+ function findPhaseForTimestamp(timestamp, phases) {
1827
+ return phases.find((p) => {
1828
+ const start = new Date(p.start).getTime();
1829
+ const end = new Date(p.end).getTime();
1830
+ return timestamp >= start && timestamp <= end;
1831
+ });
1832
+ }
1833
+
1834
+ // src/renderer/layout.ts
1835
+ var CONTENT_WIDTH = 10;
1836
+ var CONTENT_HEIGHT = 6;
1837
+ var CONTENT_ASPECT = CONTENT_WIDTH / CONTENT_HEIGHT;
1838
+ var DEFAULT_BAND_AGENTS = 0.55;
1839
+ var DEFAULT_BAND_ARTIFACTS = 0.08;
1840
+ var DEFAULT_BAND_PRS = 0.12;
1841
+ var DEFAULT_BAND_COMMITS = 0.12;
1842
+ var DEFAULT_BAND_TIMELINE = 0.07;
1843
+ var GAP_AGENTS_ARTIFACTS = 0.02;
1844
+ var GAP_ARTIFACTS_PRS = 0.02;
1845
+ var GAP_PRS_COMMITS = 0.015;
1846
+ var GAP_COMMITS_TIMELINE = 5e-3;
1847
+ function buildStackElements(worldHeight, textScale, bandProportions, gapScale) {
1848
+ const labelRowH = computeLabelRowHeight(textScale);
1849
+ const phaseBarRowH = worldHeight * 0.03;
1850
+ const labelTextScaleCapped = Math.min(textScale, 1.5);
1851
+ const playheadLabelZoneH = 0.06 + // playheadLabelOffsetY
1852
+ 0.4 * labelTextScaleCapped * (48 / 128) + // playheadLabelHeight
1853
+ 0.02;
1854
+ const gap1 = worldHeight * GAP_AGENTS_ARTIFACTS * gapScale;
1855
+ const gap2 = worldHeight * GAP_ARTIFACTS_PRS * gapScale;
1856
+ const gap3 = worldHeight * GAP_PRS_COMMITS * gapScale;
1857
+ const gap4 = worldHeight * GAP_COMMITS_TIMELINE * gapScale;
1858
+ const fixedTotal = 4 * labelRowH + playheadLabelZoneH + phaseBarRowH + gap1 + gap2 + gap3 + gap4;
1859
+ const bandBudget = worldHeight - fixedTotal;
1860
+ const bp = bandProportions;
1861
+ const rawTotal = bp.agents + bp.artifacts + bp.prs + bp.commits + bp.timeline;
1862
+ const scale = bandBudget / rawTotal;
1863
+ return [
1864
+ { id: "label-agents", height: labelRowH },
1865
+ { id: "band-agents", height: bp.agents * scale },
1866
+ { id: "gap-1", height: gap1 },
1867
+ { id: "label-artifacts", height: labelRowH },
1868
+ { id: "band-artifacts", height: bp.artifacts * scale },
1869
+ { id: "gap-2", height: gap2 },
1870
+ { id: "label-prs", height: labelRowH },
1871
+ { id: "band-prs", height: bp.prs * scale },
1872
+ { id: "gap-3", height: gap3 },
1873
+ { id: "label-commits", height: labelRowH },
1874
+ { id: "band-commits", height: bp.commits * scale },
1875
+ { id: "playhead-zone", height: playheadLabelZoneH },
1876
+ { id: "gap-4", height: gap4 },
1877
+ { id: "phase-bar", height: phaseBarRowH },
1878
+ { id: "band-timeline", height: bp.timeline * scale }
1879
+ ];
1880
+ }
1881
+ function computeStackPositions(elements, worldTop) {
1882
+ const positions = /* @__PURE__ */ new Map();
1883
+ let y = worldTop;
1884
+ for (const el of elements) {
1885
+ const top = y;
1886
+ y -= el.height;
1887
+ positions.set(el.id, { id: el.id, top, bottom: y, height: el.height });
1888
+ }
1889
+ return positions;
1890
+ }
1891
+ function getPos(positions, id) {
1892
+ const pos = positions.get(id);
1893
+ if (!pos) throw new Error(`Stack element '${id}' not found`);
1894
+ return pos;
1895
+ }
1896
+ var MIN_AGENTS = 0.35;
1897
+ var MIN_ARTIFACTS = 0.06;
1898
+ var MIN_PRS = 0.06;
1899
+ var MIN_COMMITS = 0.06;
1900
+ var MIN_TIMELINE = 0.05;
1901
+ var BASE_BAND_PADDING = {
1902
+ /** Offset from band top for count labels (e.g., "456 commits").
1903
+ * Provides breathing room between the band label row above and the count text.
1904
+ * At textScale=1.0 (desktop): 0.08 * sqrt(1) = 0.08 → ~10px margin.
1905
+ * At textScale=2.3 (mobile): 0.08 * sqrt(2.3) ≈ 0.12 → ~5px margin. */
1906
+ countLabel: 0.08,
1907
+ /** Minimum gap between packed rows */
1908
+ rowGap: 0.01
1909
+ };
1910
+ var BAND_LABEL_MIN_WU = 0.182;
1911
+ var MIN_TIMESTAMP_PX = 7;
1912
+ var COUNT_LABEL_GAP = 0.01;
1913
+ function computeLabelRowHeight(textScale) {
1914
+ const labelTextScale = Math.min(textScale, 1.5);
1915
+ const labelFontHeight = Math.max(0.105 * labelTextScale, BAND_LABEL_MIN_WU);
1916
+ const padding = 0.04;
1917
+ return labelFontHeight + padding;
1918
+ }
1919
+ function getBandPadding(textScale = 1) {
1920
+ const s = Math.sqrt(textScale);
1921
+ return {
1922
+ countLabel: BASE_BAND_PADDING.countLabel * s,
1923
+ rowGap: BASE_BAND_PADDING.rowGap * s
1924
+ };
1925
+ }
1926
+ var BAND_PADDING = getBandPadding(1);
1927
+ function makeBand(top, bottom, textScale = 1) {
1928
+ const padding = getBandPadding(textScale);
1929
+ return {
1930
+ top,
1931
+ bottom,
1932
+ center: (top + bottom) / 2,
1933
+ height: top - bottom,
1934
+ contentTop: top,
1935
+ // Labels are in dedicated rows above — full band for content
1936
+ countLabelY: top - padding.countLabel
1937
+ };
1938
+ }
1939
+ function computeBandProportions(data) {
1940
+ const agentWeight = Math.max(1, Math.sqrt(data.agentCount / 5));
1941
+ const artifactWeight = Math.max(0.5, Math.sqrt(data.maxArtifactRows / 5));
1942
+ const prWeight = Math.max(0.5, Math.sqrt(data.maxPRRows / 3));
1943
+ const commitWeight = 0.5;
1944
+ const timelineWeight = 0.3;
1945
+ const totalWeight2 = agentWeight + artifactWeight + prWeight + commitWeight + timelineWeight;
1946
+ const totalGaps = GAP_AGENTS_ARTIFACTS + GAP_ARTIFACTS_PRS + GAP_PRS_COMMITS + GAP_COMMITS_TIMELINE;
1947
+ const availableForBands = 1 - totalGaps;
1948
+ const totalMin = MIN_AGENTS + MIN_ARTIFACTS + MIN_PRS + MIN_COMMITS + MIN_TIMELINE;
1949
+ const extraSpace = Math.max(0, availableForBands - totalMin);
1950
+ const agents = MIN_AGENTS + extraSpace * (agentWeight / totalWeight2);
1951
+ const artifacts = MIN_ARTIFACTS + extraSpace * (artifactWeight / totalWeight2);
1952
+ const prs = MIN_PRS + extraSpace * (prWeight / totalWeight2);
1953
+ const commits = MIN_COMMITS + extraSpace * (commitWeight / totalWeight2);
1954
+ const timeline = MIN_TIMELINE + extraSpace * (timelineWeight / totalWeight2);
1955
+ return { agents, artifacts, prs, commits, timeline };
1956
+ }
1957
+ var BAR_GAP_RATIO = 0.2;
1958
+ function deriveBarHeightInternal(bandHeight, maxRows) {
1959
+ return bandHeight / (maxRows * (1 + BAR_GAP_RATIO));
1960
+ }
1961
+ function getLayout(aspect, dataSummary, textScale = 1, viewportPxWidth) {
1962
+ const worldHeight = CONTENT_HEIGHT;
1963
+ const worldWidth = worldHeight * aspect;
1964
+ const STABLE_WIDTH_PX = 1280;
1965
+ const MIN_SIZE_FACTOR = 0.6;
1966
+ const sizeFactor = viewportPxWidth ? Math.max(MIN_SIZE_FACTOR, Math.min(1, viewportPxWidth / STABLE_WIDTH_PX)) : 1;
1967
+ const halfW = worldWidth / 2;
1968
+ const halfH = worldHeight / 2;
1969
+ const ppu = viewportPxWidth ? viewportPxWidth / worldWidth : 0;
1970
+ const timePadLeft = worldWidth * 0.06;
1971
+ const timePadRight = worldWidth * 0.06;
1972
+ const timeLeft = -halfW + timePadLeft;
1973
+ const timeRight = halfW - timePadRight;
1974
+ const timeWidth = timeRight - timeLeft;
1975
+ const bp = dataSummary ? computeBandProportions(dataSummary) : {
1976
+ agents: DEFAULT_BAND_AGENTS,
1977
+ artifacts: DEFAULT_BAND_ARTIFACTS,
1978
+ prs: DEFAULT_BAND_PRS,
1979
+ commits: DEFAULT_BAND_COMMITS,
1980
+ timeline: DEFAULT_BAND_TIMELINE
1981
+ };
1982
+ const gapScale = Math.sqrt(textScale);
1983
+ const stackElements = buildStackElements(worldHeight, textScale, bp, gapScale);
1984
+ const stackPositions = computeStackPositions(stackElements, halfH);
1985
+ const pos = (id) => getPos(stackPositions, id);
1986
+ const bands = {
1987
+ agents: makeBand(pos("band-agents").top, pos("band-agents").bottom, textScale),
1988
+ artifacts: makeBand(pos("band-artifacts").top, pos("band-artifacts").bottom, textScale),
1989
+ prs: makeBand(pos("band-prs").top, pos("band-prs").bottom, textScale),
1990
+ commits: makeBand(pos("band-commits").top, pos("band-commits").bottom, textScale),
1991
+ timeline: makeBand(pos("band-timeline").top, pos("band-timeline").bottom, textScale)
1992
+ };
1993
+ const separators = [
1994
+ pos("band-artifacts").top + pos("gap-1").height * 0.3,
1995
+ pos("band-prs").top + pos("gap-2").height * 0.3,
1996
+ pos("band-commits").top + pos("gap-3").height * 0.3,
1997
+ pos("band-timeline").top + pos("gap-4").height * 0.3
1998
+ ];
1999
+ const phaseBarTopY = pos("phase-bar").top;
2000
+ const phaseBarBtm = pos("phase-bar").bottom;
2001
+ const phaseBackgroundTop = separators[0];
2002
+ const agentBandW = timeWidth - 0.1;
2003
+ const agentBandH = bands.agents.height - 0.1;
2004
+ const agentBandMin = Math.min(agentBandW, agentBandH);
2005
+ const nodeRadiusRaw = Math.max(0.06, Math.min(0.25, agentBandMin * 0.047));
2006
+ const nodeRadius = nodeRadiusRaw * sizeFactor;
2007
+ const labelHeight = nodeRadius * 2.08 * textScale;
2008
+ const timeToX = (normalizedTime) => {
2009
+ return timeLeft + normalizedTime * timeWidth;
2010
+ };
2011
+ const xToProgress = (worldX) => {
2012
+ return Math.max(0, Math.min(1, (worldX - timeLeft) / timeWidth));
2013
+ };
2014
+ const hoursToX = (hours, totalHours) => {
2015
+ return timeToX(hours / totalHours);
2016
+ };
2017
+ const agentToWorld = (nx, ny) => {
2018
+ const xMin = timeLeft + 0.05;
2019
+ const xMax = timeRight - 0.05;
2020
+ const yMin = bands.agents.bottom + labelHeight + nodeRadius;
2021
+ const yMax = bands.agents.top - pillHeadroom;
2022
+ const bandW = xMax - xMin;
2023
+ const bandH = yMax - yMin;
2024
+ return {
2025
+ x: xMin + nx * bandW,
2026
+ y: yMax - ny * bandH
2027
+ };
2028
+ };
2029
+ const worldXFn = (nx) => {
2030
+ return (nx - 0.5) * worldWidth;
2031
+ };
2032
+ const worldYFn = (ny) => {
2033
+ return (0.5 - ny) * worldHeight;
2034
+ };
2035
+ const worldRadiusFn = (nr) => {
2036
+ return nr * worldWidth;
2037
+ };
2038
+ const artifactMaxRows = dataSummary?.maxArtifactRows ?? 3;
2039
+ const prMaxRows = dataSummary?.maxPRRows ?? 3;
2040
+ const artifactBarHeight = deriveBarHeightInternal(bands.artifacts.height, artifactMaxRows);
2041
+ const prBarHeight = deriveBarHeightInternal(bands.prs.height, prMaxRows);
2042
+ const artifactRowStride = artifactBarHeight * (1 + BAR_GAP_RATIO);
2043
+ const prRowStride = prBarHeight * (1 + BAR_GAP_RATIO);
2044
+ const MIN_BAR_PX = 2;
2045
+ const worstCasePPU = 390 / worldWidth;
2046
+ const minBarWidth = Math.max(0.02, MIN_BAR_PX / worstCasePPU);
2047
+ const maxBarWidth = timeWidth;
2048
+ const agentCoreRadius = nodeRadius;
2049
+ const humanSizeMultiplier = 1.5;
2050
+ const maxWorkGrowth = 1;
2051
+ const glowRadiusRatio = GLOW_RADIUS_RATIO;
2052
+ const glowIntensity = 0.5;
2053
+ const humanRingGap = 0;
2054
+ const humanRingThickness = 0.012 * sizeFactor;
2055
+ const humanRingOpacity = 0.9;
2056
+ const humanGlowRadiusRatio = 1.8;
2057
+ const humanGlowBaseIntensity = 0.15;
2058
+ const humanGlowPeakIntensity = 0.5;
2059
+ const activityPulse = 0.3;
2060
+ const minBrightness = 0.35;
2061
+ const agentShaderNodeRadius = AGENT_SHADER_NODE_RADIUS;
2062
+ const singleLinePillHeight = nodeRadius * 1.2;
2063
+ const pillGap = nodeRadius * PILL_GAP_RATIO + PILL_GAP_BASE;
2064
+ const pillHeadroom = pillGap + singleLinePillHeight;
2065
+ const agentQuadAAMargin = 0.02 * sizeFactor;
2066
+ const agentQuadSizeMultiplier = 2.2;
2067
+ const narrationOffsetY = nodeRadius * 2.2 + 0.1 * sizeFactor;
2068
+ const narrationPadX = 0.015 * sizeFactor;
2069
+ const narrationPadY = 0.03 * sizeFactor;
2070
+ const narrationMaxWidth = Math.min(4 * sizeFactor, worldWidth * 0.75);
2071
+ const narrationMinScale = 0.92;
2072
+ const labelTextScale = Math.min(textScale, 1.5);
2073
+ const playheadDotRadius = 0.04 * sizeFactor;
2074
+ const playheadLabelMinHeight = ppu > 0 ? MIN_TIMESTAMP_PX / ppu : 0;
2075
+ const playheadLabelMinWidth = playheadLabelMinHeight / (48 / 128);
2076
+ const playheadLabelWidth = Math.max(0.4 * labelTextScale * sizeFactor, playheadLabelMinWidth);
2077
+ const playheadLabelHeight = playheadLabelWidth * (48 / 128);
2078
+ const playheadLabelOffsetY = 0.06 * sizeFactor;
2079
+ const playheadCanvasWidth = 128;
2080
+ const playheadCanvasHeight = 48;
2081
+ const playheadFontSize = 24;
2082
+ const agentsBandHeight = bands.agents.top - bands.agents.bottom;
2083
+ const trustBarWidth = nodeRadius * 0.9;
2084
+ const trustBarHeight = agentsBandHeight * 0.17 * sizeFactor;
2085
+ const trustBarOffsetX = -(nodeRadius + trustBarWidth / 2 + 0.08 * sizeFactor);
2086
+ const trustBarLabelHeight = 0.07 * labelTextScale * sizeFactor;
2087
+ const trustBarLabelDisplayHeight = trustBarLabelHeight * textScale;
2088
+ const phaseLabelFontSize = 0.045 * labelTextScale * sizeFactor;
2089
+ const phaseLabelLeftPad = 0.15 * sizeFactor;
2090
+ const phaseMinLabelWidth = 0.04 * sizeFactor;
2091
+ const phaseGradientFraction = 0.15;
2092
+ const commitLineWidth = 0.02 * sizeFactor;
2093
+ const commitMinLineHeight = 0.01 * sizeFactor;
2094
+ const particleBaseRadius = 7e-3 * sizeFactor;
2095
+ const particleSizeScale = 28e-4 * sizeFactor;
2096
+ const commitGlowWidthScale = 4;
2097
+ const commitGlowHeightScale = 1.5;
2098
+ const commitGlowOpacity = 0.35;
2099
+ const commitCoreOpacity = 0.85;
2100
+ const bandLabelLeftX = -halfW + 0.15;
2101
+ const bandLabelRightX = Math.min(halfW, timeRight + 0.3 * sizeFactor);
2102
+ const atlasBasePxPerUnit = 128;
2103
+ const atlasMinFontPx = 12;
2104
+ const hourLabelMinWu = ppu > 0 ? MIN_TIMESTAMP_PX / ppu : 0;
2105
+ const hourLabelFontSizeWu = Math.max(0.07 * textScale * sizeFactor, hourLabelMinWu);
2106
+ const bandLabelFontSizeWu = Math.max(
2107
+ 0.105 * labelTextScale * sizeFactor,
2108
+ BAND_LABEL_MIN_WU * sizeFactor
2109
+ );
2110
+ const countLabelHeight = bandLabelFontSizeWu;
2111
+ const bandLabelColor = "#888888";
2112
+ return {
2113
+ sizeFactor,
2114
+ worldWidth,
2115
+ worldHeight,
2116
+ worldLeft: -halfW,
2117
+ worldRight: halfW,
2118
+ worldTop: halfH,
2119
+ worldBottom: -halfH,
2120
+ timeLeft,
2121
+ timeRight,
2122
+ timeWidth,
2123
+ bands,
2124
+ separators,
2125
+ nodeRadius,
2126
+ labelHeight,
2127
+ // Band content sizing
2128
+ artifactBarHeight,
2129
+ prBarHeight,
2130
+ artifactRowStride,
2131
+ prRowStride,
2132
+ minBarWidth,
2133
+ maxBarWidth,
2134
+ // Atlas text sizing
2135
+ atlasBasePxPerUnit,
2136
+ atlasMinFontPx,
2137
+ hourLabelFontSizeWu,
2138
+ bandLabelFontSizeWu,
2139
+ bandLabelColor,
2140
+ // Agent visual sizing
2141
+ agentCoreRadius,
2142
+ humanSizeMultiplier,
2143
+ maxWorkGrowth,
2144
+ glowRadiusRatio,
2145
+ pillHeadroom,
2146
+ glowIntensity,
2147
+ humanRingGap,
2148
+ humanRingThickness,
2149
+ humanRingOpacity,
2150
+ humanGlowRadiusRatio,
2151
+ humanGlowBaseIntensity,
2152
+ humanGlowPeakIntensity,
2153
+ activityPulse,
2154
+ minBrightness,
2155
+ agentShaderNodeRadius,
2156
+ agentQuadAAMargin,
2157
+ agentQuadSizeMultiplier,
2158
+ // Narration positioning
2159
+ narrationOffsetY,
2160
+ narrationPadX,
2161
+ narrationPadY,
2162
+ narrationMaxWidth,
2163
+ narrationMinScale,
2164
+ // Playhead sizing
2165
+ playheadDotRadius,
2166
+ playheadLabelWidth,
2167
+ playheadLabelHeight,
2168
+ playheadLabelOffsetY,
2169
+ playheadCanvasWidth,
2170
+ playheadCanvasHeight,
2171
+ playheadFontSize,
2172
+ // Trust bar sizing
2173
+ trustBarWidth,
2174
+ trustBarHeight,
2175
+ trustBarOffsetX,
2176
+ trustBarLabelHeight,
2177
+ trustBarLabelDisplayHeight,
2178
+ // Phase background positioning
2179
+ phaseBarTop: phaseBarTopY,
2180
+ phaseBarBottom: phaseBarBtm,
2181
+ phaseBackgroundTop,
2182
+ phaseLabelFontSize,
2183
+ phaseLabelLeftPad,
2184
+ phaseMinLabelWidth,
2185
+ phaseGradientFraction,
2186
+ // Commit band sizing
2187
+ commitLineWidth,
2188
+ commitMinLineHeight,
2189
+ // Particle sizing
2190
+ particleBaseRadius,
2191
+ particleSizeScale,
2192
+ // Commit band glow
2193
+ commitGlowWidthScale,
2194
+ commitGlowHeightScale,
2195
+ commitGlowOpacity,
2196
+ commitCoreOpacity,
2197
+ // Label positioning
2198
+ bandLabelLeftX,
2199
+ bandLabelRightX,
2200
+ countLabelHeight,
2201
+ // Band label font size — also used for count labels (same size per @snorre)
2202
+ countLabelFontSizeWu: bandLabelFontSizeWu,
2203
+ // Band label row Y positions (from stacking loop)
2204
+ // Band label is in upper portion of label row, count label below it.
2205
+ // Small top padding (0.02) prevents label mesh from touching worldTop edge.
2206
+ bandLabelY: {
2207
+ agents: pos("label-agents").top - 0.02 - bandLabelFontSizeWu / 2,
2208
+ artifacts: pos("label-artifacts").top - 0.02 - bandLabelFontSizeWu / 2,
2209
+ prs: pos("label-prs").top - 0.02 - bandLabelFontSizeWu / 2,
2210
+ commits: pos("label-commits").top - 0.02 - bandLabelFontSizeWu / 2
2211
+ },
2212
+ // Count label Y positions — in the gutter, just below band label text.
2213
+ // These overlap into the band area below the label row. Zero extra Y space consumed.
2214
+ countLabelY: {
2215
+ agents: pos("label-agents").top - 0.02 - bandLabelFontSizeWu - COUNT_LABEL_GAP - countLabelHeight / 2,
2216
+ artifacts: pos("label-artifacts").top - 0.02 - bandLabelFontSizeWu - COUNT_LABEL_GAP - countLabelHeight / 2,
2217
+ prs: pos("label-prs").top - 0.02 - bandLabelFontSizeWu - COUNT_LABEL_GAP - countLabelHeight / 2,
2218
+ commits: pos("label-commits").top - 0.02 - bandLabelFontSizeWu - COUNT_LABEL_GAP - countLabelHeight / 2
2219
+ },
2220
+ // Dedicated row bounds (for invariant tests + consumers)
2221
+ playheadLabelZoneTop: pos("playhead-zone").top,
2222
+ playheadLabelZoneBottom: pos("playhead-zone").bottom,
2223
+ // Stacking loop data (for invariant tests)
2224
+ stackElements,
2225
+ stackPositions,
2226
+ // Helper functions
2227
+ timeToX,
2228
+ xToProgress,
2229
+ hoursToX,
2230
+ agentToWorld,
2231
+ worldX: worldXFn,
2232
+ worldY: worldYFn,
2233
+ worldRadius: worldRadiusFn
2234
+ };
2235
+ }
2236
+ var MAX_TEXT_SCALE = 2;
2237
+ function getTextScale(_viewportPxWidth, _worldWidth) {
2238
+ return MAX_TEXT_SCALE;
2239
+ }
2240
+ function computeMinYGapNorm(layout) {
2241
+ const { nodeRadius, labelHeight } = layout;
2242
+ const agentBandHeight = layout.bands.agents.height;
2243
+ if (agentBandHeight <= 0) return MIN_Y_GAP_FALLBACK;
2244
+ const NODE_RADIUS_REF2 = 0.15;
2245
+ const layoutScale = nodeRadius / NODE_RADIUS_REF2;
2246
+ const typicalCoreScale = 1.5 * layoutScale;
2247
+ const visualRadius = nodeRadius * typicalCoreScale;
2248
+ const labelGap = visualRadius * 0.65;
2249
+ const halfExtent = visualRadius + labelGap + labelHeight / 2;
2250
+ const gapWu = halfExtent * 2;
2251
+ return gapWu / agentBandHeight;
2252
+ }
2253
+ var MIN_Y_GAP_FALLBACK = 0.06;
2254
+ function computeMinXGapNorm(layout) {
2255
+ const { nodeRadius, labelHeight } = layout;
2256
+ const p0 = layout.agentToWorld(0, 0.5);
2257
+ const p1 = layout.agentToWorld(1, 0.5);
2258
+ const bandWidth = Math.abs(p1.x - p0.x);
2259
+ if (bandWidth <= 0) return 0.1;
2260
+ const NODE_RADIUS_REF2 = 0.15;
2261
+ const layoutScale = nodeRadius / NODE_RADIUS_REF2;
2262
+ const typicalCoreScale = 1.5 * layoutScale;
2263
+ const visualRadius = nodeRadius * typicalCoreScale;
2264
+ const labelWidth = labelHeight * 3;
2265
+ const halfExtentX = Math.max(visualRadius, labelWidth / 2);
2266
+ const gapWu = halfExtentX * 2;
2267
+ return gapWu / bandWidth;
2268
+ }
2269
+ function computeBandAspect(layout) {
2270
+ const p00 = layout.agentToWorld(0, 0);
2271
+ const p11 = layout.agentToWorld(1, 1);
2272
+ const bandW = Math.abs(p11.x - p00.x);
2273
+ const bandH = Math.abs(p11.y - p00.y);
2274
+ if (bandH <= 0) return 5;
2275
+ return bandW / bandH;
2276
+ }
2277
+ var DEFAULT_LAYOUT = getLayout(CONTENT_ASPECT);
2278
+
2279
+ // src/renderer/band-sizing.ts
2280
+ var MIN_ROWS = 3;
2281
+ function computeArtifactPeakRows(events) {
2282
+ const times = [];
2283
+ for (const e of events) {
2284
+ if (e.type === "artifact") {
2285
+ times.push(e.normalizedTime);
2286
+ }
2287
+ }
2288
+ if (times.length === 0) return MIN_ROWS;
2289
+ times.sort((a, b) => a - b);
2290
+ const barWidth = 3e-3;
2291
+ const gap = 1e-3;
2292
+ const rows = [];
2293
+ let maxRows = 0;
2294
+ for (const t of times) {
2295
+ let placed = false;
2296
+ for (let r = 0; r < rows.length; r++) {
2297
+ if (rows[r] <= t) {
2298
+ rows[r] = t + barWidth + gap;
2299
+ placed = true;
2300
+ break;
2301
+ }
2302
+ }
2303
+ if (!placed) {
2304
+ rows.push(t + barWidth + gap);
2305
+ }
2306
+ if (rows.length > maxRows) {
2307
+ maxRows = rows.length;
2308
+ }
2309
+ }
2310
+ return Math.max(MIN_ROWS, maxRows);
2311
+ }
2312
+ function computePRPeakRows(events) {
2313
+ const created = /* @__PURE__ */ new Map();
2314
+ const merged = /* @__PURE__ */ new Map();
2315
+ for (const e of events) {
2316
+ const prNumber = e.metadata.prNumber;
2317
+ if (prNumber === void 0) continue;
2318
+ if (e.type === "pr_created") {
2319
+ created.set(prNumber, e.normalizedTime);
2320
+ } else if (e.type === "pr_merged") {
2321
+ merged.set(prNumber, e.normalizedTime);
2322
+ }
2323
+ }
2324
+ if (created.size === 0) return MIN_ROWS;
2325
+ const intervals = [];
2326
+ for (const [prNumber, openTime] of created) {
2327
+ const closeTime = merged.get(prNumber) ?? 1;
2328
+ intervals.push({ open: openTime, close: closeTime });
2329
+ }
2330
+ intervals.sort((a, b) => a.open - b.open);
2331
+ const rows = [];
2332
+ let maxRows = 0;
2333
+ for (const iv of intervals) {
2334
+ let placed = false;
2335
+ for (let r = 0; r < rows.length; r++) {
2336
+ if (rows[r] <= iv.open) {
2337
+ rows[r] = iv.close + 1e-3;
2338
+ placed = true;
2339
+ break;
2340
+ }
2341
+ }
2342
+ if (!placed) {
2343
+ rows.push(iv.close + 1e-3);
2344
+ }
2345
+ if (rows.length > maxRows) {
2346
+ maxRows = rows.length;
2347
+ }
2348
+ }
2349
+ return Math.max(MIN_ROWS, maxRows);
2350
+ }
2351
+
2352
+ exports.BAND_PADDING = BAND_PADDING;
2353
+ exports.DEFAULT_LAYOUT = DEFAULT_LAYOUT;
2354
+ exports.computeAgentPositions = computeAgentPositions;
2355
+ exports.computeArtifactPeakRows = computeArtifactPeakRows;
2356
+ exports.computeBandAspect = computeBandAspect;
2357
+ exports.computeLayoutPositions = computeLayoutPositions;
2358
+ exports.computeMinXGapNorm = computeMinXGapNorm;
2359
+ exports.computeMinYGapNorm = computeMinYGapNorm;
2360
+ exports.computePRPeakRows = computePRPeakRows;
2361
+ exports.enrichVizData = enrichVizData;
2362
+ exports.getFrameState = getFrameState;
2363
+ exports.getLayout = getLayout;
2364
+ exports.getTextScale = getTextScale;
2365
+ exports.processData = processData;
2366
+ exports.transformToCurationData = transformToCurationData;
2367
+ exports.transformToRawData = transformToRawData;
2368
+ //# sourceMappingURL=index.cjs.map
2369
+ //# sourceMappingURL=index.cjs.map