miriad-viz 0.9.1 → 0.9.2

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.
@@ -0,0 +1,189 @@
1
+ // src/cli/guided/validate-curated-files.ts
2
+ function validateTimelineEvents(data) {
3
+ const errors = [];
4
+ const warnings = [];
5
+ if (data === null || typeof data !== "object" || !Array.isArray(data.events)) {
6
+ errors.push("timeline-events.json: `events` must be an array");
7
+ return { errors, warnings };
8
+ }
9
+ const events = data.events;
10
+ const messageBeanEvents = events.filter((e) => {
11
+ if (e === null || typeof e !== "object") return false;
12
+ const type = e.type;
13
+ return type === "message" || type === "beam";
14
+ });
15
+ if (messageBeanEvents.length === 0) {
16
+ errors.push(
17
+ "0 message/beam events \u2014 no chat pills will render. Add events with type 'message' or 'beam', fields: { type, t, from, to }."
18
+ );
19
+ }
20
+ const missingT = events.filter((e) => {
21
+ if (e === null || typeof e !== "object") return true;
22
+ return !e.t;
23
+ });
24
+ if (missingT.length > 0) {
25
+ warnings.push(
26
+ `${missingT.length} event(s) missing 't' field (timestamp) \u2014 events without timestamps may not render correctly`
27
+ );
28
+ }
29
+ const messageBeanMissingFields = messageBeanEvents.filter((e) => {
30
+ const ev = e;
31
+ return !ev.from || !ev.to;
32
+ });
33
+ if (messageBeanMissingFields.length > 0) {
34
+ warnings.push(
35
+ `${messageBeanMissingFields.length} message/beam event(s) missing 'from' or 'to' field \u2014 chat pills need both fields to render correctly`
36
+ );
37
+ }
38
+ if (messageBeanEvents.length === 0 && events.length > 0) {
39
+ const milestoneEvents = events.filter((e) => {
40
+ if (e === null || typeof e !== "object") return false;
41
+ return e.type === "milestone";
42
+ });
43
+ if (milestoneEvents.length > 0) {
44
+ warnings.push(
45
+ "Only milestone events found. Milestones render as markers, NOT chat pills. Add message/beam events for chat pills."
46
+ );
47
+ }
48
+ }
49
+ return { errors, warnings };
50
+ }
51
+ function validateRetroPageData(data) {
52
+ const errors = [];
53
+ const warnings = [];
54
+ if (data === null || typeof data !== "object") {
55
+ errors.push("retro-page-data.json: `meta` is missing or not an object");
56
+ return { errors, warnings };
57
+ }
58
+ const d = data;
59
+ if (d.meta === null || d.meta === void 0 || typeof d.meta !== "object") {
60
+ errors.push("retro-page-data.json: `meta` is missing or not an object");
61
+ return { errors, warnings };
62
+ }
63
+ const meta = d.meta;
64
+ if (!meta.title) {
65
+ errors.push("meta.title is required");
66
+ }
67
+ if (!meta.subtitle) {
68
+ errors.push("meta.subtitle is required");
69
+ }
70
+ if (!meta.startDate) {
71
+ errors.push(
72
+ "meta.startDate is required \u2014 transform uses this to calculate the project time span"
73
+ );
74
+ }
75
+ if (!meta.endDate) {
76
+ errors.push(
77
+ "meta.endDate is required \u2014 transform uses this to calculate the project time span"
78
+ );
79
+ }
80
+ if (!d.teamAssembly || !Array.isArray(d.teamAssembly) || d.teamAssembly.length === 0) {
81
+ warnings.push("No team assembly entries");
82
+ } else {
83
+ const teamAssembly = d.teamAssembly;
84
+ for (let i = 0; i < teamAssembly.length; i++) {
85
+ const entry = teamAssembly[i];
86
+ if (entry === null || typeof entry !== "object") continue;
87
+ const e = entry;
88
+ if (!e.agent) {
89
+ errors.push(`teamAssembly[${i}] missing 'agent' field \u2014 use 'agent', not 'name'`);
90
+ }
91
+ if (!e.time) {
92
+ warnings.push(
93
+ `teamAssembly[${i}] missing 'time' field \u2014 agent join timestamp will be null`
94
+ );
95
+ }
96
+ }
97
+ }
98
+ if (!d.phases || !Array.isArray(d.phases) || d.phases.length === 0) {
99
+ warnings.push("No phases defined \u2014 timeline will have no phase labels");
100
+ } else {
101
+ const phases = d.phases;
102
+ for (let i = 0; i < phases.length; i++) {
103
+ const phase = phases[i];
104
+ if (phase === null || typeof phase !== "object") continue;
105
+ const p = phase;
106
+ if (!p.start || !p.end) {
107
+ warnings.push(`phases[${i}] missing start/end timestamps`);
108
+ }
109
+ }
110
+ }
111
+ if (!d.milestones || !Array.isArray(d.milestones) || d.milestones.length === 0) {
112
+ warnings.push("No milestones defined");
113
+ }
114
+ if (!d.trustArc || !Array.isArray(d.trustArc) || d.trustArc.length === 0) {
115
+ warnings.push("No trust arc data");
116
+ }
117
+ if (!d.editorialNarration || !Array.isArray(d.editorialNarration) || d.editorialNarration.length === 0) {
118
+ warnings.push("No editorial narration \u2014 generic phase-based narration will be used");
119
+ }
120
+ return { errors, warnings };
121
+ }
122
+ function validateRetroPageQuotes(data) {
123
+ const errors = [];
124
+ const warnings = [];
125
+ if (data === null || typeof data !== "object" || Array.isArray(data)) {
126
+ errors.push("retro-page-quotes.json: must be an object (not an array or primitive)");
127
+ return { errors, warnings };
128
+ }
129
+ const d = data;
130
+ const categories = ["good", "bad", "ugly"];
131
+ const foundCategories = categories.filter(
132
+ (cat) => Array.isArray(d[cat]) && d[cat].length > 0
133
+ );
134
+ if (foundCategories.length === 0) {
135
+ warnings.push("No quote categories found");
136
+ return { errors, warnings };
137
+ }
138
+ for (const cat of foundCategories) {
139
+ const entries = d[cat];
140
+ for (let i = 0; i < entries.length; i++) {
141
+ const entry = entries[i];
142
+ if (entry === null || typeof entry !== "object") continue;
143
+ const e = entry;
144
+ if (e.speaker !== void 0 && e.author === void 0) {
145
+ warnings.push(`quotes[${i}] uses 'speaker' \u2014 should be 'author'`);
146
+ }
147
+ if (!e.text) {
148
+ warnings.push(`quotes[${i}] missing 'text' field`);
149
+ }
150
+ if (!e.timestamp) {
151
+ warnings.push(`quotes[${i}] missing 'timestamp' field`);
152
+ }
153
+ }
154
+ }
155
+ return { errors, warnings };
156
+ }
157
+ function validateCuratedFiles(timelineEvents, retroPageData, retroPageQuotes) {
158
+ const errors = [];
159
+ const warnings = [];
160
+ if (timelineEvents === null) {
161
+ errors.push("timeline-events.json: could not be read or parsed");
162
+ } else {
163
+ const r = validateTimelineEvents(timelineEvents);
164
+ errors.push(...r.errors);
165
+ warnings.push(...r.warnings);
166
+ }
167
+ if (retroPageData === null) {
168
+ errors.push("retro-page-data.json: could not be read or parsed");
169
+ } else {
170
+ const r = validateRetroPageData(retroPageData);
171
+ errors.push(...r.errors);
172
+ warnings.push(...r.warnings);
173
+ }
174
+ if (retroPageQuotes === null) {
175
+ errors.push("retro-page-quotes.json: could not be read or parsed");
176
+ } else {
177
+ const r = validateRetroPageQuotes(retroPageQuotes);
178
+ errors.push(...r.errors);
179
+ warnings.push(...r.warnings);
180
+ }
181
+ return { errors, warnings };
182
+ }
183
+
184
+ export {
185
+ validateTimelineEvents,
186
+ validateRetroPageData,
187
+ validateRetroPageQuotes,
188
+ validateCuratedFiles
189
+ };
package/dist-cli/index.js CHANGED
@@ -1579,6 +1579,7 @@ async function runNext(flags) {
1579
1579
  "audio-approved": flags["audio-approved"] === true || void 0,
1580
1580
  // v2 gate flags
1581
1581
  "script-approved": flags["script-approved"] === true || void 0,
1582
+ "curate-approved": flags["curate-approved"] === true || void 0,
1582
1583
  "voices-approved": flags["voices-approved"] === true || void 0,
1583
1584
  "timing-approved": flags["timing-approved"] === true || void 0,
1584
1585
  "sound-approved": flags["sound-approved"] === true || void 0,
@@ -1597,6 +1598,34 @@ async function runNext(flags) {
1597
1598
  progress.project.dataDir,
1598
1599
  progress.project.outputDir
1599
1600
  );
1601
+ if (flags["curate-approved"]) {
1602
+ const { validateCuratedFiles } = await import("./validate-curated-files-ZLNFHBXT.js");
1603
+ const curateDataDir = resolve3(projectDir, progress.project.dataDir);
1604
+ const tryReadJSON = (filename) => {
1605
+ try {
1606
+ return JSON.parse(readFileSync2(resolve3(curateDataDir, filename), "utf-8"));
1607
+ } catch {
1608
+ return null;
1609
+ }
1610
+ };
1611
+ const validation = validateCuratedFiles(
1612
+ tryReadJSON("timeline-events.json"),
1613
+ tryReadJSON("retro-page-data.json"),
1614
+ tryReadJSON("retro-page-quotes.json")
1615
+ );
1616
+ if (validation.errors.length > 0) {
1617
+ console.log("\n\u2717 Curation validation failed:\n");
1618
+ for (const e of validation.errors) console.log(` \u2717 ${e}`);
1619
+ for (const w of validation.warnings) console.log(` \u26A0 ${w}`);
1620
+ console.log("\nFix the errors above and try again.");
1621
+ process.exit(1);
1622
+ }
1623
+ if (validation.warnings.length > 0) {
1624
+ console.log("\n\u26A0 Curation warnings:\n");
1625
+ for (const w of validation.warnings) console.log(` \u26A0 ${w}`);
1626
+ console.log("");
1627
+ }
1628
+ }
1600
1629
  const result = computeNext(
1601
1630
  progress,
1602
1631
  existingFiles,
@@ -1682,7 +1711,7 @@ async function main() {
1682
1711
  case "transform": {
1683
1712
  const { projectDir, progress } = requireProject();
1684
1713
  const { parseDuration } = await import("./parse-duration-NVLCEFAF.js");
1685
- const { runTransform } = await import("./transform-SCT5EYPV.js");
1714
+ const { runTransform } = await import("./transform-Z4LA7G44.js");
1686
1715
  const padding = {};
1687
1716
  if (typeof flags["pad-start"] === "string") {
1688
1717
  padding.padStartMs = parseDuration(flags["pad-start"]);
@@ -1710,7 +1739,7 @@ async function main() {
1710
1739
  console.log("");
1711
1740
  console.log(chainResult.previewTable);
1712
1741
  }
1713
- const { runTransform } = await import("./transform-SCT5EYPV.js");
1742
+ const { runTransform } = await import("./transform-Z4LA7G44.js");
1714
1743
  await runTransform({ projectDir, progress });
1715
1744
  const { runPreview: runPreview2 } = await import("./preview-UUWVOX5Y.js");
1716
1745
  await runPreview2({ projectDir, progress, port, noOpen: flags["no-open"] === true });
@@ -1,3 +1,9 @@
1
+ import {
2
+ TimingFileSchema
3
+ } from "./chunk-SKRQW7PY.js";
4
+ import {
5
+ validateCuratedFiles
6
+ } from "./chunk-K6N7F6GL.js";
1
7
  import {
2
8
  buildArtifactData
3
9
  } from "./chunk-JITEBF25.js";
@@ -15,9 +21,6 @@ import {
15
21
  markInProgress,
16
22
  writeProgress
17
23
  } from "./chunk-GLABTDMO.js";
18
- import {
19
- TimingFileSchema
20
- } from "./chunk-SKRQW7PY.js";
21
24
 
22
25
  // src/cli/guided/steps/transform.ts
23
26
  import { resolve as resolve2 } from "path";
@@ -659,6 +662,22 @@ function printDataInventory(summary, warnings) {
659
662
  }
660
663
  function runPipeline(dataDir, outDir, options) {
661
664
  const { sources, warnings } = loadSources(dataDir);
665
+ const curationValidation = validateCuratedFiles(
666
+ sources.timelineEvents,
667
+ sources.retroPageData,
668
+ sources.retroPageQuotes
669
+ );
670
+ if (curationValidation.errors.length > 0) {
671
+ console.warn("\n\u26A0 Curated file validation issues:\n");
672
+ for (const e of curationValidation.errors) console.warn(` \u2717 ${e}`);
673
+ for (const w of curationValidation.warnings) console.warn(` \u26A0 ${w}`);
674
+ console.warn("\n Transform will continue, but output may be incomplete.");
675
+ console.warn(" Fix the issues above and re-run for best results.\n");
676
+ } else if (curationValidation.warnings.length > 0) {
677
+ console.warn("\n\u26A0 Curated file warnings:\n");
678
+ for (const w of curationValidation.warnings) console.warn(` \u26A0 ${w}`);
679
+ console.warn("");
680
+ }
662
681
  const summary = validateSources(sources);
663
682
  printDataInventory(summary, warnings);
664
683
  const rawData = transformToRawData(sources);
@@ -0,0 +1,12 @@
1
+ import {
2
+ validateCuratedFiles,
3
+ validateRetroPageData,
4
+ validateRetroPageQuotes,
5
+ validateTimelineEvents
6
+ } from "./chunk-K6N7F6GL.js";
7
+ export {
8
+ validateCuratedFiles,
9
+ validateRetroPageData,
10
+ validateRetroPageQuotes,
11
+ validateTimelineEvents
12
+ };
@@ -1795,6 +1795,20 @@ function computeTimeLabelWidth(totalHours) {
1795
1795
  return `${totalChars}ch`;
1796
1796
  }
1797
1797
  var MANUAL_SPEEDS = [0.5, 1, 2, 4];
1798
+ var MOBILE_BREAKPOINT = 640;
1799
+ function useIsMobile(breakpoint = MOBILE_BREAKPOINT) {
1800
+ const [isMobile, setIsMobile] = react.useState(
1801
+ () => typeof window !== "undefined" ? window.innerWidth <= breakpoint : false
1802
+ );
1803
+ react.useEffect(() => {
1804
+ const mql = window.matchMedia(`(max-width: ${breakpoint}px)`);
1805
+ const handler = (e) => setIsMobile(e.matches);
1806
+ setIsMobile(mql.matches);
1807
+ mql.addEventListener("change", handler);
1808
+ return () => mql.removeEventListener("change", handler);
1809
+ }, [breakpoint]);
1810
+ return isMobile;
1811
+ }
1798
1812
  function PlayIcon({ size = 14 }) {
1799
1813
  return /* @__PURE__ */ jsxRuntime.jsx(
1800
1814
  "svg",
@@ -1851,6 +1865,20 @@ function SoundOffIcon({ size = 16 }) {
1851
1865
  }
1852
1866
  );
1853
1867
  }
1868
+ function ChevronDownIcon({ size = 10 }) {
1869
+ return /* @__PURE__ */ jsxRuntime.jsx(
1870
+ "svg",
1871
+ {
1872
+ width: size,
1873
+ height: size,
1874
+ viewBox: "0 0 24 24",
1875
+ fill: "currentColor",
1876
+ role: "img",
1877
+ "aria-label": "Speed menu",
1878
+ children: /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M7 10l5 5 5-5z" })
1879
+ }
1880
+ );
1881
+ }
1854
1882
  function GearIcon({ size = 14 }) {
1855
1883
  return /* @__PURE__ */ jsxRuntime.jsx(
1856
1884
  "svg",
@@ -1878,6 +1906,19 @@ function Footer({
1878
1906
  }) {
1879
1907
  const { progress, playing, speed, toggle, setSpeed, scrub } = playback;
1880
1908
  const lastManualSpeed = react.useRef(1);
1909
+ const isMobile = useIsMobile();
1910
+ const [speedMenuOpen, setSpeedMenuOpen] = react.useState(false);
1911
+ const speedMenuRef = react.useRef(null);
1912
+ react.useEffect(() => {
1913
+ if (!speedMenuOpen) return;
1914
+ const handleClickOutside = (e) => {
1915
+ if (speedMenuRef.current && !speedMenuRef.current.contains(e.target)) {
1916
+ setSpeedMenuOpen(false);
1917
+ }
1918
+ };
1919
+ document.addEventListener("mousedown", handleClickOutside);
1920
+ return () => document.removeEventListener("mousedown", handleClickOutside);
1921
+ }, [speedMenuOpen]);
1881
1922
  const isAuto = hasTimingFile && !muted;
1882
1923
  const handleKeyDown = react.useCallback(
1883
1924
  (e) => {
@@ -1959,10 +2000,69 @@ function Footer({
1959
2000
  step: 1e-3,
1960
2001
  value: progress,
1961
2002
  onChange: (e) => scrub(Number(e.target.value)),
1962
- style: scrubberStyle
2003
+ style: isMobile ? mobileScrubberStyle : scrubberStyle
1963
2004
  }
1964
2005
  ),
1965
- /* @__PURE__ */ jsxRuntime.jsxs("div", { style: speedGroupStyle, children: [
2006
+ isMobile ? /* @__PURE__ */ jsxRuntime.jsxs("div", { ref: speedMenuRef, style: speedMenuContainerStyle, children: [
2007
+ /* @__PURE__ */ jsxRuntime.jsxs(
2008
+ "button",
2009
+ {
2010
+ type: "button",
2011
+ onClick: () => setSpeedMenuOpen((v) => !v),
2012
+ style: {
2013
+ ...speedMenuTriggerStyle,
2014
+ color: speedMenuOpen ? "#00e5cc" : "#fff",
2015
+ background: speedMenuOpen ? "rgba(0, 229, 204, 0.12)" : "rgba(255,255,255,0.08)"
2016
+ },
2017
+ title: "Speed",
2018
+ children: [
2019
+ isAuto ? "Auto" : `${speed}\xD7`,
2020
+ /* @__PURE__ */ jsxRuntime.jsx(ChevronDownIcon, {})
2021
+ ]
2022
+ }
2023
+ ),
2024
+ speedMenuOpen && /* @__PURE__ */ jsxRuntime.jsxs("div", { style: speedMenuDropdownStyle, children: [
2025
+ hasTimingFile && /* @__PURE__ */ jsxRuntime.jsxs(
2026
+ "button",
2027
+ {
2028
+ type: "button",
2029
+ onClick: () => {
2030
+ handleAutoClick();
2031
+ setSpeedMenuOpen(false);
2032
+ },
2033
+ style: {
2034
+ ...speedMenuItemStyle,
2035
+ color: isAuto ? "#00e5cc" : "#fff",
2036
+ fontWeight: isAuto ? 700 : 400
2037
+ },
2038
+ children: [
2039
+ "Auto ",
2040
+ autoSpeedLabel
2041
+ ]
2042
+ }
2043
+ ),
2044
+ MANUAL_SPEEDS.map((s) => /* @__PURE__ */ jsxRuntime.jsxs(
2045
+ "button",
2046
+ {
2047
+ type: "button",
2048
+ onClick: () => {
2049
+ handleManualSpeed(s);
2050
+ setSpeedMenuOpen(false);
2051
+ },
2052
+ style: {
2053
+ ...speedMenuItemStyle,
2054
+ color: !isAuto && speed === s ? "#00e5cc" : "#fff",
2055
+ fontWeight: !isAuto && speed === s ? 700 : 400
2056
+ },
2057
+ children: [
2058
+ s,
2059
+ "\xD7"
2060
+ ]
2061
+ },
2062
+ s
2063
+ ))
2064
+ ] })
2065
+ ] }) : /* @__PURE__ */ jsxRuntime.jsxs("div", { style: speedGroupStyle, children: [
1966
2066
  hasTimingFile && /* @__PURE__ */ jsxRuntime.jsxs(
1967
2067
  "button",
1968
2068
  {
@@ -2003,7 +2103,7 @@ function Footer({
2003
2103
  s
2004
2104
  ))
2005
2105
  ] }),
2006
- /* @__PURE__ */ jsxRuntime.jsx("span", { style: { ...timeLabelStyle, width: timeLabelWidth }, children: timeLabel }),
2106
+ !isMobile && /* @__PURE__ */ jsxRuntime.jsx("span", { style: { ...timeLabelStyle, width: timeLabelWidth }, children: timeLabel }),
2007
2107
  hasTimingFile && onToggleMute && /* @__PURE__ */ jsxRuntime.jsx(
2008
2108
  "button",
2009
2109
  {
@@ -2102,6 +2202,54 @@ var muteButtonStyle = {
2102
2202
  minWidth: "32px",
2103
2203
  minHeight: "28px"
2104
2204
  };
2205
+ var mobileScrubberStyle = {
2206
+ flex: 1,
2207
+ minWidth: 0,
2208
+ cursor: "pointer",
2209
+ accentColor: "#3b82f6",
2210
+ height: "24px"
2211
+ };
2212
+ var speedMenuContainerStyle = {
2213
+ position: "relative",
2214
+ flexShrink: 0
2215
+ };
2216
+ var speedMenuTriggerStyle = {
2217
+ background: "rgba(255,255,255,0.08)",
2218
+ border: "none",
2219
+ borderRadius: "4px",
2220
+ color: "#fff",
2221
+ fontSize: "11px",
2222
+ padding: "4px 6px",
2223
+ cursor: "pointer",
2224
+ display: "flex",
2225
+ alignItems: "center",
2226
+ gap: "2px",
2227
+ minHeight: "28px"
2228
+ };
2229
+ var speedMenuDropdownStyle = {
2230
+ position: "absolute",
2231
+ bottom: "100%",
2232
+ right: 0,
2233
+ marginBottom: "4px",
2234
+ background: "rgba(0, 0, 0, 0.95)",
2235
+ border: "1px solid rgba(255, 255, 255, 0.15)",
2236
+ borderRadius: "6px",
2237
+ padding: "4px 0",
2238
+ display: "flex",
2239
+ flexDirection: "column",
2240
+ minWidth: "80px",
2241
+ zIndex: 100
2242
+ };
2243
+ var speedMenuItemStyle = {
2244
+ background: "none",
2245
+ border: "none",
2246
+ color: "#fff",
2247
+ fontSize: "13px",
2248
+ padding: "8px 12px",
2249
+ cursor: "pointer",
2250
+ textAlign: "left",
2251
+ whiteSpace: "nowrap"
2252
+ };
2105
2253
  var devButtonStyle = {
2106
2254
  background: "rgba(255,255,255,0.08)",
2107
2255
  border: "none",