explainer-video 0.0.0-stage → 0.4.1

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