miriad-viz 0.2.2 → 0.3.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.
@@ -207,19 +207,6 @@ function generateRetroPageData(dateRange, agents, rosterTypes) {
207
207
  ],
208
208
  _hint: "Categories: theGood, theBad, theUgly, theFunny (optional). Avoid placing two quotes from the same agent close together \u2014 interleave quotes from different agents."
209
209
  },
210
- trustArc: [
211
- {
212
- time: startISO,
213
- level: 50,
214
- label: "Starting trust level",
215
- _hint: "0 = no trust, 100 = full trust. Transform normalizes to 0-1."
216
- },
217
- {
218
- time: endISO,
219
- level: 80,
220
- label: "Final trust level"
221
- }
222
- ],
223
210
  editorialNarration: [
224
211
  {
225
212
  time: startISO,
@@ -236,17 +223,38 @@ function generateRetroPageData(dateRange, agents, rosterTypes) {
236
223
  text: "Replace with narration for the next phase",
237
224
  type: "milestone"
238
225
  }
239
- ]
226
+ ],
227
+ _narrationTimingWarning: "\u26A0\uFE0F narration-editorial.json uses FRACTIONAL 0-1 for start/end (e.g. 0.23, 0.68). Everything else in curation uses ISO timestamps. Do NOT mix them up."
240
228
  };
241
229
  }
