explainer-video 0.0.0-stage → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (150) hide show
  1. package/.claude-plugin/plugin.json +12 -0
  2. package/LICENSE +21 -0
  3. package/README.md +152 -2
  4. package/cordis.patch.yml +7 -0
  5. package/lib/index.js +35 -0
  6. package/package.json +56 -4
  7. package/plugin.json +13 -0
  8. package/skills/coursework-author/.synced-from +5 -0
  9. package/skills/coursework-author/SKILL.md +209 -0
  10. package/skills/craft-codeflow-flows/SKILL.md +116 -0
  11. package/skills/craft-codeflow-mine/SKILL.md +207 -0
  12. package/skills/craft-codeflow-plan/SKILL.md +65 -0
  13. package/skills/craft-codeflow-scene/SKILL.md +168 -0
  14. package/skills/craft-video-align/SKILL.md +109 -0
  15. package/skills/craft-video-assemble/SKILL.md +69 -0
  16. package/skills/craft-video-audio/SKILL.md +59 -0
  17. package/skills/craft-video-enrich/SKILL.md +46 -0
  18. package/skills/craft-video-plan/SKILL.md +57 -0
  19. package/skills/craft-video-publish/SKILL.md +79 -0
  20. package/skills/craft-video-render/SKILL.md +68 -0
  21. package/skills/craft-video-scene/SKILL.md +112 -0
  22. package/skills/craft-video-script/SKILL.md +80 -0
  23. package/skills/explainer-video-from-codebase/FLOWS.md +577 -0
  24. package/skills/explainer-video-from-codebase/SKILL.md +659 -0
  25. package/skills/explainer-video-from-codebase/tools/apply-flows.mjs +531 -0
  26. package/skills/explainer-video-from-codebase/tools/approve-flows.mjs +56 -0
  27. package/skills/explainer-video-from-codebase/tools/codemap-md.mjs +116 -0
  28. package/skills/explainer-video-from-codebase/tools/config/dotenv.mjs +13 -0
  29. package/skills/explainer-video-from-codebase/tools/config/json.mjs +14 -0
  30. package/skills/explainer-video-from-codebase/tools/config/redact.mjs +16 -0
  31. package/skills/explainer-video-from-codebase/tools/config/toml-lite.mjs +13 -0
  32. package/skills/explainer-video-from-codebase/tools/config/yaml-lite.mjs +18 -0
  33. package/skills/explainer-video-from-codebase/tools/excerpt-check.mjs +186 -0
  34. package/skills/explainer-video-from-codebase/tools/excerpt.mjs +116 -0
  35. package/skills/explainer-video-from-codebase/tools/flow-diagram.mjs +168 -0
  36. package/skills/explainer-video-from-codebase/tools/flow-plan.mjs +129 -0
  37. package/skills/explainer-video-from-codebase/tools/flows-md.mjs +87 -0
  38. package/skills/explainer-video-from-codebase/tools/lang/c.mjs +684 -0
  39. package/skills/explainer-video-from-codebase/tools/lang/go.mjs +673 -0
  40. package/skills/explainer-video-from-codebase/tools/lang/python-config.mjs +143 -0
  41. package/skills/explainer-video-from-codebase/tools/lang/python-deps.mjs +62 -0
  42. package/skills/explainer-video-from-codebase/tools/lang/python-stdlib.mjs +24 -0
  43. package/skills/explainer-video-from-codebase/tools/lang/python.mjs +609 -0
  44. package/skills/explainer-video-from-codebase/tools/lang/typescript.mjs +963 -0
  45. package/skills/explainer-video-from-codebase/tools/lib/decompose.mjs +102 -0
  46. package/skills/explainer-video-from-codebase/tools/lib/groups.mjs +77 -0
  47. package/skills/explainer-video-from-codebase/tools/lib/observe-coverage.mjs +68 -0
  48. package/skills/explainer-video-from-codebase/tools/lib/observe-v8.mjs +232 -0
  49. package/skills/explainer-video-from-codebase/tools/lib/observe.mjs +162 -0
  50. package/skills/explainer-video-from-codebase/tools/lib/redact.mjs +28 -0
  51. package/skills/explainer-video-from-codebase/tools/lib/sandbox.mjs +97 -0
  52. package/skills/explainer-video-from-codebase/tools/lib/test-links.mjs +28 -0
  53. package/skills/explainer-video-from-codebase/tools/lib/toml.mjs +129 -0
  54. package/skills/explainer-video-from-codebase/tools/lib/trace-c.mjs +95 -0
  55. package/skills/explainer-video-from-codebase/tools/lib/trace-go.mjs +101 -0
  56. package/skills/explainer-video-from-codebase/tools/lib/trace-js.mjs +136 -0
  57. package/skills/explainer-video-from-codebase/tools/lib/tree-sitter.mjs +19 -0
  58. package/skills/explainer-video-from-codebase/tools/lib/walk.mjs +138 -0
  59. package/skills/explainer-video-from-codebase/tools/mine.mjs +409 -0
  60. package/skills/explainer-video-from-codebase/tools/propose.mjs +127 -0
  61. package/skills/explainer-video-from-codebase/tools/rank.mjs +119 -0
  62. package/skills/explainer-video-from-codebase/tools/ranking-md.mjs +76 -0
  63. package/skills/explainer-video-from-codebase/tools/rules/c-handlers.mjs +53 -0
  64. package/skills/explainer-video-from-codebase/tools/rules/celery-taskiq.mjs +48 -0
  65. package/skills/explainer-video-from-codebase/tools/rules/click-typer.mjs +6 -0
  66. package/skills/explainer-video-from-codebase/tools/rules/express.mjs +72 -0
  67. package/skills/explainer-video-from-codebase/tools/rules/fastapi.mjs +99 -0
  68. package/skills/explainer-video-from-codebase/tools/rules/go-http.mjs +173 -0
  69. package/skills/explainer-video-from-codebase/tools/rules/go-test.mjs +108 -0
  70. package/skills/explainer-video-from-codebase/tools/rules/grpc.mjs +57 -0
  71. package/skills/explainer-video-from-codebase/tools/rules/gtest.mjs +97 -0
  72. package/skills/explainer-video-from-codebase/tools/rules/haystack.mjs +55 -0
  73. package/skills/explainer-video-from-codebase/tools/rules/jest-vitest.mjs +100 -0
  74. package/skills/explainer-video-from-codebase/tools/rules/js-http-client.mjs +111 -0
  75. package/skills/explainer-video-from-codebase/tools/rules/nextjs.mjs +53 -0
  76. package/skills/explainer-video-from-codebase/tools/rules/process-spawn.mjs +49 -0
  77. package/skills/explainer-video-from-codebase/tools/rules/py-http-client.mjs +26 -0
  78. package/skills/explainer-video-from-codebase/tools/rules/pytest.mjs +107 -0
  79. package/skills/explainer-video-from-codebase/tools/rules/react-router.mjs +17 -0
  80. package/skills/explainer-video-from-codebase/tools/rules/registry.mjs +31 -0
  81. package/skills/explainer-video-from-codebase/tools/trace/c/fmtrace-sh +14 -0
  82. package/skills/explainer-video-from-codebase/tools/trace/js/fmtrace-net.cjs +33 -0
  83. package/skills/explainer-video-from-codebase/tools/trace/js/fmtrace-srcmap.cjs +41 -0
  84. package/skills/explainer-video-from-codebase/tools/trace/python/fmtrace_plugin.py +87 -0
  85. package/skills/explainer-video-from-codebase/tools/trace/python/sitecustomize.py +330 -0
  86. package/skills/explainer-video-from-codebase/tools/trace/sandbox/linux-bwrap.sh +73 -0
  87. package/skills/explainer-video-from-codebase/tools/trace/sandbox/macos-nonet.sb +41 -0
  88. package/skills/explainer-video-from-codebase/tools/trace.mjs +577 -0
  89. package/skills/explainer-video-from-codebase/vendor/tree-sitter/LICENSES/tree-sitter-c.txt +21 -0
  90. package/skills/explainer-video-from-codebase/vendor/tree-sitter/LICENSES/tree-sitter-cpp.txt +21 -0
  91. package/skills/explainer-video-from-codebase/vendor/tree-sitter/LICENSES/tree-sitter-go.txt +21 -0
  92. package/skills/explainer-video-from-codebase/vendor/tree-sitter/LICENSES/tree-sitter-javascript.txt +21 -0
  93. package/skills/explainer-video-from-codebase/vendor/tree-sitter/LICENSES/tree-sitter-python.txt +21 -0
  94. package/skills/explainer-video-from-codebase/vendor/tree-sitter/LICENSES/tree-sitter-typescript.txt +21 -0
  95. package/skills/explainer-video-from-codebase/vendor/tree-sitter/LICENSES/web-tree-sitter.txt +21 -0
  96. package/skills/explainer-video-from-codebase/vendor/tree-sitter/MANIFEST.json +172 -0
  97. package/skills/explainer-video-from-codebase/vendor/tree-sitter/tree-sitter-c.wasm +0 -0
  98. package/skills/explainer-video-from-codebase/vendor/tree-sitter/tree-sitter-cpp.wasm +0 -0
  99. package/skills/explainer-video-from-codebase/vendor/tree-sitter/tree-sitter-go.wasm +0 -0
  100. package/skills/explainer-video-from-codebase/vendor/tree-sitter/tree-sitter-javascript.wasm +0 -0
  101. package/skills/explainer-video-from-codebase/vendor/tree-sitter/tree-sitter-python.wasm +0 -0
  102. package/skills/explainer-video-from-codebase/vendor/tree-sitter/tree-sitter-tsx.wasm +0 -0
  103. package/skills/explainer-video-from-codebase/vendor/tree-sitter/tree-sitter-typescript.wasm +0 -0
  104. package/skills/explainer-video-from-codebase/vendor/tree-sitter/web-tree-sitter.js +4081 -0
  105. package/skills/explainer-video-from-codebase/vendor/tree-sitter/web-tree-sitter.wasm +0 -0
  106. package/skills/explainer-video-from-coursework/ARCHITECTURE.md +59 -0
  107. package/skills/explainer-video-from-coursework/PIPELINE.md +343 -0
  108. package/skills/explainer-video-from-coursework/ROADMAP.md +725 -0
  109. package/skills/explainer-video-from-coursework/SERIES.md +232 -0
  110. package/skills/explainer-video-from-coursework/SETUP.md +105 -0
  111. package/skills/explainer-video-from-coursework/SKILL.md +858 -0
  112. package/skills/explainer-video-from-coursework/scenes/_pt-chart.js +12 -0
  113. package/skills/explainer-video-from-coursework/scenes/_pt-equation.js +10 -0
  114. package/skills/explainer-video-from-coursework/scenes/_pt-hubspoke.js +7 -0
  115. package/skills/explainer-video-from-coursework/scenes/_pt-loopflow.js +7 -0
  116. package/skills/explainer-video-from-coursework/scenes/_pt-sequence.js +12 -0
  117. package/skills/explainer-video-from-coursework/scenes/_pt-timeline.js +11 -0
  118. package/skills/explainer-video-from-coursework/src/scene-code.html +127 -0
  119. package/skills/explainer-video-from-coursework/src/scene-code.js +379 -0
  120. package/skills/explainer-video-from-coursework/src/scene-core.js +1250 -0
  121. package/skills/explainer-video-from-coursework/src/scene-diagram.html +50 -0
  122. package/skills/explainer-video-from-coursework/src/scene-svg.html +50 -0
  123. package/skills/explainer-video-from-coursework/src/timeline.js +297 -0
  124. package/skills/explainer-video-from-coursework/src/timing-chart.js +160 -0
  125. package/skills/explainer-video-from-coursework/tools/align.mjs +419 -0
  126. package/skills/explainer-video-from-coursework/tools/apply-enrichment.mjs +206 -0
  127. package/skills/explainer-video-from-coursework/tools/assemble.mjs +223 -0
  128. package/skills/explainer-video-from-coursework/tools/build-audio.mjs +234 -0
  129. package/skills/explainer-video-from-coursework/tools/chromium.mjs +73 -0
  130. package/skills/explainer-video-from-coursework/tools/debug-page.mjs +75 -0
  131. package/skills/explainer-video-from-coursework/tools/env.mjs +30 -0
  132. package/skills/explainer-video-from-coursework/tools/frame-dedup.mjs +131 -0
  133. package/skills/explainer-video-from-coursework/tools/key-source.mjs +151 -0
  134. package/skills/explainer-video-from-coursework/tools/model.mjs +21 -0
  135. package/skills/explainer-video-from-coursework/tools/naming.mjs +38 -0
  136. package/skills/explainer-video-from-coursework/tools/onset-audit.mjs +51 -0
  137. package/skills/explainer-video-from-coursework/tools/pacing-check.mjs +350 -0
  138. package/skills/explainer-video-from-coursework/tools/planner.mjs +616 -0
  139. package/skills/explainer-video-from-coursework/tools/publish.mjs +230 -0
  140. package/skills/explainer-video-from-coursework/tools/render.mjs +205 -0
  141. package/skills/explainer-video-from-coursework/tools/resolve-beats.mjs +239 -0
  142. package/skills/explainer-video-from-coursework/tools/script-check.mjs +174 -0
  143. package/skills/explainer-video-from-coursework/tools/stamp.mjs +71 -0
  144. package/skills/explainer-video-from-coursework/tools/timing-report.mjs +299 -0
  145. package/skills/explainer-video-from-coursework/tools/transcribe-gemini.mjs +96 -0
  146. package/skills/explainer-video-from-coursework/tools/tts-gemini.mjs +394 -0
  147. package/skills/explainer-video-from-coursework/tools/verify-audio.mjs +88 -0
  148. package/skills/explainer-video-from-coursework/tools/workdir.mjs +124 -0
  149. package/skills/explainer-video-from-coursework/vendor/plotly-basic.min.js +8 -0
  150. package/skills/explainer-video-from-coursework/vendor/plotly-basic.min.js.LICENSE.txt +16 -0
