miriad-viz 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/README.md +139 -0
  2. package/bin/miriad-viz.mjs +45 -0
  3. package/dist-lib/frame-state-eeHrDxl-.d.cts +228 -0
  4. package/dist-lib/frame-state-eeHrDxl-.d.ts +228 -0
  5. package/dist-lib/index.cjs +2369 -0
  6. package/dist-lib/index.cjs.map +1 -0
  7. package/dist-lib/index.d.cts +707 -0
  8. package/dist-lib/index.d.ts +707 -0
  9. package/dist-lib/index.js +2352 -0
  10. package/dist-lib/index.js.map +1 -0
  11. package/dist-lib/layout-Cc23UM-j.d.cts +353 -0
  12. package/dist-lib/layout-Cc23UM-j.d.ts +353 -0
  13. package/dist-lib/renderer/index.cjs +3697 -0
  14. package/dist-lib/renderer/index.cjs.map +1 -0
  15. package/dist-lib/renderer/index.d.cts +205 -0
  16. package/dist-lib/renderer/index.d.ts +205 -0
  17. package/dist-lib/renderer/index.js +3668 -0
  18. package/dist-lib/renderer/index.js.map +1 -0
  19. package/dist-lib/viewer/exports.cjs +10572 -0
  20. package/dist-lib/viewer/exports.cjs.map +1 -0
  21. package/dist-lib/viewer/exports.d.cts +130 -0
  22. package/dist-lib/viewer/exports.d.ts +130 -0
  23. package/dist-lib/viewer/exports.js +10541 -0
  24. package/dist-lib/viewer/exports.js.map +1 -0
  25. package/package.json +108 -0
  26. package/template/remotion/README.md +139 -0
  27. package/template/remotion/package.json +29 -0
  28. package/template/remotion/pnpm-lock.yaml +2816 -0
  29. package/template/remotion/public/data/.gitkeep +0 -0
  30. package/template/remotion/remotion.config.ts +5 -0
  31. package/template/remotion/src/MiriadViz.tsx +220 -0
  32. package/template/remotion/src/Root.tsx +39 -0
  33. package/template/remotion/tsconfig.json +16 -0
  34. package/template/viewer/README.md +13 -0
  35. package/template/viewer/index.html +17 -0
  36. package/template/viewer/package.json +27 -0
  37. package/template/viewer/public/data/.gitkeep +0 -0
  38. package/template/viewer/src/App.tsx +313 -0
  39. package/template/viewer/src/main.tsx +4 -0
  40. package/template/viewer/tsconfig.json +16 -0
  41. package/template/viewer/vite.config.ts +12 -0
