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.
- package/dist-cli/{curate-KLE2K3TB.js → curate-EWAMDCOW.js} +29 -21
- package/dist-cli/index.js +175 -24
- package/dist-cli/timeline-EDSW3EWB.js +335 -0
- package/docs/curation-guide.md +137 -60
- package/package.json +1 -1
|
@@ -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
|
|
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:
|
|
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:
|
|
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:
|
|
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,
|
|
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
|
|
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
|
|
144
|
-
|
|
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
|
|
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(""
|
|
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
|
|
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
|
|
454
|
-
output.push(
|
|
455
|
-
output.push(
|
|
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
|
|
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
|
|
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-
|
|
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-
|
|
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
|
+
};
|
package/docs/curation-guide.md
CHANGED
|
@@ -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,
|
|
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.
|
|
51
|
-
6.
|
|
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,
|
|
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:
|
|
476
|
+
### Step 2: Mine the History
|
|
392
477
|
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
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:
|
|
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
|
|
525
|
+
### Step 7: Validate with Timeline
|
|
449
526
|
|
|
450
|
-
|
|
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
|
-
|
|
453
|
-
|
|
454
|
-
|
|
533
|
+
Fix any issues, run `timeline` again until clean.
|
|
534
|
+
|
|
535
|
+
### Step 8: Write the JSON Files
|
|
455
536
|
|
|
456
|
-
|
|
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
|
|