242
- function generateRetroPageQuotes() {
230
+ function generateRetroPageQuotes(dateRange) {
231
+ const start = dateRange ? new Date(dateRange.earliest) : /* @__PURE__ */ new Date();
232
+ const end = dateRange ? new Date(dateRange.latest) : /* @__PURE__ */ new Date();
233
+ const mid = new Date(Math.round((start.getTime() + end.getTime()) / 2));
234
+ const fmt = (d) => {
235
+ const months = [
236
+ "Jan",
237
+ "Feb",
238
+ "Mar",
239
+ "Apr",
240
+ "May",
241
+ "Jun",
242
+ "Jul",
243
+ "Aug",
244
+ "Sep",
245
+ "Oct",
246
+ "Nov",
247
+ "Dec"
248
+ ];
249
+ return `${months[d.getUTCMonth()]} ${d.getUTCDate()} ${String(d.getUTCHours()).padStart(2, "0")}:${String(d.getUTCMinutes()).padStart(2, "0")}`;
250
+ };
243
251
  return {
244
- _comment: 'Additional quotes organized by category. Timestamps can be informal like "Feb 10 21:18". Avoid placing two quotes from the same agent close together \u2014 interleave different agents.',
252
+ _comment: 'Additional quotes organized by category. Timestamps can be informal (e.g. "Mar 2 14:30"). Avoid placing two quotes from the same agent close together \u2014 interleave different agents.',
245
253
  good: [
246
254
  {
247
255
  text: "Replace with a positive quote",
248
256
  author: "@agent-name",
249
- timestamp: "Feb 10 12:00",
257
+ timestamp: fmt(start),
250
258
  context: "What was happening when this was said"
251
259
  }
252
260
  ],
@@ -254,7 +262,7 @@ function generateRetroPageQuotes() {
254
262
  {
255
263
  text: "Replace with a quote about a challenge",
256
264
  author: "@agent-name",
257
- timestamp: "Feb 10 15:00",
265
+ timestamp: fmt(mid),
258
266
  context: "What went wrong"
259
267
  }
260
268
  ],
@@ -262,7 +270,7 @@ function generateRetroPageQuotes() {
262
270
  {
263
271
  text: "Replace with a quote about the hardest moment",
264
272
  author: "@agent-name",
265
- timestamp: "Feb 10 18:00",
273
+ timestamp: fmt(end),
266
274
  context: "The lowest point"
267
275
  }
268
276
  ]
@@ -398,7 +406,7 @@ async function runCurate(options) {
398
406
  const retroData = generateRetroPageData(dateRange, agents, rosterTypes);
399
407
  writeFileSync(retroDataPath, `${JSON.stringify(retroData, null, 2)}
400
408
  `);
401
- const retroQuotes = generateRetroPageQuotes();
409
+ const retroQuotes = generateRetroPageQuotes(dateRange);
402
410
  writeFileSync(retroQuotesPath, `${JSON.stringify(retroQuotes, null, 2)}
403
411
  `);
404
412
  const extractDateRange = {
@@ -412,7 +420,7 @@ async function runCurate(options) {
412
420
  console.log(`
413
421
  Generated curation scaffolds in ${progress.project.dataDir}/:
414
422
  `);
415
- console.log(" \u25CB retro-page-data.json \u2014 fill in phases, milestones, quotes, trust arc");
423
+ console.log(" \u25CB retro-page-data.json \u2014 fill in phases, milestones, quotes, narration");
416
424
  console.log(" \u25CB retro-page-quotes.json \u2014 add additional quotes");
417
425
  console.log(` \u2713 timeline-events.json \u2014 auto-generated (${eventCount} events)`);
418
426
  if (dateRange) {
package/dist-cli/index.js CHANGED
@@ -13,6 +13,23 @@ import { existsSync as existsSync3, readFileSync as readFileSync2, readdirSync }
13
13
  import { resolve as resolve3 } from "path";
14
14
 
15
15
  // src/cli/guided/data-summary.ts
16
+ function formatShortDate(d) {
17
+ const months = [
18
+ "Jan",
19
+ "Feb",
20
+ "Mar",
21
+ "Apr",
22
+ "May",
23
+ "Jun",
24
+ "Jul",
25
+ "Aug",
26
+ "Sep",
27
+ "Oct",
28
+ "Nov",
29
+ "Dec"
30
+ ];
31
+ return `${months[d.getUTCMonth()]} ${d.getUTCDate()} ${String(d.getUTCHours()).padStart(2, "0")}:${String(d.getUTCMinutes()).padStart(2, "0")}`;
32
+ }
16
33
  function buildDataSummary(data) {
17
34
  const lines = [];
18
35
  const parts = [];
@@ -58,8 +75,54 @@ function buildDataSummary(data) {
58
75
  lines.push("");
59
76
  lines.push(` \u{1F4CA} Data inventory: ${parts.join(", ")}`);
60
77
  }
78
+ const dateRange = getDateRangeFromData(data);
79
+ if (dateRange) {
80
+ lines.push(
81
+ ` \u{1F4C5} Date range: ${formatShortDate(dateRange.earliest)} \u2192 ${formatShortDate(dateRange.latest)}`
82
+ );
83
+ }
84
+ if (data.chatActivity?.agentTotals) {
85
+ const sorted = Object.entries(data.chatActivity.agentTotals).sort(([, a], [, b]) => b - a).slice(0, 5);
86
+ if (sorted.length > 0) {
87
+ const speakerList = sorted.map(([agent, count]) => `${agent} (${count})`).join(", ");
88
+ lines.push(` \u{1F5E3}\uFE0F Top speakers: ${speakerList}`);
89
+ }
90
+ }
91
+ if (data.chatActivity?.bursts && data.chatActivity.bursts.length > 0) {
92
+ const topBursts = data.chatActivity.bursts.sort((a, b) => b.messages - a.messages).slice(0, 3);
93
+ if (topBursts.length > 0) {
94
+ lines.push(" \u{1F525} Activity bursts:");
95
+ for (const burst of topBursts) {
96
+ const start = formatShortDate(new Date(burst.start));
97
+ lines.push(
98
+ ` ${start} \u2014 ${burst.messages} msgs${burst.label ? ` (${burst.label})` : ""}`
99
+ );
100
+ }
101
+ }
102
+ }
61
103
  return lines;
62
104
  }
105
+ function getDateRangeFromData(data) {
106
+ const allDates = [];
107
+ if (data.commits) {
108
+ for (const c of data.commits) {
109
+ const t = new Date(c.date).getTime();
110
+ if (!Number.isNaN(t)) allDates.push(t);
111
+ }
112
+ }
113
+ if (data.chatActivity?.coverage) {
114
+ const { start, end } = data.chatActivity.coverage;
115
+ const s = new Date(start).getTime();
116
+ const e = new Date(end).getTime();
117
+ if (!Number.isNaN(s)) allDates.push(s);
118
+ if (!Number.isNaN(e)) allDates.push(e);
119
+ }
120
+ if (allDates.length === 0) return null;
121
+ return {
122
+ earliest: new Date(Math.min(...allDates)),
123
+ latest: new Date(Math.max(...allDates))
124
+ };
125
+ }
63
126
 
64
127
  // src/cli/guided/docs.ts
65
128
  import { existsSync, readFileSync } from "fs";
@@ -121,13 +184,11 @@ function curationContext() {
121
184
  " \u2502 Each phase gets a name, color, and time range. \u2502",
122
185
  " \u2502 \u2502",
123
186
  " \u2502 Quotes \u2192 speech bubbles above agents in the viz. \u2502",
124
- " \u2502 Memorable lines shown as floating pills. \u2502",
187
+ " \u2502 Sequenced like a conversation \u2014 one at a time. \u2502",
125
188
  " \u2502 \u2502",
126
189
  " \u2502 Narration \u2192 text in the narration lane below the \u2502",
127
190
  " \u2502 timeline. Documentary-style storytelling. \u2502",
128
191
  " \u2502 \u2502",
129
- " \u2502 Trust arc \u2192 a trust bar that rises/falls over time. \u2502",
130
- " \u2502 \u2502",
131
192
  " \u2502 Journey: extract \u2192 curate \u2192 preview \u2192 iterate \u2192 video \u2502",
132
193
  " \u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256F"
133
194
  ];
@@ -140,8 +201,9 @@ function computeNext(progress, existingFiles, logTails, flags = {}, dataSummary,
140
201
  return { action: "all_done", progress, output: output2 };
141
202
  }
142
203
  const config = STEP_CONFIG[step];
143
- const stepPrefix = step === "extract" ? "\u26D4 FOLLOW THESE INSTRUCTIONS EXACTLY. This step requires human input BEFORE running any commands." : config.creative ? "\u26D4 FOLLOW THESE INSTRUCTIONS EXACTLY. This is a CREATIVE step \u2014 present results to the human and WAIT for their direction before proceeding." : "\u26D4 FOLLOW THESE INSTRUCTIONS EXACTLY. This is a mechanical step \u2014 run it, report the result, then call `next` again.";
144
- const output = [stepPrefix];
204
+ const output = [
205
+ config.creative ? "\u26D4 FOLLOW THESE INSTRUCTIONS EXACTLY. This is a CREATIVE step \u2014 present results to the human and WAIT for their direction before proceeding." : "\u26D4 FOLLOW THESE INSTRUCTIONS EXACTLY. This is a mechanical step \u2014 run it, report the result, then call `next` again."
206
+ ];
145
207
  const state = progress.steps[step];
146
208
  if (state.status === "error") {
147
209
  output.push("", `\u2717 ${config.label} failed: ${state.error || "unknown error"}`);
@@ -338,10 +400,13 @@ function computeNext(progress, existingFiles, logTails, flags = {}, dataSummary,
338
400
  output.push("", "\u270F\uFE0F Curation in progress");
339
401
  output.push(...curationContext());
340
402
  output.push("", " Edit the scaffold files in data/:");
341
- output.push(" \u25CB retro-page-data.json \u2014 phases, milestones, quotes, trust arc");
403
+ output.push(" \u25CB retro-page-data.json \u2014 phases, milestones, quotes");
342
404
  output.push(" \u25CB retro-page-quotes.json \u2014 additional quotes");
405
+ output.push(" \u25CB narration-editorial.json \u2014 editorial narration (fractional 0-1 timing)");
343
406
  output.push(" \u2713 timeline-events.json \u2014 auto-generated");
344
- output.push("", " When done, run 'npx miriad-viz next' again.");
407
+ output.push("");
408
+ output.push(" \u26A0\uFE0F Run `npx miriad-viz timeline` to check sequencing after every edit.");
409
+ output.push(" When timeline is clean, run 'npx miriad-viz next' to advance.");
345
410
  return { action: "waiting_for_edit", step, progress, output };
346
411
  }
347
412
  const outputsExist = config.outputFiles.some((f) => fileSet.has(f));
@@ -421,6 +486,62 @@ function computeNext(progress, existingFiles, logTails, flags = {}, dataSummary,
421
486
  if (step === "curate") {
422
487
  output.push("", `\u25B6 ${config.label}...`);
423
488
  output.push(...curationContext());
489
+ const story = progress.project.story;
490
+ if (story?.direction) {
491
+ output.push("");
492
+ output.push(" \u{1F4D6} Story direction from init:");
493
+ output.push(` Arc: ${story.direction}`);
494
+ if (story.focusPeriod) output.push(` Focus period: ${story.focusPeriod}`);
495
+ if (story.keyMoments?.length) {
496
+ output.push(` Key moments: ${story.keyMoments.join("; ")}`);
497
+ }
498
+ if (story.notes) output.push(` Notes: ${story.notes}`);
499
+ }
500
+ output.push("");
501
+ output.push(" \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550");
502
+ output.push(" CURATION METHODOLOGY \u2014 follow these phases in order:");
503
+ output.push(" \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550");
504
+ output.push("");
505
+ output.push(" Phase A \u2014 Present data findings (mechanical):");
506
+ output.push(" Read the extracted data summary. Reference the story direction.");
507
+ output.push(` "Your human asked for [arc]. Here's what the data shows..."`);
508
+ output.push("");
509
+ output.push(" Phase B \u2014 Mine the data (mechanical):");
510
+ output.push(" Search by each focus agent to understand their arc.");
511
+ output.push(' Search for emotional keywords: "fixed", "broken", "wtf",');
512
+ output.push(' "sorry", "actually", "wrong", all-caps, 3+ exclamation marks.');
513
+ output.push(" Search around turning point timestamps. Verify numbers.");
514
+ output.push("");
515
+ output.push(" Phase C \u2014 Show findings to human (CREATIVE STOP):");
516
+ output.push(' "Here are the 5 most dramatic moments I found: [moments]"');
517
+ output.push(' "Does this match your story? Should I adjust focus?"');
518
+ output.push(" WAIT for human feedback before proceeding.");
519
+ output.push("");
520
+ output.push(" Phase D \u2014 Propose structure + quotes (CREATIVE STOP):");
521
+ output.push(" Present proposed phases, milestones, candidate quotes.");
522
+ output.push(" Quotes sequenced as DIALOG \u2014 one pill at a time, no overlap.");
523
+ output.push(` "Here's the story I'd tell. Does this work?"`);
524
+ output.push(" WAIT for human feedback before writing files.");
525
+ output.push("");
526
+ output.push(" Phase E \u2014 Write curation files + validate:");
527
+ output.push(" Write retro-page-data.json, retro-page-quotes.json.");
528
+ output.push(" Run `npx miriad-viz timeline` to check sequencing.");
529
+ output.push(" Fix issues, run timeline again until clean.");
530
+ output.push("");
531
+ output.push(" \u26A0\uFE0F Run `npx miriad-viz timeline` BEFORE calling `next`.");
532
+ output.push(" It shows narration + quotes on a timeline with overlap/gap");
533
+ output.push(" warnings. Run it after every edit. When clean, run `next`.");
534
+ output.push("");
535
+ output.push(" \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550");
536
+ output.push(" SEQUENCING RULES \u2014 hard constraints:");
537
+ output.push(" \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550");
538
+ output.push("");
539
+ output.push(" \u2022 Same-agent quotes: NEVER overlap. Absolute.");
540
+ output.push(" \u2022 Cross-agent quotes: some overlap OK, but reads like");
541
+ output.push(" a conversation \u2014 one talks, then another responds.");
542
+ output.push(" \u2022 Narration: each segment finishes before next starts.");
543
+ output.push(" \u2022 \u26A0\uFE0F narration-editorial.json uses FRACTIONAL 0-1 for");
544
+ output.push(" start/end. Everything else uses ISO timestamps.");
424
545
  const curationDoc = readBundledDoc("miriad-viz-curation");
425
546
  if (curationDoc) {
426
547
  output.push("");
@@ -430,6 +551,26 @@ function computeNext(progress, existingFiles, logTails, flags = {}, dataSummary,
430
551
  }
431
552
  return { action: "run_inline", step, progress, output };
432
553
  }
554
+ if (step === "init") {
555
+ output.push("", `\u25B6 ${config.label}...`);
556
+ output.push("");
557
+ output.push(" After collecting repo, agents, and channel info, ask the human");
558
+ output.push(" about the STORY in natural conversation (not forms, not structured_ask):");
559
+ output.push("");
560
+ output.push(` "What's the story of this project? What's the arc?"`);
561
+ output.push(' "Any key turning points or moments to focus on?"');
562
+ output.push(' "What tone \u2014 dramatic, funny, documentary?"');
563
+ output.push(' "Any time range to focus on, or the full history?"');
564
+ output.push("");
565
+ output.push(" Write the answers to progress.json under project.story:");
566
+ output.push(" { direction, focusPeriod, keyMoments, notes }");
567
+ output.push(' All fields optional. If human says "just show me what happened":');
568
+ output.push(' story.direction = "exploratory \u2014 discover the story from the data"');
569
+ output.push("");
570
+ output.push(" The story direction drives everything downstream \u2014 extract scope,");
571
+ output.push(" curation focus, what the agent mines for in the data.");
572
+ return { action: "run_inline", step, progress, output };
573
+ }
433
574
  if (config.mode === "inline") {
434
575
  output.push("", `\u25B6 ${config.label}...`);
435
576
  return { action: "run_inline", step, progress, output };
@@ -440,24 +581,20 @@ function computeNext(progress, existingFiles, logTails, flags = {}, dataSummary,
440
581
  if (step === "extract") {
441
582
  output.push("");
442
583
  output.push(" \u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510");
443
- output.push(" \u2502 \u26D4 BEFORE RUNNING ANYTHING \u2014 Talk to the human first \u2502");
444
- output.push(" \u2502 \u2502");
445
- output.push(" \u2502 1. Explain what miriad-viz does (animated timeline \u2502");
446
- output.push(" \u2502 visualization of channel activity \u2014 commits, PRs, \u2502");
447
- output.push(" \u2502 messages, agent interactions rendered as video) \u2502");
448
- output.push(" \u2502 \u2502");
449
- output.push(" \u2502 2. Ask about scope: \u2502");
450
- output.push(' \u2502 "Which time period? Full channel history, or a \u2502');
451
- output.push(' \u2502 specific sprint/week/date range?" \u2502');
584
+ output.push(" \u2502 FIRST: Explain to the human what you're about to do \u2502");
452
585
  output.push(" \u2502 \u2502");
453
- output.push(" \u2502 3. Ask about data sources: \u2502");
454
- output.push(' \u2502 "Do you have a git repo for this project? \u2502');
455
- output.push(' \u2502 Where is the chat data?" \u2502');
586
+ output.push(" \u2502 miriad-viz builds an animated timeline visualization \u2502");
587
+ output.push(" \u2502 of project activity \u2014 commits, PRs, messages, and \u2502");
588
+ output.push(" \u2502 agent interactions rendered as a video. \u2502");
456
589
  output.push(" \u2502 \u2502");
457
- output.push(" \u2502 4. Confirm the plan before extracting. \u2502");
590
+ output.push(" \u2502 The pipeline: \u2502");
591
+ output.push(" \u2502 1. Extract \u2014 pull raw data (git, chat, artifacts) \u2502");
592
+ output.push(" \u2502 2. Curate \u2014 editorial content (phases, quotes) \u2502");
593
+ output.push(" \u2502 3. Preview \u2014 interactive browser preview \u2502");
594
+ output.push(" \u2502 4. Audio \u2014 narration, music, SFX \u2502");
595
+ output.push(" \u2502 5. Render \u2014 final MP4 video \u2502");
458
596
  output.push(" \u2502 \u2502");
459
- output.push(" \u2502 Only run the extraction command AFTER the human \u2502");
460
- output.push(" \u2502 confirms the scope and data sources. \u2502");
597
+ output.push(" \u2502 Then proceed with extraction. \u2502");
461
598
  output.push(" \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518");
462
599
  const rulesDoc = readBundledDoc("miriad-viz-rules");
463
600
  if (rulesDoc) {
@@ -493,6 +630,12 @@ function computeNext(progress, existingFiles, logTails, flags = {}, dataSummary,
493
630
  }
494
631
  output.push("");
495
632
  }
633
+ const focusPeriod = progress.project.story?.focusPeriod;
634
+ if (focusPeriod) {
635
+ output.push(` \u{1F4C5} Story focus period: ${focusPeriod}`);
636
+ output.push(" Consider using --from/--to flags to scope extraction.");
637
+ output.push("");
638
+ }
496
639
  if (fileSet.has("extract-channel.sh")) {
497
640
  output.push(" \u{1F4D6} Extraction script found. Run it in background:");
498
641
  output.push(" nohup ./extract-channel.sh ./raw-data &> chat-extract.log &");
@@ -669,6 +812,7 @@ Commands:
669
812
  transform Run transform pipeline
670
813
  preview Launch the viewer
671
814
  render Export video via Remotion
815
+ timeline Check curation sequencing (narrative linter)
672
816
  init-viewer <dir> Scaffold a viewer project
673
817
  init-remotion <dir> Scaffold a Remotion video project
674
818
 
@@ -798,7 +942,7 @@ async function runNext(flags) {
798
942
  if (result.action === "run_inline" && result.step) {
799
943
  switch (result.step) {
800
944
  case "curate": {
801
- const { runCurate } = await import("./curate-KLE2K3TB.js");
945
+ const { runCurate } = await import("./curate-EWAMDCOW.js");
802
946
  await runCurate({ projectDir, progress: result.progress });
803
947
  writeProgress(projectDir, result.progress);
804
948
  if (result.progress.steps.curate.status === "complete") {
@@ -873,7 +1017,7 @@ async function main() {
873
1017
  }
874
1018
  case "curate": {
875
1019
  const { projectDir, progress } = requireProject();
876
- const { runCurate } = await import("./curate-KLE2K3TB.js");
1020
+ const { runCurate } = await import("./curate-EWAMDCOW.js");
877
1021
  await runCurate({ projectDir, progress });
878
1022
  break;
879
1023
  }
@@ -904,6 +1048,13 @@ async function main() {
904
1048
  await runRender({ projectDir, progress, draft: flags.draft === true });
905
1049
  break;
906
1050
  }
1051
+ case "timeline": {
1052
+ const { projectDir, progress } = requireProject();
1053
+ const { runTimeline } = await import("./timeline-EDSW3EWB.js");
1054
+ const exitCode = runTimeline(projectDir, progress);
1055
+ if (exitCode !== 0) process.exitCode = exitCode;
1056
+ break;
1057
+ }
907
1058
  case "init-viewer":
908
1059
  case "init-remotion": {
909
1060
  const templateName = command === "init-viewer" ? "viewer" : "remotion";
@@ -0,0 +1,335 @@
1
+ // src/cli/guided/steps/timeline.ts
2
+ import { existsSync, readFileSync } from "fs";
3
+ import { resolve } from "path";
4
+
5
+ // src/cli/guided/validate-curation.ts
6
+ function parseTimestamp(timeStr, referenceYear) {
7
+ if (!timeStr) return null;
8
+ const isoDate = new Date(timeStr);
9
+ if (!Number.isNaN(isoDate.getTime()) && timeStr.includes("-")) {
10
+ return isoDate.getTime();
11
+ }
12
+ const months = {
13
+ jan: 0,
14
+ feb: 1,
15
+ mar: 2,
16
+ apr: 3,
17
+ may: 4,
18
+ jun: 5,
19
+ jul: 6,
20
+ aug: 7,
21
+ sep: 8,
22
+ oct: 9,
23
+ nov: 10,
24
+ dec: 11
25
+ };
26
+ const match = timeStr.match(/^(\w{3})\s+(\d{1,2})(?:\s+(\d{1,2}):(\d{2}))?/i);
27
+ if (match) {
28
+ const month = months[match[1].toLowerCase()];
29
+ if (month !== void 0) {
30
+ const day = Number.parseInt(match[2], 10);
31
+ const hour = match[3] ? Number.parseInt(match[3], 10) : 12;
32
+ const minute = match[4] ? Number.parseInt(match[4], 10) : 0;
33
+ const year = referenceYear ?? (/* @__PURE__ */ new Date()).getFullYear();
34
+ return new Date(year, month, day, hour, minute).getTime();
35
+ }
36
+ }
37
+ return null;
38
+ }
39
+ function resolveQuotes(input, startMs, durationMs) {
40
+ const quotes = [];
41
+ const referenceYear = new Date(startMs).getFullYear();
42
+ const rpd = input.retroPageData?.quotes;
43
+ if (rpd) {
44
+ for (const category of [rpd.theGood, rpd.theBad, rpd.theUgly, rpd.theFunny]) {
45
+ if (!category) continue;
46
+ for (const q of category) {
47
+ const ms = parseTimestamp(q.time, referenceYear);
48
+ if (ms !== null && durationMs > 0) {
49
+ quotes.push({
50
+ author: q.author.replace(/^@/, ""),
51
+ text: q.text,
52
+ normalizedTime: (ms - startMs) / durationMs
53
+ });
54
+ }
55
+ }
56
+ }
57
+ }
58
+ const rpq = input.retroPageQuotes;
59
+ if (rpq) {
60
+ for (const category of [rpq.good, rpq.bad, rpq.ugly]) {
61
+ if (!category) continue;
62
+ for (const q of category) {
63
+ const ms = parseTimestamp(q.timestamp, referenceYear);
64
+ if (ms !== null && durationMs > 0) {
65
+ quotes.push({
66
+ author: q.author.replace(/^@/, ""),
67
+ text: q.timestamp,
68
+ // Use timestamp as identifier
69
+ normalizedTime: (ms - startMs) / durationMs
70
+ });
71
+ }
72
+ }
73
+ }
74
+ }
75
+ return quotes.sort((a, b) => a.normalizedTime - b.normalizedTime);
76
+ }
77
+ function resolveNarration(input, startMs, durationMs) {
78
+ const entries = input.retroPageData?.editorialNarration;
79
+ if (!entries || entries.length === 0) return [];
80
+ const result = [];
81
+ for (const entry of entries) {
82
+ const ms = parseTimestamp(entry.time);
83
+ if (ms !== null && durationMs > 0) {
84
+ result.push({
85
+ text: entry.text,
86
+ phaseTitle: entry.phaseTitle ?? "",
87
+ normalizedTime: (ms - startMs) / durationMs
88
+ });
89
+ }
90
+ }
91
+ return result.sort((a, b) => a.normalizedTime - b.normalizedTime);
92
+ }
93
+ function computePillWindow(totalHours) {
94
+ const SIM_HOURS_PER_SECOND = 0.6;
95
+ const MIN_PILL_SECONDS = 5;
96
+ const playbackSeconds = totalHours / SIM_HOURS_PER_SECOND;
97
+ if (playbackSeconds <= 0) return 0.04;
98
+ return MIN_PILL_SECONDS / playbackSeconds;
99
+ }
100
+ function getTimeRange(input) {
101
+ const meta = input.retroPageData?.meta;
102
+ if (!meta?.startDate || !meta?.endDate) return null;
103
+ const startMs = new Date(meta.startDate).getTime();
104
+ const endMs = new Date(meta.endDate).getTime();
105
+ if (Number.isNaN(startMs) || Number.isNaN(endMs)) return null;
106
+ const durationMs = endMs - startMs;
107
+ const totalHours = meta.totalHours ?? durationMs / 36e5;
108
+ return { startMs, endMs, durationMs, totalHours };
109
+ }
110
+ function validateCurationFiles(input) {
111
+ const errors = [];
112
+ const warnings = [];
113
+ const range = getTimeRange(input);
114
+ if (!range) {
115
+ errors.push("Missing or invalid meta.startDate/endDate \u2014 cannot validate timing.");
116
+ return { valid: false, errors, warnings };
117
+ }
118
+ const { startMs, durationMs, totalHours } = range;
119
+ const pillWindow = computePillWindow(totalHours);
120
+ const quotes = resolveQuotes(input, startMs, durationMs);
121
+ const narration = resolveNarration(input, startMs, durationMs);
122
+ const byAgent = /* @__PURE__ */ new Map();
123
+ for (const q of quotes) {
124
+ let group = byAgent.get(q.author);
125
+ if (!group) {
126
+ group = [];
127
+ byAgent.set(q.author, group);
128
+ }
129
+ group.push(q);
130
+ }
131
+ for (const [agent, agentQuotes] of byAgent) {
132
+ for (let i = 0; i < agentQuotes.length - 1; i++) {
133
+ const current = agentQuotes[i];
134
+ const next = agentQuotes[i + 1];
135
+ const gap = next.normalizedTime - current.normalizedTime;
136
+ if (gap < pillWindow) {
137
+ const currentPct = (current.normalizedTime * 100).toFixed(1);
138
+ const nextPct = (next.normalizedTime * 100).toFixed(1);
139
+ errors.push(
140
+ `Same-agent overlap: @${agent} has quotes at ${currentPct}% and ${nextPct}% (gap ${(gap * 100).toFixed(1)}% < pill window ${(pillWindow * 100).toFixed(1)}%)`
141
+ );
142
+ }
143
+ }
144
+ }
145
+ for (let i = 0; i < narration.length - 1; i++) {
146
+ const current = narration[i];
147
+ const next = narration[i + 1];
148
+ const gap = next.normalizedTime - current.normalizedTime;
149
+ if (gap < pillWindow) {
150
+ const currentPct = (current.normalizedTime * 100).toFixed(1);
151
+ const nextPct = (next.normalizedTime * 100).toFixed(1);
152
+ errors.push(
153
+ `Narration overlap: entries at ${currentPct}% and ${nextPct}% (gap ${(gap * 100).toFixed(1)}% < window ${(pillWindow * 100).toFixed(1)}%)`
154
+ );
155
+ }
156
+ }
157
+ for (let i = 0; i < quotes.length - 1; i++) {
158
+ const current = quotes[i];
159
+ const next = quotes[i + 1];
160
+ if (current.author === next.author) continue;
161
+ const gap = next.normalizedTime - current.normalizedTime;
162
+ if (gap < pillWindow * 0.5) {
163
+ const currentPct = (current.normalizedTime * 100).toFixed(1);
164
+ const nextPct = (next.normalizedTime * 100).toFixed(1);
165
+ warnings.push(
166
+ `Cross-agent overlap: @${current.author} at ${currentPct}% and @${next.author} at ${nextPct}% are very close (gap ${(gap * 100).toFixed(1)}%)`
167
+ );
168
+ }
169
+ }
170
+ const teamAgents = new Set(
171
+ (input.retroPageData?.teamAssembly ?? []).map((a) => a.agent.replace(/^@/, ""))
172
+ );
173
+ if (teamAgents.size > 0) {
174
+ const quoteAgents = new Set(quotes.map((q) => q.author));
175
+ for (const agent of quoteAgents) {
176
+ if (!teamAgents.has(agent)) {
177
+ warnings.push(`Quote references @${agent} who is not in teamAssembly.`);
178
+ }
179
+ }
180
+ }
181
+ const allEvents = [
182
+ ...quotes.map((q) => q.normalizedTime),
183
+ ...narration.map((n) => n.normalizedTime)
184
+ ].sort((a, b) => a - b);
185
+ const GAP_THRESHOLD = 0.2;
186
+ if (allEvents.length > 0) {
187
+ if (allEvents[0] > GAP_THRESHOLD) {
188
+ warnings.push(
189
+ `Gap at start: no content until ${(allEvents[0] * 100).toFixed(0)}% of timeline.`
190
+ );
191
+ }
192
+ for (let i = 0; i < allEvents.length - 1; i++) {
193
+ const gap = allEvents[i + 1] - allEvents[i];
194
+ if (gap > GAP_THRESHOLD) {
195
+ const startPct = (allEvents[i] * 100).toFixed(0);
196
+ const endPct = (allEvents[i + 1] * 100).toFixed(0);
197
+ warnings.push(`Gap: no content from ${startPct}% to ${endPct}% of timeline.`);
198
+ }
199
+ }
200
+ const last = allEvents[allEvents.length - 1];
201
+ if (1 - last > GAP_THRESHOLD) {
202
+ warnings.push(`Gap at end: no content after ${(last * 100).toFixed(0)}% of timeline.`);
203
+ }
204
+ }
205
+ return {
206
+ valid: errors.length === 0,
207
+ errors,
208
+ warnings
209
+ };
210
+ }
211
+ function printTimeline(input) {
212
+ const range = getTimeRange(input);
213
+ if (!range) {
214
+ return "Cannot render timeline: missing meta.startDate/endDate.";
215
+ }
216
+ const { startMs, durationMs, totalHours } = range;
217
+ const pillWindow = computePillWindow(totalHours);
218
+ const quotes = resolveQuotes(input, startMs, durationMs);
219
+ const narration = resolveNarration(input, startMs, durationMs);
220
+ const lines = [];
221
+ lines.push(
222
+ `Timeline (${totalHours.toFixed(1)}h, pill window: ${(pillWindow * 100).toFixed(1)}%):`
223
+ );
224
+ lines.push("");
225
+ const events = [];
226
+ for (const q of quotes) {
227
+ const truncated = q.text.length > 40 ? `${q.text.slice(0, 37)}...` : q.text;
228
+ events.push({
229
+ normalizedTime: q.normalizedTime,
230
+ type: "quote",
231
+ label: `@${q.author}: "${truncated}"`,
232
+ agent: q.author
233
+ });
234
+ }
235
+ for (const n of narration) {
236
+ const truncated = n.text.length > 40 ? `${n.text.slice(0, 37)}...` : n.text;
237
+ const prefix = n.phaseTitle ? `[${n.phaseTitle}] ` : "";
238
+ events.push({
239
+ normalizedTime: n.normalizedTime,
240
+ type: "narration",
241
+ label: `${prefix}"${truncated}"`
242
+ });
243
+ }
244
+ events.sort((a, b) => a.normalizedTime - b.normalizedTime);
245
+ let lastEnd = 0;
246
+ const GAP_THRESHOLD = 0.2;
247
+ for (const event of events) {
248
+ const pos = event.normalizedTime;
249
+ if (pos - lastEnd > GAP_THRESHOLD) {
250
+ const gapStart = (lastEnd * 100).toFixed(0);
251
+ const gapEnd = (pos * 100).toFixed(0);
252
+ lines.push(` \u26A0\uFE0F GAP: ${gapStart}%-${gapEnd}% \u2014 no narration or quotes`);
253
+ }
254
+ const pct = (pos * 100).toFixed(1);
255
+ if (event.type === "narration") {
256
+ const endPct = ((pos + pillWindow) * 100).toFixed(1);
257
+ lines.push(`${pct.padStart(5)}%-${endPct.padStart(5)}% [narration] ${event.label}`);
258
+ } else {
259
+ lines.push(`${pct.padStart(5)}% ${event.label}`);
260
+ }
261
+ lastEnd = pos + pillWindow;
262
+ }
263
+ if (events.length > 0 && 1 - lastEnd > GAP_THRESHOLD) {
264
+ const gapStart = (lastEnd * 100).toFixed(0);
265
+ lines.push(` \u26A0\uFE0F GAP: ${gapStart}%-100% \u2014 no narration or quotes`);
266
+ }
267
+ lines.push("");
268
+ lines.push(`Summary: ${quotes.length} quotes, ${narration.length} narration entries`);
269
+ const validation = validateCurationFiles(input);
270
+ if (validation.errors.length > 0) {
271
+ lines.push("");
272
+ lines.push("ERRORS:");
273
+ for (const err of validation.errors) {
274
+ lines.push(` \u274C ${err}`);
275
+ }
276
+ }
277
+ if (validation.warnings.length > 0) {
278
+ lines.push("");
279
+ lines.push("WARNINGS:");
280
+ for (const warn of validation.warnings) {
281
+ lines.push(` \u26A0\uFE0F ${warn}`);
282
+ }
283
+ }
284
+ if (validation.valid && validation.warnings.length === 0) {
285
+ lines.push("");
286
+ lines.push("\u2705 No issues found.");
287
+ }
288
+ return lines.join("\n");
289
+ }
290
+
291
+ // src/cli/guided/steps/timeline.ts
292
+ function readCurationFiles(dataDir) {
293
+ const input = {};
294
+ const retroDataPath = resolve(dataDir, "retro-page-data.json");
295
+ if (existsSync(retroDataPath)) {
296
+ try {
297
+ input.retroPageData = JSON.parse(readFileSync(retroDataPath, "utf-8"));
298
+ } catch {
299
+ console.error(` \u26A0\uFE0F Could not parse ${retroDataPath}`);
300
+ }
301
+ }
302
+ const retroQuotesPath = resolve(dataDir, "retro-page-quotes.json");
303
+ if (existsSync(retroQuotesPath)) {
304
+ try {
305
+ input.retroPageQuotes = JSON.parse(readFileSync(retroQuotesPath, "utf-8"));
306
+ } catch {
307
+ console.error(` \u26A0\uFE0F Could not parse ${retroQuotesPath}`);
308
+ }
309
+ }
310
+ return input;
311
+ }
312
+ function runTimeline(projectDir, progress) {
313
+ const dataDir = resolve(projectDir, progress.project.dataDir);
314
+ if (!existsSync(dataDir)) {
315
+ console.error(`
316
+ \u2717 Data directory not found: ${dataDir}`);
317
+ console.error(" Run extraction first: npx miriad-viz next");
318
+ return 1;
319
+ }
320
+ const retroDataPath = resolve(dataDir, "retro-page-data.json");
321
+ if (!existsSync(retroDataPath)) {
322
+ console.error("\n\u2717 No curation files found.");
323
+ console.error(" Run curation first: npx miriad-viz curate");
324
+ return 1;
325
+ }
326
+ const input = readCurationFiles(dataDir);
327
+ const timeline = printTimeline(input);
328
+ console.log("");
329
+ console.log(timeline);
330
+ const validation = validateCurationFiles(input);
331
+ return validation.valid ? 0 : 1;
332
+ }
333
+ export {
334
+ runTimeline
335
+ };
@@ -7,7 +7,7 @@ How to author the editorial layer that turns raw data into a story.
7
7
  miriad-viz separates data into two streams:
8
8
 
9
9
  - **RawData** — automated extraction. Commits, PRs, messages, agent joins. Deterministic: given the same channel, you always get the same output.
10
- - **CurationData** — editorial content. Phases, milestones, quotes, trust arc, and editorial narration. Requires human judgment: what mattered, when the story turned, which words captured the moment, and how to tell the story.
10
+ - **CurationData** — editorial content. Phases, milestones, quotes, and editorial narration. Requires human judgment: what mattered, when the story turned, which words captured the moment, and how to tell the story.
11
11
 
12
12
  RawData tells you *what happened*. CurationData tells you *what it meant*.
13
13
 
@@ -47,12 +47,120 @@ The process:
47
47
  2. Form hypotheses about the story arc before picking quotes
48
48
  3. Verify every quote verbatim against the original message (timestamp, exact wording, speaker)
49
49
  4. Organize quotes by tone and arc position (good/bad/ugly, or by phase)
50
- 5. Author the trust arc as a series of keyframes with human-readable labels
51
- 6. Define phase boundaries at narrative turning points, not arbitrary time slices
52
- 7. Iterate with the team — they'll catch misattributions and add context you missed
50
+ 5. Define phase boundaries at narrative turning points, not arbitrary time slices
51
+ 6. Iterate with the team they'll catch misattributions and add context you missed
53
52
 
54
53
  **When to use:** External-facing content, published retrospectives, video narration. When accuracy and narrative quality both matter.
55
54
 
55
+ ---
56
+
57
+ ## Mining Methodology
58
+
59
+ Build the arc first, search for quotes second. Don't browse messages hoping to find a story. Understand what happened, then find the evidence.
60
+
61
+ ### Phase A — Build the Skeleton
62
+
63
+ Before touching any quotes, understand the story structure:
64
+
65
+ 1. **Read message history chronologically in chunks.** Don't try to read everything — filter strategically.
66
+ 2. **Filter by key players** to understand each person's journey through the project:
67
+ - Filter by the human to see every intervention
68
+ - Filter by leads for technical decisions
69
+ - Filter by testers/reviewers for QA moments
70
+ 3. **Build a timeline of turning points** — not every message, just the moments where the direction changed.
71
+ 4. **Form your hypothesis:** What's the arc? Setup → crisis → recovery? Exploration → convergence → polish? Name the acts before you search for quotes.
72
+
73
+ ### Phase B — Find the Beats (Targeted Search)
74
+
75
+ Once you know the story beats, search for specific moments. **Search by emotion and turning points, not by topic.**
76
+
77
+ Effective search terms:
78
+ - `"fixed"` — finds every time someone claimed something was fixed (reveals fix loops)
79
+ - `"broken"`, `"wrong"`, `"wtf"` — finds frustration peaks
80
+ - `"sorry"`, `"actually"` — finds corrections and ownership moments
81
+ - `"never"`, `"always"`, `"stop"`, `"please"` — finds intensity
82
+ - All-caps messages, 3+ exclamation marks — finds emotional peaks
83
+ - `"screenshot"`, `"vision"`, `"prove"` — finds methodology shifts
84
+
85
+ **The technique:** The good quotes are AT the turning points. Search for the moment, the quote comes with it.
86
+
87
+ ### Phase C — Show Findings to the Human
88
+
89
+ Present your top 5-10 most dramatic moments with actual quotes. Ask:
90
+ - "Does this match your story? Should I adjust focus?"
91
+ - "Are there moments I'm missing?"
92
+
93
+ **WAIT for the human's refinement before proposing structure.**
94
+
95
+ ### Phase D — Propose Structure + Quotes
96
+
97
+ Present proposed phases, milestones, and candidate quotes. The human drives editorial vision — what's funny, what's honest, what structure serves the story. The agent does legwork.
98
+
99
+ **WAIT for human feedback before writing files.**
100
+
101
+ ### Phase E — Write + Validate + Iterate
102
+
103
+ Write the curation files, then validate:
104
+
105
+ ```bash
106
+ npx miriad-viz timeline
107
+ ```
108
+
109
+ This shows your narration + quotes on a timeline with overlap/gap warnings. Run it after every edit. When clean, run `next` to advance.
110
+
111
+ ### Verify Every Number
112
+
113
+ If a number appears in your curation, you must be able to point to the message or commit that proves it. Verify before writing, not after.
114
+
115
+ - Test counts → search messages for test count claims
116
+ - Lines deleted → check git log for the specific PR
117
+ - PR counts → list every PR in the range
118
+ - QA misses → find the specific "confirmed fixed" / "looks correct" messages
119
+
120
+ ---
121
+
122
+ ## Sequencing Rules (Conversation Model)
123
+
124
+ Everything is sequenced like a conversation. No talking over each other.
125
+
126
+ ### Chat Pills (Quotes)
127
+
128
+ - **Same agent: NEVER overlap.** Absolute rule. Two quotes from the same speaker must not be visible at the same time.
129
+ - **Different agents: some overlap OK**, but should read like a conversation — one person talks, then another responds. Not two full pills stacked at the exact same time.
130
+ - Think dialog turns or group chat: quick sequential bursts with pauses.
131
+ - If original data has simultaneous messages, curation staggers them.
132
+
133
+ ### Narration Lines
134
+
135
+ - Each narration segment finishes before the next starts. Sequenced, no overlap.
136
+ - Like a narrator reading a script — one line at a time.
137
+
138
+ ### Validation
139
+
140
+ Use `npx miriad-viz timeline` to check sequencing. The validator checks:
141
+
142
+ 1. **Same-agent quote overlap → REJECT** (hard error, must fix)
143
+ 2. **Narration segment overlap → REJECT** (hard error, must fix)
144
+ 3. **Cross-agent quote overlap → WARN** (some OK, excessive is bad)
145
+
146
+ ---
147
+
148
+ ## ⚠️ Narration Timing Format Warning
149
+
150
+ **`narration-editorial.json` uses FRACTIONAL 0-1 for `start`/`end`.** Everything else in curation uses ISO timestamps.
151
+
152
+ ```json
153
+ // narration-editorial.json — FRACTIONAL (0-1)
154
+ { "start": 0.23, "end": 0.68, "l1": "THE CRISIS", ... }
155
+
156
+ // retro-page-data.json — ISO TIMESTAMPS
157
+ { "time": "2026-03-01T10:00:00+01:00", "label": "💡 Kickoff", ... }
158
+ ```
159
+
160
+ Do NOT mix them up. If you put ISO timestamps in narration-editorial.json, the narration will not display. If you put fractions in retro-page-data.json, the phases will be wrong.
161
+
162
+ ---
163
+
56
164
  ## CurationData Contract
57
165
 
58
166
  The `CurationData` interface (defined in `src/types/raw-data.ts`):
@@ -126,25 +234,6 @@ Milestones are the timeline's signposts. They appear as vertical markers with la
126
234
  - Emoji prefixes work well for visual scanning: 💡 🔧 💔 ✅ 🏁
127
235
  - Space them so labels don't visually overlap. The renderer places labels at the milestone's timestamp — two milestones within minutes of each other will collide. If events are that close, pick the more important one.
128
236
 
129
- ### Trust Keyframes
130
-
131
- ```typescript
132
- interface CurationTrustKeyframe {
133
- timestamp: number; // unix ms
134
- level: number; // 0-1 (0 = no trust, 1 = full trust)
135
- label: string; // human-readable, e.g. "Trust hits rock bottom"
136
- }
137
- ```
138
-
139
- The trust arc is the emotional throughline. It tracks the human's confidence in the AI team over time. The engine interpolates between keyframes using smoothstep, so you only need to define the inflection points.
140
-
141
- **Guidelines:**
142
- - This is the most editorial piece of curation. It's a subjective judgment call — there's no algorithm that can extract "trust level" from chat messages.
143
- - Place keyframes at moments where trust visibly changed: a promise broken, a recovery delivered, a surprise failure, an unqualified praise.
144
- - 10-20 keyframes for a multi-day project. The smoothstep interpolation handles the gaps.
145
- - The renderer maps trust to color: red (0-0.3) → amber (0.3-0.7) → cyan (0.7-1.0). Design your arc knowing these thresholds.
146
- - Labels should be readable standalone — someone scanning the trust arc data should understand the story without reading the full history.
147
-
148
237
  ## Reasoning About Timing
149
238
 
150
239
  Your timeline has a total duration. The engine maps that to a progress bar (0→1). Every visual element — pills, narration, milestones — occupies a fraction of that bar. To make good curation decisions, you need to reason about how your content maps to screen time.
@@ -197,6 +286,8 @@ The queue logic: milestones are sorted by time and always take priority. Phase n
197
286
 
198
287
  Editorial narration lives in a dedicated file: `narration-editorial.json`
199
288
 
289
+ ⚠️ **This file uses FRACTIONAL 0-1 for `start`/`end`, NOT ISO timestamps.** See [Narration Timing Format Warning](#️-narration-timing-format-warning) above.
290
+
200
291
  ```json
201
292
  [
202
293
  {
@@ -275,7 +366,7 @@ The CLI expects curation data as JSON files in the data directory:
275
366
 
276
367
  ### `retro-page-data.json`
277
368
 
278
- The primary curation file. Contains phases, milestones, quotes, trust arc, team assembly, and project metadata.
369
+ The primary curation file. Contains phases, milestones, quotes, team assembly, and project metadata.
279
370
 
280
371
  ```json
281
372
  {
@@ -325,20 +416,12 @@ The primary curation file. Contains phases, milestones, quotes, trust arc, team
325
416
  "theBad": [],
326
417
  "theUgly": [],
327
418
  "theFunny": []
328
- },
329
- "trustArc": [
330
- {
331
- "time": "2026-03-01T10:00:00",
332
- "level": 80,
333
- "label": "Excited kickoff"
334
- }
335
- ]
419
+ }
336
420
  }
337
421
  ```
338
422
 
339
423
  **Notes:**
340
424
  - `quotes.time` uses informal format ("Mar 2 14:30") — the transform layer parses this. ISO 8601 also works.
341
- - `trustArc.level` is 0-100 in the source file. The transform normalizes to 0-1.
342
425
  - `phases.color` and `phases.prRange` are informational — the processing layer assigns its own colors and doesn't use PR ranges.
343
426
  - `teamAssembly` feeds into RawData agent joins, not CurationData directly.
344
427
 
@@ -367,6 +450,8 @@ Both files are merged by `transformToCurationData()` in the transform layer. Quo
367
450
 
368
451
  Editorial narration for the narration lane. See the [Editorial Narration](#editorial-narration) section above for the full format specification and writing guide.
369
452
 
453
+ ⚠️ **Uses FRACTIONAL 0-1 for `start`/`end`, NOT ISO timestamps.**
454
+
370
455
  ```json
371
456
  [
372
457
  {
@@ -388,15 +473,13 @@ This file is optional. Without it, the narration lane will display quote text (f
388
473
 
389
474
  Run extraction to produce the automated data files (`git-commits.json`, `pr-details.json`, `chat-activity.json`, etc.). You need this context before you can curate — the raw data tells you the timeline, the players, and the activity patterns.
390
475
 
391
- ### Step 2: Read the History
476
+ ### Step 2: Mine the History
392
477
 
393
- Read the channel's message history. For Tier 2, an agent can do this via MCP tools (`get_messages` with pagination). For Tier 3, a human reads alongside.
394
-
395
- Look for:
396
- - **Turning points** where did the project's direction change? These become phase boundaries.
397
- - **Emotional peaks** moments of frustration, celebration, surprise, disappointment. These become trust keyframes.
398
- - **Memorable quotes** — words that capture a moment better than any summary could. These become quotes.
399
- - **Concrete milestones** — first working prototype, first test passing, first deployment, first user feedback. These become milestones.
478
+ Follow the [Mining Methodology](#mining-methodology) above:
479
+ 1. Build the skeleton — filter by key players, identify turning points
480
+ 2. Search by emotion — `"fixed"`, `"broken"`, `"sorry"`, `"actually"`, all-caps
481
+ 3. Show findings to the human top 5-10 dramatic moments with quotes
482
+ 4. Get human refinement before proposing structure
400
483
 
401
484
  ### Step 3: Define Phases
402
485
 
@@ -426,13 +509,7 @@ Search for quotes that:
426
509
  - [ ] Speaker attribution is correct
427
510
  - [ ] Phase assignment makes sense
428
511
 
429
- ### Step 6: Author the Trust Arc
430
-
431
- Plot the human's confidence over time. This is subjective — embrace that. The trust arc is editorial, not data.
432
-
433
- Start at the beginning: how confident was the human when the project kicked off? Walk through each phase and ask: did trust go up or down here? By how much? Place a keyframe at each inflection point.
434
-
435
- ### Step 7: Write Editorial Narration
512
+ ### Step 6: Write Editorial Narration
436
513
 
437
514
  Once you have phases and milestones, write the narration layer. See [Editorial Narration](#editorial-narration) for the full guide. The short version:
438
515
 
@@ -445,30 +522,30 @@ This step is optional but strongly recommended. Without narration, the visualiza
445
522
 
446
523
  **Tier 2 shortcut:** Have an LLM read your phases + milestones + a sample of key messages, then draft the narration. Review and rewrite — LLM-drafted narration tends to be too formal and too long. Cut aggressively. Add humor. Make it sound like a person, not a report.
447
524
 
448
- ### Step 8: Write the JSON Files
525
+ ### Step 7: Validate with Timeline
449
526
 
450
- Create `retro-page-data.json`, `retro-page-quotes.json`, and `narration-editorial.json` in your data directory following the formats above. Run the pipeline:
527
+ Run `npx miriad-viz timeline` to check your work. It shows:
528
+ - Narration segments + quotes on a single timeline axis
529
+ - Same-agent quote overlaps (errors)
530
+ - Narration overlaps (errors)
531
+ - Gaps where the narration lane will be empty (warnings)
451
532
 
452
- ```bash
453
- pnpm viz <data-dir>
454
- ```
533
+ Fix any issues, run `timeline` again until clean.
534
+
535
+ ### Step 8: Write the JSON Files
455
536
 
456
- The CLI will validate the files and produce `viz-data.json`. Check the output:
457
- - Are all phases present with correct time ranges?
458
- - Are quotes assigned to the right phases?
459
- - Does the trust arc have the shape you intended?
460
- - Does the narration lane show editorial text (not quote fallback)?
537
+ Create `retro-page-data.json`, `retro-page-quotes.json`, and `narration-editorial.json` in your data directory following the formats above. Run `npx miriad-viz next` to validate and advance.
461
538
 
462
539
  ### Step 9: Iterate
463
540
 
464
541
  Watch the visualization. Does the story come through? Common issues:
465
542
  - **Quotes appearing at wrong times** — check timestamps
466
543
  - **Phases too short or too long** — adjust boundaries
467
- - **Trust arc too flat** — add more keyframes at inflection points
468
544
  - **Too many milestones clustered together** — thin them out, keep the most important
469
- - **Narration lane empty or showing quotes** — check `narration-editorial.json` is loaded and entries have correct `start`/`end` times
545
+ - **Narration lane empty or showing quotes** — check `narration-editorial.json` is loaded and entries have correct `start`/`end` times (fractional 0-1, NOT ISO timestamps)
470
546
  - **Narration feels rushed** — widen the `start`/`end` windows so text stays on screen longer
471
547
  - **Narration gaps (lane goes blank)** — add phase gap-fillers or lower the gap threshold
548
+ - **Same-agent quotes overlapping** — run `npx miriad-viz timeline` to find them, then stagger
472
549
 
473
550
  ## The Measurement Paradox
474
551
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "miriad-viz",
3
- "version": "0.2.2",
3
+ "version": "0.3.0",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/snorrees/miriad-viz.git"