@@ -0,0 +1,725 @@
1
+ # Roadmap — what exists, what's left, what's questionable
2
+
3
+ Status snapshot for the `explainer-video-from-coursework` skill. Companion to
4
+ [`ARCHITECTURE.md`](./ARCHITECTURE.md), which states the code-vs-agent boundary.
5
+
6
+ Last updated after the `what-is-a-harness` run — videos 1–5 shipped end-to-end
7
+ (83 segments, 83 scenes, 29 min of finished video) and `/publish` built. Its P1 findings
8
+ are §2.9; the publish contract, the per-video work-dir layout and the beat model are
9
+ recorded as shipped in §1. Previously: the `high-level-design` run — video 30 through the
10
+ scene stage (15 scenes, `pacing-check` green, stills reviewed) — and the render and
11
+ assemble stages written into `SKILL.md`; video 6 (SOLID) completed end-to-end.
12
+
13
+ ---
14
+
15
+ ## 1. What exists
16
+
17
+ ### The skill
18
+
19
+ `SKILL.md` — all eight stages (plan, enrich, script, audio, scene, publish, render,
20
+ assemble), each with its command, its `done when` gate, and the visual-check
21
+ protocol (vision gate + the `LOOK` step). Distributed as one plugin of sixteen skills
22
+ (`plugin.json` + `skills/`) — see §2.3.
23
+
24
+ ### The stage skills
25
+
26
+ Eight sibling skills, one per stage, at `skills/craft-video-<stage>/SKILL.md` — top-level
27
+ under `skills/`, so every platform that reads the plugin discovers all nine. Each is a
28
+ complete contract for its stage — prerequisites, command, what the agent writes, hard
29
+ checks, `done when` — and each is separately invocable, so one stage can be re-run alone.
30
+ They reach the pipeline's tools as `../explainer-video-from-coursework/tools/<tool>.mjs`,
31
+ resolved against the stage skill's own folder.
32
+
33
+ `SKILL.md` stays **monolithic on purpose** — a full run loads in one pass — and is the
34
+ contract of record. Every stage file ends with a **Maintenance** note requiring the
35
+ matching `SKILL.md` section to be reconciled whenever the stage file changes: edit one,
36
+ edit both, or they silently diverge.
37
+
38
+ ### The pipeline
39
+
40
+ ```
41
+ source notes
42
+ │
43
+ ├─ /plan tools/planner.mjs ............. decompose + assert coverage [code]
44
+ ├─ /enrich agent writes enrichment.json
45
+ │ tools/apply-enrichment.mjs .... merge + validate [code]
46
+ ├─ /script agent writes scripts/NN-*.json
47
+ │ tools/script-check.mjs ........ structure/speakability/budget [code]
48
+ ├─ /audio tools/build-audio.mjs ......... Gemini TTS + MEASURED timeline [code, API]
49
+ ├─ /scene agent writes scenes/*.js
50
+ │ tools/pacing-check.mjs ........ reveal rate + dead air [code]
51
+ ├─ /publish agent writes publish.json
52
+ │ tools/publish.mjs ............. chapters + captions [code]
53
+ ├─ /render tools/render.mjs .............. Chromium frames + ffmpeg [code]
54
+ └─ /assemble tools/assemble.mjs ............ per-segment render + concat [code]
55
+ tools/verify-audio.mjs ........ sample-level audio match [code]
56
+ ```
57
+
58
+ Stages are named in prose (`/render`, `/assemble`) rather than numbered, so inserting a
59
+ stage does not silently rewrite history: publish's insertion at 6 moved render to 7 and
60
+ assemble to 8, and "stages 6–7" had meant render/assemble.
61
+
62
+ Per-video timing (stages 3–8) accrues in `out/<name>/timing.jsonl` via `tools/stamp.mjs`
63
+ (`begin`/`end` — the end is folded into each stage's *Done when*); `tools/timing-report.mjs`
64
+ renders `timing.csv` (long), `timing.md` (one heading per video), `timing.svg` (static), and
65
+ `timing.html` (interactive Plotly — the `vendor/plotly-basic.min.js` bundle and
66
+ `src/timing-chart.js` are inlined, so the file opens offline). The stage lives in the
67
+ `stage` *value*, not a column name, so stages can be added/renamed/merged without changing
68
+ the file's shape.
69
+
70
+ **Grouping came out of a spike, not a guess.** A single 85-row chart is unreadable, and the
71
+ plan already groups videos by module (first path segment of a video's first source), so the
72
+ report facets by module: one aggregate bar per module with a click to drill into its videos,
73
+ falling back to the per-video chart when the ledger spans a single module (`--flat` forces
74
+ it). Three layouts were prototyped — module filter list, small multiples, and
75
+ overview-plus-drill — and overview-plus-drill won: it is the only one that shows the whole
76
+ series without spending either a click per module or a wall of truncated labels. The spike
77
+ also surfaced that a fractional legend `y` collapses on short charts, which is why the
78
+ legend is placed from reserved pixels instead.
79
+
80
+ ### Renderers
81
+
82
+ | File | Covers | Vocabulary |
83
+ |---|---|---|
84
+ | `src/scene-svg.html` | the open lane — freeform SVG | author's own (`renderAt(t, svg, timeline)`) |
85
+ | `src/scene-core.js` | primitive library — `chart`, `hubSpoke`, `loopFlow`, `timeline`, `equation`, `sequence`, and the `flow` idiom | primitives take author params; `flow` keeps `client`, `server`, `hub`, `box` |
86
+ | `src/scene-code.js` | code walkthroughs, before/after refactors | code panels; for code excerpted from a repo, also real line numbers, elision lines, a focus window per sentence and a pause badge |
87
+
88
+ With no `work=` parameter, `src/scene-svg.html:34` resolves `?scene=<name>` to
89
+ `../scenes/<name>.js` — so the top-level `scenes/` is a live path for fixtures, not a
90
+ leftover directory. It holds the six `_pt-*` configs, one per primitive.
91
+
92
+ ### Publish — chapters and captions (stage 6)
93
+
94
+ `tools/publish.mjs` exists and the stage runs; render and assemble do not depend on it. The
95
+ agent writes `out/<name>/<NN>/publish.json` — `chapters[]` (segment keys plus a `title`) and a
96
+ `blurb`. The tool resolves the timecodes from the measured windows, validates, and emits two
97
+ files, copied into the video's own folder under `--dest` — `<NN>-<slug>/` — alongside
98
+ the MP4. `assemble`'s `--dest` now lands the MP4 in the **same** folder (both derive the
99
+ name from the script's `videoIndex` + title via `tools/naming.mjs`), so the fixed
100
+ `description.md` / `captions.srt` filenames can never collide across a series and the
101
+ destination sorts into watch order:
102
+
103
+ | File | Contents | Delivered to |
104
+ |---|---|---|
105
+ | `description.md` | the blurb, **then** the chapter block, paste-ready as **plain text** (no markdown — the field does not render it) | the video **description** box |
106
+ | `captions.srt` | the cue list, one cue per segment | the **caption track** upload |
107
+
108
+ Granularity is one chapter and one cue per segment, SRT only, and chapter titles are
109
+ **authored** rather than taken from the scene badges.
110
+
111
+ **The rationale is in [`PIPELINE.md`](./PIPELINE.md) §6** — why the stage sits before the
112
+ expensive ones, why authored titles beat reusing `stageNames`, and why chapters can only come
113
+ from the description. The calls it made are §3.8.
114
+
115
+ ### Work-dir layout — one directory per video
116
+
117
+ Each video's work dir (`out/<name>/<NN>/`) holds its own `timeline.js`, `manifest.json`,
118
+ and `scenes/`, so nothing is global:
119
+
120
+ - `build-audio` writes `timeline.js` beside `timeline.json` in `--out` (was `src/`).
121
+ - `assemble` reads `manifest.json` from `--work` (was `scenes/manifest.json`).
122
+ - the scene hosts resolve `timeline.js` and the scene config from a `?work=` query param,
123
+ forwarded by `render.mjs` and `assemble.mjs`.
124
+
125
+ Two videos can now coexist and be re-rendered independently; only Chromium's `--port` is
126
+ shared, so renders still run one at a time. This closed the structural gap that the
127
+ pipeline assumes one video at a time.
128
+
129
+ The **destination** mirrors this: each finished video lands in
130
+ `<destination>/<NN>-<slug>/` (`.mp4` + `description.md` + `captions.srt`), one folder per
131
+ video — see `tools/naming.mjs`. The folder name is the plan's `videoIndex` plus a slug of
132
+ the title, so listing the destination sorts into consumption order.
133
+
134
+ ### Beats anchor to narration lines, not to seconds
135
+
136
+ A scene used to declare `at: 6.6` — a hand-guessed second — which nothing measured.
137
+ On the module-07 pilot the "April" reveal fired at 6.6s while the words *In April*
138
+ begin at 12.6s: a six-second desync that `pacing-check` passed, because that check can
139
+ only compare beats against the **segment**, never against the narration inside it.
140
+
141
+ Two new stages close it:
142
+
143
+ - **`tools/align.mjs`** (stage 4b) transcribes the whole narration track with
144
+ `gemini-3.5-transcribe` (`wordTimestamp: true`), cuts the words into segments at the
145
+ measured boundaries, aligns each segment's words back onto that segment's script
146
+ words, and writes every narration **line's** span to `lines.json`.
147
+ Alignment is mandatory because the transcript is not an echo: it rewrote *forty* as
148
+ `40`, *fail* as `failed`, inserted commas and merged two sentences. Our punctuation
149
+ fixes the boundaries; the aligned words supply the times. Offsets are clamped
150
+ monotonic — the API occasionally returns an outlier (one plan put `A delta` at 3.7s
151
+ between neighbours at 12.4s and 14.2s).
152
+ - **`tools/resolve-beats.mjs`** resolves `{ id, line }` (or `{ lines: [from,to] }`) to
153
+ seconds, writes `beats.json`, and **fails** on a raw `at` with no `line`, a
154
+ nonexistent line, out-of-order beats, a baked `at` that disagrees with the measured
155
+ onset by >60 ms, or **a narration line with no beat**.
156
+
157
+ `pacing-check` now resolves from `beats.json` and flags any remaining hand-written
158
+ `at`. It also had a latent unit bug: `winEnd` subtracted the lead-in although `at` is
159
+ already window-relative, silently removing the last 0.6 s of every window. That only
160
+ looked harmless while beats were hand-placed nowhere near the end; line-anchored beats
161
+ run to the last spoken word and exposed it.
162
+
163
+ **The `/scene ↔ /audio` pad loop is demoted.** It existed because a hand-guessed beat
164
+ could not be trusted to fit its window, so padding was the routine repair. With dwells
165
+ measured from the lines a beat covers, `at + min` is inside the window by construction —
166
+ the module-07 pilot ran 123 beats across 30 scenes with zero shortfalls and zero pad.
167
+ A shortfall now means the scene *over-asked*: widen the `lines` range, redesign the
168
+ beat, and pad only as a last resort. `craft-video-audio` no longer presents padding as
169
+ the expected re-invocation.
170
+
171
+ Honest status: the three pilots' 87 scenes were migrated by a proposer (keyword +
172
+ monotonic DP over line text) with manual overrides on the beats it mis-read, dwells
173
+ recomputed from the measured line spans. Measured on the shipped pilot it moved
174
+ `april` from 6.60s to 13.20s. Per-scene line assignment still deserves a human pass —
175
+ the proposer gets roughly four in five right.
176
+
177
+ ### The transcribe request budget, and why stage 4b is one request per video
178
+
179
+ `gemini-3.5-transcribe` carries its own hard cap, discovered the expensive way: the
180
+ first alignment implementation transcribed per segment, and a 13-minute video cost ~30
181
+ requests. The API's `QuotaFailure` payload names the limit exactly —
182
+ `GenerateRequestsPerDayPerProjectPerModel`, `quotaValue: "100"`, scoped to the model
183
+ and the project, with a `RetryInfo.retryDelay` around 18 h. The Batch API is not
184
+ supported for this model, so the cap cannot be queued around, and being per-project it
185
+ cannot be spread across keys.
186
+
187
+ Stage 4b therefore transcribes `narration.wav` **once** and cuts the words into
188
+ segments at the measured boundaries. The model allows 30 minutes per request when word
189
+ timestamps are enabled, so longer tracks are cut at segment boundaries into several
190
+ passes. `--per-segment` survives as a fallback and is chosen automatically for a small
191
+ number of missing segments.
192
+
193
+ Two consequences worth keeping in view: alignment is a **one-time** cost per video
194
+ (the `words.json` cache is keyed on WAV + script text, and should be committed), and
195
+ the per-video length cap now has a second justification — `--max-min 25` keeps a video
196
+ inside a single transcribe pass, where the old 14 had only watchability behind it.
197
+
198
+ ### The beat model — the scene declares its own timing
199
+
200
+ A segment's visual is authored as a list of **beats**, each with a minimum dwell, declared
201
+ as `window.SCENE_BEATS` alongside the config. `pacing-check` resolves each beat against the
202
+ **measured** narration, reports shortfalls as prompts (exit 0), and errors only when a beat
203
+ cannot fit inside its segment at all. The dead-air check works in window-relative seconds
204
+ throughout, so it no longer mixes absolute event times with the relative window, and a
205
+ declared `hold` is no longer flagged as dead air.
206
+
207
+ The scene↔audio loop is wired: a shortfall is resolved by setting `pad` (hold silence) on
208
+ the script segment and `hold: true` on the beat, then re-running `/audio` — the TTS is
209
+ text-cached, so that is free and the window re-tiles wider.
210
+
211
+ **Two open questions about it** are recorded in §2.9.9.
212
+
213
+ ### Proven output
214
+
215
+ Two runs went end-to-end. **Neither's work dir survives** — see §2.11.
216
+
217
+ **Video 6 (SOLID)** — 7 segments, 205.70s, Aoede narration, audio verified at residual
218
+ 0.0155. Produced from real notes (`DEVELOPER-NOTES/.../low-level-design`) by the full
219
+ pipeline.
220
+
221
+ **A five-video series**, from `GenAI_Notes/Harness Engineering/modules/01-what-is-a-harness`:
222
+ 83 segments, 83 scenes, 29.0 min finished, every video audio-verified (residuals
223
+ 0.013–0.017). The first run to exercise the per-video work dirs at scale — five videos
224
+ coexisting, each re-rendered independently. Delivered to
225
+ `GenAI_Notes/Harness Engineering/modules/videos/`: the five MP4s plus video 02's
226
+ `description.md` and `captions.srt` are the surviving artifacts.
227
+
228
+ ### Checks (4 of them)
229
+
230
+ | Check | Ask | Catches |
231
+ |---|---|---|
232
+ | coverage (in planner) | is every source heading assigned? | silently dropped content |
233
+ | `script-check` | is the narration speakable and on budget? | URLs, markdown, uncovered sections |
234
+ | `pacing-check` | can a human read this? is anything still happening? | unreadable reveals, dead air |
235
+ | `verify-audio` | does the finished audio match the narration? | wrong-slice muxing |
236
+
237
+ ---
238
+
239
+ ## 2. Remaining
240
+
241
+ **Numbering has gaps on purpose.** §2.2, §2.8 and §2.10 were resolved or built and now
242
+ live in §1; §2.4 (no dispatcher, no preflight) and §2.5 (Node ≥22 undeclared) were resolved
243
+ by making the tools fail loud and recording the agent-driven decision in §3. Numbers are
244
+ left vacant rather than reused — §2.9.8 is cited by number from `SKILL.md` and
245
+ `PIPELINE.md`, so renumbering §2 would break those. A gap means "done", not "deleted".
246
+
247
+ ### 2.1 Portability — implemented, unverified off macOS
248
+
249
+ **Open — Windows Chromium has no command-free path yet.** The `win32` branch searches the
250
+ Playwright cache, so a Windows user must run `npx playwright install --only-shell` — a
251
+ command, against this skill's GUI-over-commands stance. *Candidate fix:* auto-detect the
252
+ preinstalled Edge/Chrome on `win32`
253
+ (`C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe`,
254
+ `C:\Program Files\Google\Chrome\Application\chrome.exe`) and use it before falling back to
255
+ the cache. Undecided, not implemented.
256
+
257
+ **Remaining:** one real run each on Linux and Windows — that alone flips this to "resolved".
258
+
259
+ ### 2.3 Packaging — restructured; publish + register remain
260
+
261
+ The repo is now an Agent Plugins plugin: root `plugin.json` + `.claude-plugin/plugin.json`
262
+ + `package.json`, the main skill self-contained under
263
+ `skills/explainer-video-from-coursework/` (docs + `tools/` + `src/` + `scenes/` +
264
+ `SETUP.md`), with the 8 stage skills as siblings at `skills/craft-video-<stage>/`. Targets:
265
+ Claude Code, Codex, DSH via **npm**; Antigravity via **git** (Cursor parked —
266
+ marketplace-only, manual review).
267
+
268
+ **Done:** the restructure, the manifests, both marketplace catalogs
269
+ (`.claude-plugin/marketplace.json` + `.agents/plugins/marketplace.json`, `source: npm`),
270
+ the `source: npm` syntax pinned against primary docs, the 8 stage skills moved to the top
271
+ level of `skills/` so all nine resolve on every platform, **one** package serving all four
272
+ surfaces (`cordis.patch.yml` + `lib/index.js` at the root, so DSH reads the same package
273
+ Claude Code and Codex do — no second package, no duplication, and no DeepSeek dependency
274
+ declared), a root `README.md`, MIT licensing across the manifests, `DISTRIBUTION.md`, and a
275
+ tag-triggered staged release in `.github/workflows/release.yml` + `RELEASING.md`.
276
+
277
+ **Remaining (user-owned):** the one-time npm Trusted Publisher config, then the first tagged
278
+ release, and the four installs on real machines.
279
+
280
+ ### 2.6 `planner.mjs` — remaining input gaps
281
+
282
+ Robustness against odd inputs is done: zero-content input errors instead of emitting
283
+ a 1-video/0-minute plan, bad paths (nonexistent, or a file instead of a directory)
284
+ error cleanly, and the `--index-targets` dedupe was **deleted rather than flagged**.
285
+ What remains:
286
+
287
+ - **Only `.md` is parsed.** `.mdx`, `.rst`, `.txt`, `.ipynb` are invisible — silent
288
+ omission, not an error. In one tree, 633 non-md files of which 72 matched the
289
+ code-extension list leave ~560 files unaccounted for.
290
+ - **The code-extension list is hardcoded** (`.java .py .ts .js .go .cpp .c .rs`) —
291
+ no `.kt .swift .rb .php .cs .scala .sh .sql`.
292
+ - **Coverage is structure-dependent.** The same file reported 0 orphans planned
293
+ alone and 2 orphans planned inside a larger tree. "0 orphans" is a property of
294
+ the run, not of the content.
295
+
296
+ ### 2.7 `render.mjs` parts are silent by convention, not by flag
297
+
298
+ `assemble.mjs` never passes `--narration` to `render.mjs`, which defaults it to
299
+ `out/narration.wav`. Parts come out silent only because no such file exists — a
300
+ project keeps its track at `out/<name>/narration.wav` instead. The silence is
301
+ intentional ("muxing narration onto each one would restart the whole track at every
302
+ boundary"), but it is inherited from a directory convention rather than stated:
303
+ any run that ever leaves a track at `out/narration.wav` re-muxes the full narration
304
+ onto every part and silently reintroduces the double-mux bug.
305
+
306
+ Needs: `assemble.mjs` to pass an explicit `--silent` (or `--narration ''`).
307
+
308
+ ---
309
+
310
+ ### 2.9 Findings from the `what-is-a-harness` run
311
+
312
+ Recorded while producing five videos end-to-end (83 narrated segments, 83 authored
313
+ scenes, 29 min of finished video) from
314
+ `GenAI_Notes/Harness Engineering/modules/01-what-is-a-harness`.
315
+
316
+ **Every item below was observed in that run, not theorised.** Priorities are tagged per
317
+ item — all P1 except 2.9.9; this file does not track the P0 queue. Two are marked
318
+ *(applied in-run)* — those exist only in the working tree that produced these videos:
319
+ `explainer-video/` is **not a git repository**, so there is no baseline to diff them
320
+ against and they are not preserved anywhere else.
321
+
322
+ #### 2.9.1 `draw` silently accepts the wrong shape *(P1)*
323
+
324
+ `reveal` takes a bare two-element array — `reveal: { node: ['seg@2.0', 'seg@3.0'] }` —
325
+ but `draw` takes a wrapper — `draw: { edge: { at: ['seg@2.0', 'seg@3.0'] } }`.
326
+ Passing the bare array to `draw` does not error:
327
+
328
+ - `scene-core.js:563` reads `drawSpec.at[0]`;
329
+ - `drawSpec.at` therefore resolves to `Array.prototype.at` — a *function* — so `[0]`
330
+ is `undefined`, `fadeIn(t, undefined, undefined)` returns `NaN`, and
331
+ `setAttribute('opacity', 'NaN')` is rejected by the DOM, leaving the attribute at
332
+ its default `1`.
333
+
334
+ The edge consequently renders **fully visible from frame 0**: a connector floating
335
+ before the nodes it joins exist. Both `script-check` and `pacing-check` stay green,
336
+ because neither looks at geometry — it was caught only by sampling the assembled MP4.
337
+
338
+ Observed in `v1-demo`. **Fix:** normalise both shapes at load, or validate `draw`
339
+ keys. *Small.*
340
+
341
+ #### 2.9.2 `size: { w }` silently disables a box's auto-height *(P1)*
342
+
343
+ `scene-core.js:158` is `if (kind === 'box' && !nd.size)`. Supplying *any* `size` —
344
+ even only `w` — skips the height computation entirely.
345
+
346
+ Widening a box to stop its member text overflowing therefore silently clips the last
347
+ member line against the bottom border. Observed in `v1-blur`: three members in a
348
+ `size: { w: 640 }` box rendered at the default `h: 132` instead of the required 160.
349
+
350
+ **Fix:** keep auto-height when `members` exist and `size.h` was not given explicitly.
351
+ *Small.*
352
+
353
+ #### 2.9.3 Flows that terminate on a wide node draw a stray ring *(P1)*
354
+
355
+ `scene-core.js:292` gives every flow target an arrival ring of fixed `r: 58`,
356
+ independent of node size. On a 470px `box` (or a 252px `hub`) that ring sits wholly
357
+ inside the node, so it reads as a random circle rather than an arrival highlight.
358
+
359
+ The skill already knows this, but only as example-scene lore:
360
+ `out/hld-full/scenes/lb-l4-vs-l7.js` carries the comment *"route THROUGH the boxes,
361
+ never INTO them"*. Nothing enforces it.
362
+
363
+ Observed in 4 scenes: `v1-demo`, `v1-loop`, `v3-agenttool`, `v3-ex-agenttool`.
364
+
365
+ **Fix:** warn at load when a target's half-width or half-height exceeds the ring
366
+ radius, or scale the ring to the node bounds. *Small.*
367
+
368
+ #### 2.9.4 Nothing asserts that a scene *opens* with something on screen *(P1)*
369
+
370
+ `pacing-check`'s dead-air rule subtracts the narration windows before measuring
371
+ silence, so "blank stage with a voice over it" is deliberately never flagged. A
372
+ scene can be entirely pacing-clean and still show an empty frame for seconds after a
373
+ hard cut.
374
+
375
+ Observed: **16 of 83** scenes had a first visual onset later than 2.5s; the worst —
376
+ `v1-support`, `v1-coding`, `v1-research` — sat empty for 5–6s.
377
+
378
+ `tools/onset-audit.mjs` was written during the run for exactly this: it computes each
379
+ scene's earliest visual event in seconds, and reported 83/83 clean once the fixes
380
+ landed. **Fix:** fold it into `pacing-check` as a third assertion. *Small — the
381
+ script already exists.*
382
+
383
+ #### 2.9.5 Nothing checks that text fits its container *(P1)*
384
+
385
+ Every checker is timing- and structure-only; overflow, clipping and overlap are
386
+ invisible to all four of them.
387
+
388
+ The clearest case was the stage badge in `scene-core.js`: a hardcoded 292px pill that
389
+ never measured its label, with the text merely centred inside it. Measured off a real
390
+ frame:
391
+
392
+ | Label | Text | Pill | Result |
393
+ |---|---|---|---|
394
+ | `CAPABILITY VS SYSTEM` | 273px | 292px | fits (10px margin) |
395
+ | `NOT AN AFTERTHOUGHT` | 284px | 292px | fits by ~4px — reads as touching |
396
+ | `EXAMPLE · CHAT TRANSFER` | 323px | 293px | overflows 15px per side |
397
+ | `EXAMPLE · AGENT AS A TOOL` | 341px | 291px | overflows 24 / 26px |
398
+
399
+ *(Applied in-run: the pill now measures with `textW()` and re-centres, right edge
400
+ pinned at `W − 74`, wrapped in `Math.max(292, …)` so short labels are unchanged.)*
401
+
402
+ Still unchecked and carrying the same exposure: box `members` (21px mono — roughly
403
+ 33 characters is the limit in a 470px box), box labels, and `cards` labels.
404
+
405
+ **Fix:** a headless pass asserting measured text width against container width.
406
+ **Largest item in this section.**
407
+
408
+ #### 2.9.6 `progress` defaults to on *(P1)*
409
+
410
+ `scene-core.js:324` is `if (OV.progress !== false)`. The overlay draws a 6px track
411
+ plus an amber fill that tracks playback. A scrubber is a *player* affordance, but it
412
+ is drawn into the frames, so it ends up burned into the exported MP4 — where the
413
+ player draws its own controls. All 83 scenes had to opt out explicitly.
414
+
415
+ **Fix:** default it off, or drop the overlay. *One line.*
416
+
417
+ #### 2.9.7 Re-rendering is all-or-nothing *(P1)*
418
+
419
+ `assemble.mjs:89` filters the segment order down to `--only` and then concatenates
420
+ only those parts — so `--only` emits a *one-segment* video and cannot patch an
421
+ existing cut. `assemble.mjs:178` then deletes every part on success.
422
+
423
+ Fixing 4 segments of video 1 therefore cost a full re-render (23 segments, ~19 min),
424
+ because the other 19 parts no longer existed. A reuse pass keyed on `part` mtime vs
425
+ `scene` + `timeline` mtime would have taken about two minutes.
426
+
427
+ **Fix:** add `--reuse` (skip segments whose part is newer than their scene and the
428
+ timeline), and/or keep parts by default. *Medium.*
429
+
430
+ #### 2.9.8 `SKILL.md`'s work-dir convention contradicts its own "Series scope" — **RESOLVED**
431
+
432
+ - **The contradiction:** "Series scope" says each video gets its own
433
+ `out/<name>/<NN>/`, but every stage command in `SKILL.md` showed `--out out/<name>`
434
+ / `--work out/<name>`. §1 *Work-dir layout* records the per-video layout as resolved,
435
+ yet the commands were never updated to match, so the agent had to reconcile the two
436
+ itself.
437
+ - **The stale sub-claim (do not re-add):** this entry used to say `--work` must be a
438
+ path *relative to `src/`*, citing the hosts resolving `'../' + WORK + '/timeline.js'`.
439
+ That was true of an older host; `render.mjs:81` now converts `--work` with
440
+ `pathToFileURL(resolve(WORK))`, so a cwd-relative **or absolute** path both work.
441
+ - **Resolved (2026-10-20).** Stage 4/5/8 commands in `SKILL.md` and `PIPELINE.md` now
442
+ show `out/<name>/<NN>`, the stale parenthetical in *Series scope* is gone, and the
443
+ `--work` rule is documented there as cwd-relative-or-absolute.
444
+ - **Resolved differently, and better, than the filed fix.** The filed fix was
445
+ documentation only — but documentation was not the failure mode: an agent that reads
446
+ the right path from `build-audio.mjs`'s own usage string still collides on a careless
447
+ re-run, because the harm is structural. `narration.wav`, `timeline.json`, `timeline.js`,
448
+ `lines.json`, `manifest.json` and the MP4 are named identically for every video, so a
449
+ second video pointed at one dir silently overwrites the first's timings and every later
450
+ stage reads the wrong video. New `tools/workdir.mjs` stamps a `.video-identity` marker
451
+ into the work dir and `build-audio`, `align` and `assemble` **refuse** a `videoIndex`
452
+ mismatch (exit 3) naming both videos. Permissive by construction: any layout is allowed
453
+ so long as one dir serves one video, and a same-index retitle (re-enrichment) updates
454
+ the marker instead of failing.
455
+ - **Related, fixed at the same time:** `build-audio --only` wrote the global
456
+ `narration.wav` / `timeline.json` / `timeline.js` from a single segment, replacing a
457
+ finished video's timeline with a one-scene one. A partial run now stops with a
458
+ segment-scoped `timeline.<key>.json` and touches no global artefact.
459
+
460
+ #### 2.9.9 The beat model: two things worth resolving *(P2)*
461
+
462
+ - **Nothing but the checker reads `SCENE_BEATS`.** All three scene hosts
463
+ (`scene-diagram.html`, `scene-svg.html`, `scene-code.html`) and `scene-core.js` reference
464
+ it **zero** times, so beats are a *lint* input and not a rendering input — yet `SKILL.md`
465
+ says each aligned beat "resolves to `max(narration_beat, visual_beat_min)`". Holds are in
466
+ fact realised by padding the audio, so either that sentence overstates the mechanism or a
467
+ renderer-side piece is missing.
468
+ - **It fails silently when undeclared.** `tools/pacing-check.mjs:174` yields `null`, and
469
+ both the beat branch (`:175`) and its fallback (`:210`) require `BEATS && BEATS.length` —
470
+ so a scene with no beats prints no beat line at all, rather than "none declared". You
471
+ cannot tell "no beats" from "beats declared and fine" without noticing the absent line.
472
+
473
+ Adoption on a real segment is also unverified: the six `_pt-*` fixtures declare
474
+ no beats, and the 83 harness configs no longer exist.
475
+
476
+ ---
477
+
478
+ ### 2.11 Repo metadata, before this is pushed anywhere *(P2)*
479
+
480
+ - No `README.md`. `SKILL.md` is written for an agent, not for a human arriving at the repo.
481
+ - No `LICENSE`.
482
+ - `package.json` is bare `{"type": "module"}` — no `name`, `description`, or `scripts`, so
483
+ `npm run` does nothing. There is no test suite; the five delivered videos are the golden
484
+ corpus.
485
+ - `.gitignore` carries two dead lines, `frames/` and `.explainer-video/` — neither path has
486
+ ever existed here. (`preview/` and `parts/` are absent right now too, but the tools
487
+ recreate them, so those entries stay.)
488
+
489
+ ---
490
+
491
+ ### 2.12 Shorts track — parked design *(P1)*
492
+
493
+ Shorts are YouTube's discovery surface; the long videos are the product, but the Shorts
494
+ are how anyone finds them. Parked here mid-design, with the open questions intact.
495
+
496
+ Settled so far:
497
+
498
+ - **Separate, optional track.** Presence of `shorts.json` is the switch — no flag. Absent,
499
+ stages 4–8 behave exactly as they do today; present, only the producing stage reads it,
500
+ and a stale one warns rather than blocking the main pipeline.
501
+ - **Authored right after the script (stage 3.5).** A Short is **re-narrated and
502
+ compressed**, not sliced out of the long audio. That matters: slicing would need cut
503
+ points that only exist after stage 4/4b, whereas a re-narrated Short gets fresh TTS and
504
+ therefore fresh timings. The one objection that does not apply here is the timing one.
505
+ - **Mining unit: the script's own segments.** Each is already a self-contained beat, so
506
+ mining per segment is an auditable partition rather than a vibe; 1–2 *adjacent* segments
507
+ are allowed when a single one is too thin. Candidates record disjointness, so three
508
+ Shorts cannot be mined out of the same 40 seconds.
509
+ - **Each candidate is scored on four axes:** hook potential (a counterintuitive claim, a
510
+ number, a common mistake, a before/after) · self-containment (what it references outside
511
+ itself) · vertical survivability (its lane — a wide `diagram` is the expensive case) ·
512
+ loop/payoff point (does it end somewhere that rewards a rewatch).
513
+ - **Compression targets 25–45 seconds**, not "under 2 minutes". 2:00 is the ceiling, not
514
+ the goal; retention falls off well before that.
515
+ - **Cost is real:** every Short is one TTS call + one alignment pass + one vertical render.
516
+ Mining ten per video is not free, which is an argument for choosing candidates well.
517
+
518
+ Blocked on one open question: `render.mjs` hardcodes a `1920×1080` viewport, and the scene
519
+ lanes are authored wide. Vertical needs the viewport parameterised **and** an answer to
520
+ crop-vs-re-author — cropping a wide diagram or code block to 9:16 either clips it or
521
+ shrinks it past phone legibility. That decision sizes the whole track.
522
+
523
+ Recorded disagreements (mine, not settled):
524
+
525
+ 1. **Misleading hooks have a hard ceiling.** Retention past the hook is measured; a promise
526
+ the video does not keep is the one pattern that gets actively suppressed.
527
+ 2. **The controllable lever is the number of independent attempts**, not the perfection of
528
+ one — which argues for breadth and cheap production over polish per Short.
529
+ 3. **For technical coursework the realistic goal is niche reach plus funnel** to the long
530
+ form, not broad virality: 5k views from people who would watch a 12-minute explainer is
531
+ worth more than 500k from people who will not.
532
+ 4. **Contiguous range vs ordered selection** is mostly moot while Shorts are re-narrated
533
+ (the compression pass may reorder freely), but if a Short ever reuses existing audio,
534
+ ordered selection becomes mandatory and audio splicing becomes the constraint.
535
+
536
+ ## 3. Design decisions worth challenging
537
+
538
+ These are the calls I made. Some may be wrong.
539
+
540
+ ### 3.0 Where scratch lives is an input, not an inference
541
+
542
+ `out/` resolved against whatever the cwd happened to be, described as "run from the
543
+ project" — a directory that was never defined, never checked, and not something the
544
+ skills could verify. One real run put 679 MB at `<repo>/out/…`, two levels above the
545
+ notes and outside the tree whose `.gitignore` already covered `out/`, simply because
546
+ that is where the shell was.
547
+
548
+ Scratch is now a Stage 0 input with a default — the directory that holds `out/`,
549
+ defaulting to the source's parent — and `planner` verifies the ignore rule instead of
550
+ assuming it: it walks up to the git work tree, asks `git check-ignore`, and appends
551
+ `/<rel>/` when the rule is missing, printing what it changed. Outside a repo it says
552
+ nothing, because there is nothing to ignore.
553
+
554
+ The same class of bug bit the other direction: a destination *inside* the source
555
+ (`<notesDir>/videos/`) is walked by `planner`, so a published `description.md` re-enters
556
+ as a note and becomes a phantom video titled after its folder. Hence `--exclude`
557
+ (repeatable) and a warning keyed on `description.md` — a filename only this pipeline
558
+ emits. Both are worth their small cost because the failure is silent and permanent
559
+ otherwise: you notice it when a video about nothing shows up in the series.
560
+
561
+ *Challenge if:* you would rather the tools **refuse** a scratch root inside a git work
562
+ tree that does not ignore it, than edit the user's `.gitignore` for them.
563
+
564
+ ### 3.1 Code vs agent boundary
565
+
566
+ Code does anything that must be exact (parsing, packing, coverage, timing math,
567
+ rendering, verification). The agent does judgment (titles, forms, scripts, scene
568
+ authoring). No LLM API is used for reasoning — only for TTS.
569
+
570
+ *Challenge if:* you think some judgment step is too expensive/variable to leave to
571
+ an agent, or that a deterministic step should be model-assisted.
572
+
573
+ ### 3.2 One config file per segment, not a single DSL
574
+
575
+ Each segment gets its own `scenes/*.js`. There is no cross-segment timeline in
576
+ config — `assemble` stitches windows.
577
+
578
+ *Challenge if:* you'd rather have one project-level config describing the whole
579
+ video, with segments as sections.
580
+
581
+ ### 3.3 Per-segment render + concat (hard cuts)
582
+
583
+ Chosen so segments stay independently reviewable and re-renderable.
584
+
585
+ *Tradeoff:* hard cuts, no cross-fades. Deferred by request.
586
+
587
+ ### 3.4 Timing is relative to narration, not absolute
588
+
589
+ `"srp@4.6"` resolves to `segment_start + 4.6`. This fixed a real desync where
590
+ hardcoded absolute times drifted when the voice changed.
591
+
592
+ *Challenge if:* scene authoring becomes harder to reason about than absolute
593
+ times.
594
+
595
+ ### 3.5 Four separate checkers rather than one
596
+
597
+ Each is small, single-purpose, and independently runnable.
598
+
599
+ *Challenge if:* this fragments the QA story instead of giving one "is this video
600
+ good?" gate.
601
+
602
+ ### 3.6 Text-keyed audio cache
603
+
604
+ Re-runs reuse a segment's WAV only if the spoken text is unchanged, so iterating on
605
+ scene timing never re-spends TTS quota.
606
+
607
+ *Challenge if:* the script changes for non-text reasons (voice, style, model) and
608
+ the cache silently serves stale audio — **this is a real hole**: `style`, `voice`
609
+ and `model` are NOT part of the cache key.
610
+
611
+ ### 3.7 The planner's duration model — recalibrated
612
+
613
+ The `×1.35` visual-overhead factor was a guess and ran **37% over** (video 6
614
+ predicted 4.7 min, actual 3.43). Now measured and centralized in
615
+ `tools/model.mjs`: `wpm 153` (508 words → 199s of audio) and `expansion 1.10`
616
+ (205.7s picture / 199s narration = 1.03, plus a buffer). `planner`,
617
+ `script-check` and `pacing-check` all import it, so they can't drift. Re-verify
618
+ against a code-heavy video, where `codeNarration 0.15` may still under-count.
619
+
620
+ ### 3.8 Chapter granularity is bounded by segment granularity
621
+
622
+ A chapter's time is its first segment's window start, so a chapter can **group** segments
623
+ but never **split** one. A boundary can only land where the narration already breaks.
624
+
625
+ *Challenge if:* a segment carries two ideas and deserves two chapters. Grouping is a
626
+ format-compatible later change; splitting needs a second timestamp source and is not.
627
+
628
+ ### 3.9 No dispatcher — the agent is the orchestrator
629
+
630
+ There is no `pipeline.mjs` or `bin` entry point. `SKILL.md` sequences the stages and the
631
+ agent composes the `tools/*.mjs` calls, because stages 1–2 are once-per-series, 3–8 are
632
+ once-per-video, and judgment steps (authored JSON) interleave with code. Each stage is also
633
+ its own skill.
634
+
635
+ *Challenge if:* you want a non-agent entry point — a CLI or CI job that runs the whole
636
+ series unattended — which would need a driver that encodes the series/per-video loop and
637
+ the judgment steps.
638
+
639
+ ---
640
+
641
+ ## Open questions
642
+
643
+ Unresolved, captured for later. Do not act on these yet.
644
+
645
+ ### Q1 · Artefacts attribution in `planner.mjs`
646
+
647
+ The rule at `planner.mjs:340–343` attaches a code file to a video by **path
648
+ prefix** of the video's source directory. It is:
649
+
650
+ - **depth-blind** — a phase-hub README (e.g. `distributed-systems-theory/README.md`)
651
+ inherits every demo in every sub-chapter beneath it (10 files for one video).
652
+ - **content-blind** — when one README is split across N videos, all N siblings get
653
+ the identical list, whether or not their own text mentions any demo.
654
+
655
+ It never *misses* (every referenced demo is present) — it only over-includes.
656
+ Counted on `hld-full`: 107 planned entries vs 60 referenced by the carrying video's
657
+ own text; 47 inherited, 15 surviving after curation.
658
+
659
+ Open because:
660
+
661
+ - **The field's meaning is undecided.** Three rules give different answers for the
662
+ same chapter (`database-types`: 9 vs 3 vs 6 entries):
663
+ 1. *prefix* — current.
664
+ 2. *reference-evidenced* — a demo belongs to a video iff that video's own section
665
+ text mentions it (supported by the source's own structure; concept videos then
666
+ carry zero code artefacts).
667
+ 3. *topic-attributed* — map each demo to the video covering its topic (needs a
668
+ hand-built demo→topic ontology the source does not contain).
669
+ - **Nothing consumes `artefacts`.** The planner writes it and `plan.md` prints its
670
+ count; no renderer, checker, or scene reads it. Informational only today.
671
+
672
+ ### Q2 · `apply-enrichment.mjs` merge drops artefacts
673
+
674
+ The `mergeInto` branch unions `sections`, `sectionSpans` and `words` — but **not**
675
+ `artefacts`. Merging a video silently drops the folded-in video's code files from
676
+ the target's list. Never fired on `hld-full` (curation used `skip`, not
677
+ `mergeInto`), but it is a latent drop.
678
+
679
+ Related: the agreed "let `/enrich` curate artefacts" escape hatch is **not
680
+ implemented** — `enrichment.json` has no `artefacts` field, `apply-enrichment`
681
+ passes the list through untouched, and there is no subset validation.
682
+
683
+ ---
684
+
685
+ ## 4. Acceptance test
686
+
687
+ Hand a fresh agent only the skill — no conversation history — point it at a notes
688
+ directory, and see whether it produces a video without asking questions.
689
+
690
+ Today the likely failure is no longer scene authoring — that is documented — but the
691
+ platform-specific bits: Chromium discovery and the key's OS secret store, both implemented
692
+ but only verified on macOS (§2.1).
693
+
694
+ ---
695
+
696
+ ## 5. Explicitly out of scope for now
697
+
698
+ - Scripts and videos 1–5 and 7
699
+ - Cross-fades between segments
700
+ - Per-member highlighting in the UML box kind
701
+ - Productising the load-balancer project (now legacy duplication)
702
+
703
+ ---
704
+
705
+ ## 6. Planned skill family
706
+
707
+ This skill is `explainer-video-from-coursework` — it assumes structured coursework
708
+ notes (markdown READMEs, a heading per topic, `code/` demos) and is **not known to
709
+ generalise** beyond that. Three siblings are planned, split by where the source
710
+ material comes from:
711
+
712
+ | Skill | Source | What it adds over this one |
713
+ |---|---|---|
714
+ | `explainer-video-from-coursework` *(this)* | structured coursework notes | decompose + prove coverage + narrate + animate |
715
+ | `explainer-video-from-source` | a general source — docs, papers, codebases, arbitrary notes | generalise the planner's assumptions (README layout, heading structure, `code/` convention) so non-coursework sources decompose cleanly |
716
+ | `explainer-video` | no source — a narrow-to-broad topic description | author the study material first (a pre-stage that builds the notes), then feed it through the same plan→video pipeline |
717
+ | `explainer-video-from-codebase` | a codebase's major code flows (Python, TS/JS, Go, C, C++, or a mix) | mine the code (tree-sitter, grouped prod/test/eval/dev, tests as evidence), propose candidate flows, the agent proposes the series with reasons, the user approves the flow list, then a coursework-shaped plan reuses stages 3–8 |
718
+
719
+ `explainer-video-from-codebase` exists: its `SKILL.md` is the contract of record for the
720
+ whole code-flow pipeline, and four stage skills ship beside it, `craft-codeflow-mine`
721
+ (stages 1 and 1t), `craft-codeflow-flows` (1b and 1b′), `craft-codeflow-plan` (1c) and
722
+ `craft-codeflow-scene` (5). They are held to the dual-file rule by
723
+ `scripts/check-skill-sync.mjs` in CI, and stages 2–4b and 6–8 are this skill's own stage
724
+ skills. The plugin that ships both entry skills is now named `explainer-video`; the skill names are unchanged. The other two are not started. Recorded here so the naming is reserved and
725
+ the "from coursework" scope of this skill stays explicit.