package/README.md ADDED
@@ -0,0 +1,139 @@
1
+ # miriad-viz
2
+
3
+ Animated timeline visualization for Miriad channel retrospectives — showing how AI agents and humans collaborate over time.
4
+
5
+ Turn 60+ hours of channel activity into a cinematic replay: agents joining, communicating, shipping code, and hitting milestones.
6
+
7
+ ## What It Does
8
+
9
+ Given extracted channel data (commits, PRs, messages, artifacts), miriad-viz produces:
10
+
11
+ - **Interactive viewer** — scrub through time, watch the team form and build
12
+ - **Video export** — render to MP4 via Remotion for sharing
13
+
14
+ The visualization shows:
15
+ - Agent nodes that pulse with activity
16
+ - Communication beams traveling between agents
17
+ - Artifact, PR, and commit timelines
18
+ - Phase backgrounds and milestone markers
19
+ - Trust arc and narration overlays
20
+
21
+ ## Quick Start
22
+
23
+ ```bash
24
+ # Install dependencies
25
+ pnpm install
26
+
27
+ # Run extraction (requires gh CLI authenticated)
28
+ pnpm extract --repo owner/repo --out ./data
29
+
30
+ # Transform to viz-ready format
31
+ pnpm viz ./data
32
+
33
+ # Launch interactive viewer
34
+ pnpm dev
35
+ ```
36
+
37
+ ## Data Pipeline
38
+
39
+ ```
40
+ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐
41
+ │ Extraction │───▶│ Transform │───▶│ Engine │───▶│ Renderer │
42
+ │ │ │ │ │ │ │ │
43
+ │ gh CLI │ │ normalize │ │ frame state │ │ R3F + drei │
44
+ │ Miriad MCP │ │ enrich │ │ per tick │ │ zero logic │
45
+ └─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘
46
+ │ │ │
47
+ ▼ ▼ ▼
48
+ RawData VizData FrameState
49
+ (8 JSON files) (renderer-ready) (pure function)
50
+ ```
51
+
52
+ ### Data Contracts
53
+
54
+ | Layer | Input | Output | Purpose |
55
+ |-------|-------|--------|---------|
56
+ | Extraction | GitHub API, Miriad MCP | RawData (8 JSON files) | Gather raw data |
57
+ | Transform | RawData + CurationData | VizData | Normalize, enrich, compute layout |
58
+ | Engine | VizData + time | FrameState | Pure `getFrameState(t, data)` |
59
+ | Renderer | FrameState | Pixels | Declarative R3F scene |
60
+
61
+ See [docs/data-contracts.md](./docs/data-contracts.md) for full TypeScript schemas.
62
+
63
+ ## CLI Commands
64
+
65
+ ```bash
66
+ # Extraction — pull data from GitHub
67
+ pnpm extract --repo owner/repo --out ./data
68
+
69
+ # With Miriad channel data (pre-extracted via MCP)
70
+ pnpm extract --repo owner/repo --out ./data \
71
+ --chat-input ./raw-messages.json \
72
+ --artifact-input ./raw-artifacts.json
73
+
74
+ # Transform — process into viz-ready format
75
+ pnpm viz ./data # Output to ./data/output/
76
+ pnpm viz ./data --out ./custom-dir # Custom output directory
77
+ ```
78
+
79
+ See [docs/extraction-guide.md](./docs/extraction-guide.md) for step-by-step data extraction.
80
+
81
+ ## Development
82
+
83
+ ```bash
84
+ pnpm dev # Vite dev server (http://localhost:5173)
85
+ pnpm test # Run tests
86
+ pnpm lint # Biome check
87
+ pnpm lint:fix # Biome auto-fix
88
+ pnpm build # Production build
89
+ ```
90
+
91
+ ## Architecture
92
+
93
+ **4-layer pipeline** with strict separation of concerns:
94
+
95
+ 1. **Extraction** — Miriad MCP + gh CLI produce 8 JSON source files
96
+ 2. **Transform** — Normalize, enrich, compute agent layout → VizData
97
+ 3. **State Engine** — `getFrameState(t, vizData)` returns FrameState (pure function)
98
+ 4. **Renderer** — R3F/drei presents FrameState (zero data processing)
99
+
100
+ Key principle: **Renderer does zero logic.** All animation is pre-computed by the engine. The renderer is a dumb display layer that reads FrameState and draws it.
101
+
102
+ ## Stack
103
+
104
+ - **Vite** — bundler + dev server
105
+ - **React Three Fiber + drei** — declarative 3D rendering
106
+ - **Remotion** — video export
107
+ - **zustand** — state management
108
+ - **Biome** — lint + format
109
+ - **Vitest** — tests
110
+
111
+ ## Documentation
112
+
113
+ | Guide | Description |
114
+ |-------|-------------|
115
+ | [Data Contracts](./docs/data-contracts.md) | TypeScript schemas for RawData, CurationData, VizData |
116
+ | [Extraction Guide](./docs/extraction-guide.md) | How to extract data from GitHub + Miriad |
117
+ | [Curation Guide](./docs/curation-guide.md) | Editorial content: phases, milestones, quotes |
118
+ | [FEATURES.md](./FEATURES.md) | Feature tracker with implementation status |
119
+
120
+ ## Source Data Files
121
+
122
+ The extraction pipeline produces 8 JSON files:
123
+
124
+ | File | Source | Description |
125
+ |------|--------|-------------|
126
+ | `git-commits.json` | GitHub API | Commit history with stats |
127
+ | `pr-details.json` | GitHub API | Pull request metadata |
128
+ | `pr-agent-map.json` | Heuristics | Maps PRs to agent callsigns |
129
+ | `chat-activity.json` | Miriad MCP | Channel activity summary |
130
+ | `artifact-data.json` | Miriad MCP | Board artifact inventory |
131
+ | `timeline-events.json` | Processing | Unified event stream |
132
+ | `retro-page-data.json` | Editorial | Phases, milestones, trust arc |
133
+ | `retro-page-quotes.json` | Editorial | Curated quotes |
134
+
135
+ TypeScript types for all files: `src/transform/source-types.ts`
136
+
137
+ ## License
138
+
139
+ MIT
@@ -0,0 +1,45 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * CLI entry point for miriad-viz.
5
+ *
6
+ * In development (cloned repo): tsx runs TypeScript source directly.
7
+ * After build (npm publish): loads compiled JS from dist-cli/.
8
+ */
9
+
10
+ import { execFileSync } from 'node:child_process';
11
+ import { existsSync } from 'node:fs';
12
+ import { dirname, resolve } from 'node:path';
13
+ import { fileURLToPath } from 'node:url';
14
+
15
+ const __dirname = dirname(fileURLToPath(import.meta.url));
16
+ const root = resolve(__dirname, '..');
17
+
18
+ const compiledEntry = resolve(root, 'dist-cli', 'index.js');
19
+ const sourceEntry = resolve(root, 'src', 'cli', 'guided', 'index.ts');
20
+
21
+ if (existsSync(compiledEntry)) {
22
+ // Published package or post-build — load compiled JS
23
+ await import(compiledEntry);
24
+ } else if (existsSync(sourceEntry)) {
25
+ // Development — use tsx to run TypeScript directly
26
+ const tsxBin = resolve(root, 'node_modules', '.bin', 'tsx');
27
+ if (!existsSync(tsxBin)) {
28
+ console.error('Error: tsx not found. Run `pnpm install` first.');
29
+ process.exit(1);
30
+ }
31
+ try {
32
+ execFileSync(tsxBin, [sourceEntry, ...process.argv.slice(2)], {
33
+ cwd: process.cwd(),
34
+ stdio: 'inherit',
35
+ });
36
+ } catch (err) {
37
+ // execFileSync throws on non-zero exit — propagate exit code
38
+ process.exit(err.status ?? 1);
39
+ }
40
+ } else {
41
+ console.error('Error: miriad-viz CLI not found. Is the package installed correctly?');
42
+ console.error(` Looked for: ${compiledEntry}`);
43
+ console.error(` Looked for: ${sourceEntry}`);
44
+ process.exit(1);
45
+ }
@@ -0,0 +1,228 @@
1
+ /**
2
+ * CONTRACT C: FrameState — what the state engine produces.
3
+ * Pure function of (progress, vizData, layoutContext) → FrameState.
4
+ * Contains ONLY primitive values: positions, colors, opacities.
5
+ * The renderer maps these to canvas draw calls. Zero logic.
6
+ */
7
+ /**
8
+ * Minimal layout context the engine needs for sizing computations.
9
+ * Avoids coupling engine to the full Layout interface — only the
10
+ * fields needed for agent sizing and narration positioning.
11
+ */
12
+ interface LayoutContext {
13
+ /** Node radius in world units (from layout.nodeRadius) */
14
+ nodeRadius: number;
15
+ /** Label height in world units (from layout.labelHeight) */
16
+ labelHeight: number;
17
+ /** Convert normalized agent position to world coordinates */
18
+ agentToWorld: (nx: number, ny: number) => {
19
+ x: number;
20
+ y: number;
21
+ };
22
+ /** World bounds for clamping */
23
+ worldLeft: number;
24
+ worldRight: number;
25
+ worldTop: number;
26
+ /** Top of agents band in world units (from layout.bands.agents.top).
27
+ * Used to clamp narration pills so they don't extend above the band
28
+ * into label rows or legend. Flows dynamically from layout — when the
29
+ * stacking loop changes (e.g., adding legend row), this adjusts automatically. */
30
+ agentsBandTop: number;
31
+ /** Viewport width in screen pixels — for computing ppu (pixels per unit) */
32
+ viewportPxWidth: number;
33
+ /** World width in world units (typically 10) */
34
+ worldWidth: number;
35
+ /** Time axis width in world units (timeRight - timeLeft).
36
+ * Used by pack-rows to compute minimum 1px gap between bars. */
37
+ timeWidth?: number;
38
+ /** Maximum pill width in world units (from layout.narrationMaxWidth) */
39
+ narrationMaxWidth: number;
40
+ /** Horizontal padding inside pill in world units */
41
+ narrationPadX: number;
42
+ /** Vertical padding inside pill in world units */
43
+ narrationPadY: number;
44
+ /** Y offset above agent center for pill positioning */
45
+ narrationOffsetY: number;
46
+ /** Ratio of glow visual extent to core radius (from layout.glowRadiusRatio) */
47
+ glowRadiusRatio: number;
48
+ /** Minimum bar width in normalized time (layout.minBarWidth / layout.timeWidth).
49
+ * Used by pack-rows to ensure packing accounts for the renderer's minimum bar
50
+ * expansion — without this, the engine packs bars tightly but the renderer
51
+ * expands short bars past the gap, making them visually merge. */
52
+ minBarWidthNorm?: number;
53
+ /** Text scale factor for mobile legibility (from layout.textScale).
54
+ * Used to scale pill font size proportionally with other text. */
55
+ textScale?: number;
56
+ }
57
+ interface AgentState {
58
+ id: string;
59
+ x: number;
60
+ y: number;
61
+ radius: number;
62
+ color: string;
63
+ opacity: number;
64
+ glowIntensity: number;
65
+ label: string;
66
+ labelOpacity: number;
67
+ scale: number;
68
+ /** Work intensity 0-1: sliding window of recent commits/PRs/messages with decay.
69
+ * Drives node sizing (geometry scale) and core brightness.
70
+ * 0 = idle, 1 = peak activity (≥25 weighted work units in window). */
71
+ workIntensity: number;
72
+ /** Cumulative work volume: count of commits + PRs (created + merged) up to current progress.
73
+ * Drives base node sizing — bigger nodes for agents who've shipped more.
74
+ * Unlike workIntensity (instantaneous, decayed), this only grows over time. */
75
+ totalWork: number;
76
+ /** Whether this agent is human (for distinct sizing) */
77
+ isHuman: boolean;
78
+ /** Agent role — drives role tag display below name */
79
+ role: 'human' | 'builder' | 'lead' | 'reviewer' | 'tester' | 'ideation' | 'researcher' | 'auditor' | 'challenger' | 'scout' | 'pm';
80
+ /** Core circle scale factor — folds in human multiplier, work growth,
81
+ * activity pulse, bounce scale, and layout scale. The renderer reads
82
+ * this directly for shader uniforms and label positioning.
83
+ * Formula: humanMul * (1 + growthFactor * MAX_WORK_GROWTH) * activityPulse * bounceScale * layoutScale */
84
+ coreScale: number;
85
+ /** Y offset from agent center for label positioning (negative = below).
86
+ * Accounts for core scale and label gap. Renderer sets label.position.y = this value. */
87
+ labelOffsetY: number;
88
+ }
89
+ interface ParticleState {
90
+ id: string;
91
+ type: 'message';
92
+ x: number;
93
+ y: number;
94
+ opacity: number;
95
+ size: number;
96
+ color: string;
97
+ }
98
+ /** Editorial narration — documentary storytelling voice.
99
+ * Separate from quote pills (QuotePillState). Displayed in the HTML NarrationLane.
100
+ * Phase title (colored) + editorial text. Milestones are primary, phases fill gaps. */
101
+ interface EditorialNarrationState {
102
+ phaseTitle: string;
103
+ phaseColor: string;
104
+ text: string;
105
+ opacity: number;
106
+ }
107
+ interface QuotePillState {
108
+ text: string;
109
+ speaker: string;
110
+ opacity: number;
111
+ /** Speaker's normalized X position (0-1). Renderer calls layout.agentToWorld()
112
+ * to convert to world space — same code path as AgentNode. */
113
+ speakerNormX: number;
114
+ /** Speaker's normalized Y position (0-1). Renderer calls layout.agentToWorld()
115
+ * to convert to world space — same code path as AgentNode. */
116
+ speakerNormY: number;
117
+ /** World-space Y offset above agent center (glow clearance + gap + pillHeight/2).
118
+ * Renderer positions pill at: agentToWorld(normX, normY).y + yOffset */
119
+ yOffset: number;
120
+ /** World-space X offset to keep pill within viewport bounds.
121
+ * 0 when pill fits centered on agent. Positive = shift right, negative = shift left.
122
+ * Renderer positions pill at: agentToWorld(normX, normY).x + xOffset */
123
+ xOffset: number;
124
+ /** Emoji prefix from curated quote (e.g. "🏗️") */
125
+ emoji?: string;
126
+ /** Mood tag from curated quote (good | bad | great | angry | surprise) */
127
+ mood?: string;
128
+ /** Text wrapped into lines by engine (word-wrap heuristic). */
129
+ lines: string[];
130
+ /** Horizontal padding in world units. Renderer: canvasWidth = measuredText + padX * 2 */
131
+ padX: number;
132
+ /** Vertical padding in world units. Renderer: canvasHeight = measuredTextHeight + padY * 2 */
133
+ padY: number;
134
+ /** Max pill width in world units — ceiling for canvas/geometry sizing. */
135
+ maxPillWidth: number;
136
+ /** Estimated actual pill width in world units — used for X clamping.
137
+ * Single-line: content width + padding. Multi-line: maxPillWidth. */
138
+ estimatedWidth: number;
139
+ /** Speaker display name (may differ from speaker id) */
140
+ speakerLabel: string;
141
+ /** Speaker color hex */
142
+ speakerColor: string;
143
+ /** Pill background color hex (darkened speaker color) */
144
+ pillBgColor: string;
145
+ }
146
+ interface PhaseState {
147
+ id: string;
148
+ name: string;
149
+ progress: number;
150
+ color: string;
151
+ }
152
+ interface CommitDot {
153
+ normalizedTime: number;
154
+ insertions: number;
155
+ deletions: number;
156
+ agent: string;
157
+ color: string;
158
+ commitType: string;
159
+ /** Log-scaled height (0-1). Largest commit = 1.0. Uses natural log for gentle compression. */
160
+ heightNorm: number;
161
+ }
162
+ interface PRBar {
163
+ prNumber: number;
164
+ openedNormalized: number;
165
+ mergedNormalized: number | null;
166
+ agent: string;
167
+ color: string;
168
+ additions: number;
169
+ deletions: number;
170
+ /** Row index from greedy packing (0 = top row). Computed by engine. */
171
+ row: number;
172
+ }
173
+ interface ArtifactBar {
174
+ normalizedTime: number;
175
+ type: string;
176
+ agent: string;
177
+ color: string;
178
+ /** Row index from greedy packing (0 = top row). Computed by engine. */
179
+ row: number;
180
+ }
181
+ interface EdgeState {
182
+ fromId: string;
183
+ toId: string;
184
+ strength: number;
185
+ color: string;
186
+ fromColor: string;
187
+ toColor: string;
188
+ }
189
+ interface MilestoneState {
190
+ normalizedTime: number;
191
+ label: string;
192
+ visible: boolean;
193
+ }
194
+ interface AllPhaseState {
195
+ id: string;
196
+ name: string;
197
+ startNormalized: number;
198
+ endNormalized: number;
199
+ color: string;
200
+ /** Label opacity: 0 when outside phase, fades in/out at boundaries (0.5% of timeline) */
201
+ labelOpacity: number;
202
+ }
203
+ interface FrameState {
204
+ projectTitle: string;
205
+ progress: number;
206
+ agents: AgentState[];
207
+ particles: ParticleState[];
208
+ quotePills: QuotePillState[];
209
+ editorialNarration: EditorialNarrationState | null;
210
+ phase: PhaseState;
211
+ stats: {
212
+ commitsToDate: number;
213
+ prsToDate: number;
214
+ messagesToDate: number;
215
+ activeAgents: number;
216
+ };
217
+ commitDots: CommitDot[];
218
+ prBars: PRBar[];
219
+ artifactBars: ArtifactBar[];
220
+ edges: EdgeState[];
221
+ visibleMilestones: MilestoneState[];
222
+ allPhases: AllPhaseState[];
223
+ trustLevel: number;
224
+ totalHours: number;
225
+ currentHours: number;
226
+ }
227
+
228
+ export type { AgentState as A, CommitDot as C, EdgeState as E, FrameState as F, LayoutContext as L, MilestoneState as M, PRBar as P, QuotePillState as Q, AllPhaseState as a, ArtifactBar as b, ParticleState as c, PhaseState as d };
@@ -0,0 +1,228 @@
1
+ /**
2
+ * CONTRACT C: FrameState — what the state engine produces.
3
+ * Pure function of (progress, vizData, layoutContext) → FrameState.
4
+ * Contains ONLY primitive values: positions, colors, opacities.
5
+ * The renderer maps these to canvas draw calls. Zero logic.
6
+ */
7
+ /**
8
+ * Minimal layout context the engine needs for sizing computations.
9
+ * Avoids coupling engine to the full Layout interface — only the
10
+ * fields needed for agent sizing and narration positioning.
11
+ */
12
+ interface LayoutContext {
13
+ /** Node radius in world units (from layout.nodeRadius) */
14
+ nodeRadius: number;
15
+ /** Label height in world units (from layout.labelHeight) */
16
+ labelHeight: number;
17
+ /** Convert normalized agent position to world coordinates */
18
+ agentToWorld: (nx: number, ny: number) => {
19
+ x: number;
20
+ y: number;
21
+ };
22
+ /** World bounds for clamping */
23
+ worldLeft: number;
24
+ worldRight: number;
25
+ worldTop: number;
26
+ /** Top of agents band in world units (from layout.bands.agents.top).
27
+ * Used to clamp narration pills so they don't extend above the band
28
+ * into label rows or legend. Flows dynamically from layout — when the
29
+ * stacking loop changes (e.g., adding legend row), this adjusts automatically. */
30
+ agentsBandTop: number;
31
+ /** Viewport width in screen pixels — for computing ppu (pixels per unit) */
32
+ viewportPxWidth: number;
33
+ /** World width in world units (typically 10) */
34
+ worldWidth: number;
35
+ /** Time axis width in world units (timeRight - timeLeft).
36
+ * Used by pack-rows to compute minimum 1px gap between bars. */
37
+ timeWidth?: number;
38
+ /** Maximum pill width in world units (from layout.narrationMaxWidth) */
39
+ narrationMaxWidth: number;
40
+ /** Horizontal padding inside pill in world units */
41
+ narrationPadX: number;
42
+ /** Vertical padding inside pill in world units */
43
+ narrationPadY: number;
44
+ /** Y offset above agent center for pill positioning */
45
+ narrationOffsetY: number;
46
+ /** Ratio of glow visual extent to core radius (from layout.glowRadiusRatio) */
47
+ glowRadiusRatio: number;
48
+ /** Minimum bar width in normalized time (layout.minBarWidth / layout.timeWidth).
49
+ * Used by pack-rows to ensure packing accounts for the renderer's minimum bar
50
+ * expansion — without this, the engine packs bars tightly but the renderer
51
+ * expands short bars past the gap, making them visually merge. */
52
+ minBarWidthNorm?: number;
53
+ /** Text scale factor for mobile legibility (from layout.textScale).
54
+ * Used to scale pill font size proportionally with other text. */
55
+ textScale?: number;
56
+ }
57
+ interface AgentState {
58
+ id: string;
59
+ x: number;
60
+ y: number;
61
+ radius: number;
62
+ color: string;
63
+ opacity: number;
64
+ glowIntensity: number;
65
+ label: string;
66
+ labelOpacity: number;
67
+ scale: number;
68
+ /** Work intensity 0-1: sliding window of recent commits/PRs/messages with decay.
69
+ * Drives node sizing (geometry scale) and core brightness.
70
+ * 0 = idle, 1 = peak activity (≥25 weighted work units in window). */
71
+ workIntensity: number;
72
+ /** Cumulative work volume: count of commits + PRs (created + merged) up to current progress.
73
+ * Drives base node sizing — bigger nodes for agents who've shipped more.
74
+ * Unlike workIntensity (instantaneous, decayed), this only grows over time. */
75
+ totalWork: number;
76
+ /** Whether this agent is human (for distinct sizing) */
77
+ isHuman: boolean;
78
+ /** Agent role — drives role tag display below name */
79
+ role: 'human' | 'builder' | 'lead' | 'reviewer' | 'tester' | 'ideation' | 'researcher' | 'auditor' | 'challenger' | 'scout' | 'pm';
80
+ /** Core circle scale factor — folds in human multiplier, work growth,
81
+ * activity pulse, bounce scale, and layout scale. The renderer reads
82
+ * this directly for shader uniforms and label positioning.
83
+ * Formula: humanMul * (1 + growthFactor * MAX_WORK_GROWTH) * activityPulse * bounceScale * layoutScale */
84
+ coreScale: number;
85
+ /** Y offset from agent center for label positioning (negative = below).
86
+ * Accounts for core scale and label gap. Renderer sets label.position.y = this value. */
87
+ labelOffsetY: number;
88
+ }
89
+ interface ParticleState {
90
+ id: string;
91
+ type: 'message';
92
+ x: number;
93
+ y: number;
94
+ opacity: number;
95
+ size: number;
96
+ color: string;
97
+ }
98
+ /** Editorial narration — documentary storytelling voice.
99
+ * Separate from quote pills (QuotePillState). Displayed in the HTML NarrationLane.
100
+ * Phase title (colored) + editorial text. Milestones are primary, phases fill gaps. */
101
+ interface EditorialNarrationState {
102
+ phaseTitle: string;
103
+ phaseColor: string;
104
+ text: string;
105
+ opacity: number;
106
+ }
107
+ interface QuotePillState {
108
+ text: string;
109
+ speaker: string;
110
+ opacity: number;
111
+ /** Speaker's normalized X position (0-1). Renderer calls layout.agentToWorld()
112
+ * to convert to world space — same code path as AgentNode. */
113
+ speakerNormX: number;
114
+ /** Speaker's normalized Y position (0-1). Renderer calls layout.agentToWorld()
115
+ * to convert to world space — same code path as AgentNode. */
116
+ speakerNormY: number;
117
+ /** World-space Y offset above agent center (glow clearance + gap + pillHeight/2).
118
+ * Renderer positions pill at: agentToWorld(normX, normY).y + yOffset */
119
+ yOffset: number;
120
+ /** World-space X offset to keep pill within viewport bounds.
121
+ * 0 when pill fits centered on agent. Positive = shift right, negative = shift left.
122
+ * Renderer positions pill at: agentToWorld(normX, normY).x + xOffset */
123
+ xOffset: number;
124
+ /** Emoji prefix from curated quote (e.g. "🏗️") */
125
+ emoji?: string;
126
+ /** Mood tag from curated quote (good | bad | great | angry | surprise) */
127
+ mood?: string;
128
+ /** Text wrapped into lines by engine (word-wrap heuristic). */
129
+ lines: string[];
130
+ /** Horizontal padding in world units. Renderer: canvasWidth = measuredText + padX * 2 */
131
+ padX: number;
132
+ /** Vertical padding in world units. Renderer: canvasHeight = measuredTextHeight + padY * 2 */
133
+ padY: number;
134
+ /** Max pill width in world units — ceiling for canvas/geometry sizing. */
135
+ maxPillWidth: number;
136
+ /** Estimated actual pill width in world units — used for X clamping.
137
+ * Single-line: content width + padding. Multi-line: maxPillWidth. */
138
+ estimatedWidth: number;
139
+ /** Speaker display name (may differ from speaker id) */
140
+ speakerLabel: string;
141
+ /** Speaker color hex */
142
+ speakerColor: string;
143
+ /** Pill background color hex (darkened speaker color) */
144
+ pillBgColor: string;
145
+ }
146
+ interface PhaseState {
147
+ id: string;
148
+ name: string;
149
+ progress: number;
150
+ color: string;
151
+ }
152
+ interface CommitDot {
153
+ normalizedTime: number;
154
+ insertions: number;
155
+ deletions: number;
156
+ agent: string;
157
+ color: string;
158
+ commitType: string;
159
+ /** Log-scaled height (0-1). Largest commit = 1.0. Uses natural log for gentle compression. */
160
+ heightNorm: number;
161
+ }
162
+ interface PRBar {
163
+ prNumber: number;
164
+ openedNormalized: number;
165
+ mergedNormalized: number | null;
166
+ agent: string;
167
+ color: string;
168
+ additions: number;
169
+ deletions: number;
170
+ /** Row index from greedy packing (0 = top row). Computed by engine. */
171
+ row: number;
172
+ }
173
+ interface ArtifactBar {
174
+ normalizedTime: number;
175
+ type: string;
176
+ agent: string;
177
+ color: string;
178
+ /** Row index from greedy packing (0 = top row). Computed by engine. */
179
+ row: number;
180
+ }
181
+ interface EdgeState {
182
+ fromId: string;
183
+ toId: string;
184
+ strength: number;
185
+ color: string;
186
+ fromColor: string;
187
+ toColor: string;
188
+ }
189
+ interface MilestoneState {
190
+ normalizedTime: number;
191
+ label: string;
192
+ visible: boolean;
193
+ }
194
+ interface AllPhaseState {
195
+ id: string;
196
+ name: string;
197
+ startNormalized: number;
198
+ endNormalized: number;
199
+ color: string;
200
+ /** Label opacity: 0 when outside phase, fades in/out at boundaries (0.5% of timeline) */
201
+ labelOpacity: number;
202
+ }
203
+ interface FrameState {
204
+ projectTitle: string;
205
+ progress: number;
206
+ agents: AgentState[];
207
+ particles: ParticleState[];
208
+ quotePills: QuotePillState[];
209
+ editorialNarration: EditorialNarrationState | null;
210
+ phase: PhaseState;
211
+ stats: {
212
+ commitsToDate: number;
213
+ prsToDate: number;
214
+ messagesToDate: number;
215
+ activeAgents: number;
216
+ };
217
+ commitDots: CommitDot[];
218
+ prBars: PRBar[];
219
+ artifactBars: ArtifactBar[];
220
+ edges: EdgeState[];
221
+ visibleMilestones: MilestoneState[];
222
+ allPhases: AllPhaseState[];
223
+ trustLevel: number;
224
+ totalHours: number;
225
+ currentHours: number;
226
+ }
227
+
228
+ export type { AgentState as A, CommitDot as C, EdgeState as E, FrameState as F, LayoutContext as L, MilestoneState as M, PRBar as P, QuotePillState as Q, AllPhaseState as a, ArtifactBar as b, ParticleState as c, PhaseState as d };