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.
- package/README.md +139 -0
- package/bin/miriad-viz.mjs +45 -0
- package/dist-lib/frame-state-eeHrDxl-.d.cts +228 -0
- package/dist-lib/frame-state-eeHrDxl-.d.ts +228 -0
- package/dist-lib/index.cjs +2369 -0
- package/dist-lib/index.cjs.map +1 -0
- package/dist-lib/index.d.cts +707 -0
- package/dist-lib/index.d.ts +707 -0
- package/dist-lib/index.js +2352 -0
- package/dist-lib/index.js.map +1 -0
- package/dist-lib/layout-Cc23UM-j.d.cts +353 -0
- package/dist-lib/layout-Cc23UM-j.d.ts +353 -0
- package/dist-lib/renderer/index.cjs +3697 -0
- package/dist-lib/renderer/index.cjs.map +1 -0
- package/dist-lib/renderer/index.d.cts +205 -0
- package/dist-lib/renderer/index.d.ts +205 -0
- package/dist-lib/renderer/index.js +3668 -0
- package/dist-lib/renderer/index.js.map +1 -0
- package/dist-lib/viewer/exports.cjs +10572 -0
- package/dist-lib/viewer/exports.cjs.map +1 -0
- package/dist-lib/viewer/exports.d.cts +130 -0
- package/dist-lib/viewer/exports.d.ts +130 -0
- package/dist-lib/viewer/exports.js +10541 -0
- package/dist-lib/viewer/exports.js.map +1 -0
- package/package.json +108 -0
- package/template/remotion/README.md +139 -0
- package/template/remotion/package.json +29 -0
- package/template/remotion/pnpm-lock.yaml +2816 -0
- package/template/remotion/public/data/.gitkeep +0 -0
- package/template/remotion/remotion.config.ts +5 -0
- package/template/remotion/src/MiriadViz.tsx +220 -0
- package/template/remotion/src/Root.tsx +39 -0
- package/template/remotion/tsconfig.json +16 -0
- package/template/viewer/README.md +13 -0
- package/template/viewer/index.html +17 -0
- package/template/viewer/package.json +27 -0
- package/template/viewer/public/data/.gitkeep +0 -0
- package/template/viewer/src/App.tsx +313 -0
- package/template/viewer/src/main.tsx +4 -0
- package/template/viewer/tsconfig.json +16 -0
- 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
|