@hajimism/dek 0.1.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 (187) hide show
  1. package/LICENSE +21 -0
  2. package/README.ja.md +75 -0
  3. package/README.md +75 -0
  4. package/package.json +65 -0
  5. package/src/cli/address.ts +53 -0
  6. package/src/cli/build.ts +85 -0
  7. package/src/cli/check.ts +97 -0
  8. package/src/cli/commands.ts +851 -0
  9. package/src/cli/contract.ts +444 -0
  10. package/src/cli/cues.ts +18 -0
  11. package/src/cli/files.ts +309 -0
  12. package/src/cli/flags.ts +295 -0
  13. package/src/cli/format.ts +156 -0
  14. package/src/cli/goto.ts +96 -0
  15. package/src/cli/init.ts +136 -0
  16. package/src/cli/keep-alive.ts +72 -0
  17. package/src/cli/lint.ts +84 -0
  18. package/src/cli/ls.ts +119 -0
  19. package/src/cli/main.ts +231 -0
  20. package/src/cli/marks.ts +16 -0
  21. package/src/cli/mv.ts +37 -0
  22. package/src/cli/new.ts +64 -0
  23. package/src/cli/pdf.ts +16 -0
  24. package/src/cli/qr.ts +466 -0
  25. package/src/cli/ref.ts +165 -0
  26. package/src/cli/rehearse.ts +42 -0
  27. package/src/cli/result.ts +161 -0
  28. package/src/cli/scope.ts +308 -0
  29. package/src/cli/serve.ts +125 -0
  30. package/src/cli/shot.ts +134 -0
  31. package/src/cli/show.ts +103 -0
  32. package/src/cli/sync.ts +37 -0
  33. package/src/cli/text.ts +479 -0
  34. package/src/cli/theme.ts +56 -0
  35. package/src/cli/tty.ts +81 -0
  36. package/src/cli/usage.ts +198 -0
  37. package/src/cli/video.ts +17 -0
  38. package/src/cli/voice.ts +113 -0
  39. package/src/cli.ts +4 -0
  40. package/src/core/assets.ts +239 -0
  41. package/src/core/build.ts +77 -0
  42. package/src/core/bundled-theme.ts +15 -0
  43. package/src/core/capture-go.ts +72 -0
  44. package/src/core/chrome.ts +128 -0
  45. package/src/core/config-keys.ts +82 -0
  46. package/src/core/config.ts +95 -0
  47. package/src/core/css-scan.ts +158 -0
  48. package/src/core/css-transform.ts +140 -0
  49. package/src/core/css-value.ts +127 -0
  50. package/src/core/css.ts +545 -0
  51. package/src/core/cue.ts +187 -0
  52. package/src/core/deck-paths.ts +34 -0
  53. package/src/core/diagnostic.ts +114 -0
  54. package/src/core/document.ts +218 -0
  55. package/src/core/error.ts +31 -0
  56. package/src/core/escape.ts +16 -0
  57. package/src/core/fill.ts +100 -0
  58. package/src/core/finish-beat.ts +30 -0
  59. package/src/core/github.ts +234 -0
  60. package/src/core/html-scan.ts +253 -0
  61. package/src/core/html.ts +234 -0
  62. package/src/core/in-page-go.ts +128 -0
  63. package/src/core/lines.ts +43 -0
  64. package/src/core/lint/asset-refs.ts +109 -0
  65. package/src/core/lint/context.ts +116 -0
  66. package/src/core/lint/files.ts +117 -0
  67. package/src/core/lint/keys.ts +48 -0
  68. package/src/core/lint/morph.ts +91 -0
  69. package/src/core/lint/script.ts +102 -0
  70. package/src/core/lint/shared-styles.ts +165 -0
  71. package/src/core/lint/slide-html.ts +466 -0
  72. package/src/core/lint/slide-script.ts +179 -0
  73. package/src/core/lint/theme-css.ts +295 -0
  74. package/src/core/lint/timing.ts +66 -0
  75. package/src/core/lint/tokens.ts +520 -0
  76. package/src/core/lint/voice.ts +76 -0
  77. package/src/core/lint.ts +137 -0
  78. package/src/core/live-protocol.ts +130 -0
  79. package/src/core/marks.ts +198 -0
  80. package/src/core/mv.ts +293 -0
  81. package/src/core/ogp.ts +99 -0
  82. package/src/core/optional.ts +24 -0
  83. package/src/core/overflow.ts +220 -0
  84. package/src/core/page-pool.ts +53 -0
  85. package/src/core/page.ts +66 -0
  86. package/src/core/parse.ts +307 -0
  87. package/src/core/path.ts +58 -0
  88. package/src/core/pdf.ts +71 -0
  89. package/src/core/playwright-module.d.ts +43 -0
  90. package/src/core/playwright-motion.ts +42 -0
  91. package/src/core/playwright-sheet.ts +32 -0
  92. package/src/core/playwright-visual.ts +391 -0
  93. package/src/core/playwright-worker.ts +5 -0
  94. package/src/core/playwright.ts +431 -0
  95. package/src/core/position.ts +146 -0
  96. package/src/core/presenter-state.ts +43 -0
  97. package/src/core/presenter.ts +131 -0
  98. package/src/core/prose.ts +16 -0
  99. package/src/core/pseudo-text.ts +67 -0
  100. package/src/core/rail-width.ts +4 -0
  101. package/src/core/ref-name.ts +110 -0
  102. package/src/core/ref.ts +238 -0
  103. package/src/core/resolve.ts +228 -0
  104. package/src/core/rumdl.ts +99 -0
  105. package/src/core/safe-fs.ts +196 -0
  106. package/src/core/sarif.ts +135 -0
  107. package/src/core/schema.ts +89 -0
  108. package/src/core/script-lines.ts +53 -0
  109. package/src/core/sheet.ts +223 -0
  110. package/src/core/shot/morph.ts +80 -0
  111. package/src/core/shot/motion.ts +187 -0
  112. package/src/core/shot/still.ts +216 -0
  113. package/src/core/shot-cache.ts +140 -0
  114. package/src/core/size.ts +11 -0
  115. package/src/core/skeleton-record.ts +51 -0
  116. package/src/core/skeleton.ts +89 -0
  117. package/src/core/slide-draw.ts +165 -0
  118. package/src/core/slide-eval-worker.ts +58 -0
  119. package/src/core/slide-measure.ts +272 -0
  120. package/src/core/slide-script-eval.ts +258 -0
  121. package/src/core/slide-script.ts +252 -0
  122. package/src/core/spawn.ts +128 -0
  123. package/src/core/step.ts +94 -0
  124. package/src/core/still-page.ts +70 -0
  125. package/src/core/suggest.ts +33 -0
  126. package/src/core/sync.ts +316 -0
  127. package/src/core/text-contrast.ts +466 -0
  128. package/src/core/theme-excerpt.ts +187 -0
  129. package/src/core/theme-facts.ts +51 -0
  130. package/src/core/timeline.ts +216 -0
  131. package/src/core/timing.ts +88 -0
  132. package/src/core/toml-keys.ts +349 -0
  133. package/src/core/url-attributes.ts +103 -0
  134. package/src/core/video-document.ts +15 -0
  135. package/src/core/visual-cache.ts +100 -0
  136. package/src/core/visual.ts +558 -0
  137. package/src/core/voice.ts +321 -0
  138. package/src/core/zod.ts +79 -0
  139. package/src/runtime/browser.ts +203 -0
  140. package/src/runtime/clone.ts +52 -0
  141. package/src/runtime/deck-control.ts +43 -0
  142. package/src/runtime/fit.ts +32 -0
  143. package/src/runtime/go.ts +56 -0
  144. package/src/runtime/input.ts +128 -0
  145. package/src/runtime/laser-view.ts +110 -0
  146. package/src/runtime/laser.ts +145 -0
  147. package/src/runtime/live-host.ts +33 -0
  148. package/src/runtime/live-reload.ts +29 -0
  149. package/src/runtime/live.ts +88 -0
  150. package/src/runtime/marks-view.ts +74 -0
  151. package/src/runtime/motion.ts +113 -0
  152. package/src/runtime/navigator.ts +189 -0
  153. package/src/runtime/player.ts +28 -0
  154. package/src/runtime/presenter-view.ts +289 -0
  155. package/src/runtime/presenter.ts +52 -0
  156. package/src/runtime/rail-view.ts +229 -0
  157. package/src/runtime/rail.ts +52 -0
  158. package/src/runtime/rehearse.ts +264 -0
  159. package/src/runtime/routes.ts +142 -0
  160. package/src/runtime/settle.ts +45 -0
  161. package/src/runtime/slide.d.ts +28 -0
  162. package/src/runtime/socket.ts +143 -0
  163. package/src/runtime/stage.ts +259 -0
  164. package/src/runtime/step.ts +139 -0
  165. package/src/runtime/window.ts +38 -0
  166. package/src/server/auth.ts +210 -0
  167. package/src/server/deck-snapshot.ts +146 -0
  168. package/src/server/dev.ts +251 -0
  169. package/src/server/hub.ts +104 -0
  170. package/src/server/lan.ts +51 -0
  171. package/src/server/lock.ts +97 -0
  172. package/src/server/pages.ts +71 -0
  173. package/src/server/pairing.ts +40 -0
  174. package/src/server/project-watch.ts +142 -0
  175. package/src/server/rooms.ts +133 -0
  176. package/src/server/routes.ts +286 -0
  177. package/src/server/serial.ts +25 -0
  178. package/src/server/watch.ts +308 -0
  179. package/src/theme/default.css +175 -0
  180. package/src/video/bake.ts +152 -0
  181. package/src/video/mux.ts +103 -0
  182. package/src/video/recorder.ts +115 -0
  183. package/src/video/sidecar.ts +60 -0
  184. package/src/video/worker.ts +51 -0
  185. package/src/voice/engine.ts +177 -0
  186. package/src/voice/synth.ts +127 -0
  187. package/src/voice/wav.ts +115 -0
@@ -0,0 +1,316 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { loadConfig } from "./config.ts";
4
+ import { type DeckPaths, deckPaths } from "./deck-paths.ts";
5
+ import { parseRefSource, refDir, refTitle } from "./ref.ts";
6
+ import { asResolvedDeck, listSlides, type ResolvedDeck, SLIDE_SIDECARS } from "./resolve.ts";
7
+ import { outputDir, readSourceIfExists, removeInside, writeInside } from "./safe-fs.ts";
8
+ import { frontmatterJsonSchema } from "./schema.ts";
9
+ import { deckLayouts, sectionSkeleton } from "./skeleton.ts";
10
+ import { type SkeletonRecord, skeletonRecord } from "./skeleton-record.ts";
11
+
12
+ /**
13
+ * `updated` lists skeletons nobody edited, rewritten because the script moved on; `removed`, the
14
+ * ones whose section is gone from the script. `kept` lists the slides whose section is gone too
15
+ * but that sync left alone, being the author's. `dekFiles` is what became of dek's own files at
16
+ * the project root.
17
+ */
18
+ export type SyncResult = {
19
+ created: string[];
20
+ updated: string[];
21
+ removed: string[];
22
+ kept: string[];
23
+ dekFiles: FileChanges;
24
+ };
25
+
26
+ /** Files a write made (`created`) or brought up to date (`updated`); a current one is in neither. */
27
+ export type FileChanges = { created: string[]; updated: string[] };
28
+
29
+ /** What a write did to one file; undefined when it was already current and left alone. */
30
+ type Written = "created" | "updated" | undefined;
31
+
32
+ function writeIfChanged(path: string, contents: string, root: string): Written {
33
+ const current = readSourceIfExists(path, root);
34
+ if (current === contents) {
35
+ return undefined;
36
+ }
37
+ writeInside(path, contents, root);
38
+ return current === undefined ? "created" : "updated";
39
+ }
40
+
41
+ /**
42
+ * dek's own files at the project root, in the order they are written: the frontmatter schema and
43
+ * the slide types an editor reads, and AGENTS.md, whose dek block agents read. They follow this
44
+ * version of dek and the project's refs, not any one deck.
45
+ */
46
+ export function dekFilePaths(root: string): string[] {
47
+ return dekFileWriters(root).map(([path]) => path);
48
+ }
49
+
50
+ function dekFileWriters(root: string): Array<[path: string, write: () => Written]> {
51
+ return [
52
+ [join(root, ".dek", "schema.json"), () => writeFrontmatterSchema(root)],
53
+ [join(root, ".dek", "slide.d.ts"), () => writeSlideTypes(root)],
54
+ [join(root, "AGENTS.md"), () => writeAgentsMd(root)],
55
+ ];
56
+ }
57
+
58
+ /**
59
+ * Brings dek's own files up to date, writing only the ones that differ. They change when dek or
60
+ * the project's refs do, so the first command to run after either, whichever it is, reports them.
61
+ */
62
+ export function writeDekFiles(root: string): FileChanges {
63
+ const changes: FileChanges = { created: [], updated: [] };
64
+ for (const [path, write] of dekFileWriters(root)) {
65
+ const written = write();
66
+ if (written) {
67
+ changes[written].push(path);
68
+ }
69
+ }
70
+ return changes;
71
+ }
72
+
73
+ const slideTypesPath = join(import.meta.dir, "..", "runtime", "slide.d.ts");
74
+
75
+ /** Compiler settings that let an editor type-check slide scripts against `.dek/slide.d.ts`. */
76
+ export function defaultTsconfig(): string {
77
+ return `${JSON.stringify(
78
+ {
79
+ compilerOptions: {
80
+ lib: ["ESNext", "DOM", "DOM.Iterable"],
81
+ // Slide scripts run in the browser; keep Node and Bun globals out of reach.
82
+ types: [],
83
+ target: "ESNext",
84
+ module: "Preserve",
85
+ moduleDetection: "force",
86
+ strict: true,
87
+ noEmit: true,
88
+ },
89
+ include: [".dek/*.d.ts", "decks/*/slides/*.ts"],
90
+ },
91
+ null,
92
+ 2,
93
+ )}\n`;
94
+ }
95
+
96
+ /**
97
+ * Keeps `.dek/slide.d.ts` in step with this dek. An unchanged file is left
98
+ * alone so the editor's TypeScript server does not reload on every sync.
99
+ * `tsconfig.json` is the project's; only `dekc init` writes one.
100
+ */
101
+ function writeSlideTypes(root: string): Written {
102
+ return writeIfChanged(
103
+ join(root, ".dek", "slide.d.ts"),
104
+ readFileSync(slideTypesPath, "utf8"),
105
+ root,
106
+ );
107
+ }
108
+
109
+ function writeFrontmatterSchema(root: string): Written {
110
+ const schema = `${JSON.stringify(frontmatterJsonSchema(), null, 2)}\n`;
111
+ return writeIfChanged(join(root, ".dek", "schema.json"), schema, root);
112
+ }
113
+
114
+ export function syncDeck(dir: string): SyncResult;
115
+ export function syncDeck(source: ResolvedDeck): SyncResult;
116
+ export function syncDeck(input: string | ResolvedDeck): SyncResult {
117
+ const { project, deck } = asResolvedDeck(input);
118
+ const paths = deckPaths(deck.dir);
119
+ outputDir(paths.slides, project.root);
120
+ const layouts = deckLayouts(deck.dir);
121
+ const record = skeletonRecord(deck.dir);
122
+
123
+ const existing = new Map(listSlides(deck.dir).map((slide) => [slide.slug, slide.path]));
124
+ const created: string[] = [];
125
+ const updated: string[] = [];
126
+ const { removed, kept } = removeOrphanSkeletons(
127
+ { root: project.root, paths, record },
128
+ [...existing].filter(([slug]) => !deck.deck.sections.some((section) => section.slug === slug)),
129
+ );
130
+
131
+ for (const [index, section] of deck.deck.sections.entries()) {
132
+ const skeleton = sectionSkeleton(deck.deck, index, layouts);
133
+ const current = existing.get(section.slug);
134
+ if (current) {
135
+ const html = readFileSync(current, "utf8");
136
+ if (!record.owns(html, skeleton)) {
137
+ continue;
138
+ }
139
+ if (html !== skeleton) {
140
+ writeInside(current, skeleton, project.root);
141
+ updated.push(current);
142
+ }
143
+ record.keep(skeleton);
144
+ continue;
145
+ }
146
+ const path = paths.slide(section.slug, ".html");
147
+ if (existsSync(path)) {
148
+ continue;
149
+ }
150
+ writeInside(path, skeleton, project.root);
151
+ created.push(path);
152
+ record.keep(skeleton);
153
+ }
154
+ record.save();
155
+
156
+ return { created, updated, removed, kept, dekFiles: writeDekFiles(project.root) };
157
+ }
158
+
159
+ /**
160
+ * A slide whose section is gone from the script is still dek's own output while it is a skeleton
161
+ * nobody edited and nothing sits beside it: the script no longer asks for it, and it holds nothing
162
+ * the script could not write again. Anything the author touched stays, and lint names it
163
+ * (DEK002). A section renamed in the script gets a fresh skeleton under its new id, so nothing is
164
+ * lost there either; the same holds for an id that flickers while a heading is being typed.
165
+ */
166
+ function removeOrphanSkeletons(
167
+ { root, paths, record }: { root: string; paths: DeckPaths; record: SkeletonRecord },
168
+ orphans: [string, string][],
169
+ ): { removed: string[]; kept: string[] } {
170
+ const removed: string[] = [];
171
+ const kept: string[] = [];
172
+ for (const [slug, path] of orphans) {
173
+ const html = readFileSync(path, "utf8");
174
+ const beside = ([...SLIDE_SIDECARS, ".js"] as const).some((ext) =>
175
+ existsSync(paths.slide(slug, ext)),
176
+ );
177
+ if (!record.owns(html, undefined) || beside) {
178
+ kept.push(path);
179
+ if (record.owns(html, undefined)) {
180
+ record.keep(html);
181
+ }
182
+ continue;
183
+ }
184
+ removeInside(path, root);
185
+ removed.push(path);
186
+ }
187
+ return { removed, kept };
188
+ }
189
+
190
+ /**
191
+ * AGENTS.md is shared: dek owns only the block between these markers and rewrites it in place.
192
+ * What the author writes around it is theirs.
193
+ */
194
+ const AGENTS_BEGIN =
195
+ "<!-- dek:begin (dek rewrites this block; write your own notes outside it) -->";
196
+ const AGENTS_END = "<!-- dek:end -->";
197
+
198
+ /**
199
+ * Writes dek's block into AGENTS.md. An unchanged file is left alone, so a sync on every save does
200
+ * not touch it.
201
+ */
202
+ export function writeAgentsMd(root: string): Written {
203
+ const path = join(root, "AGENTS.md");
204
+ return writeIfChanged(
205
+ path,
206
+ withAgentsBlock(readSourceIfExists(path, root), agentsMd(root)),
207
+ root,
208
+ );
209
+ }
210
+
211
+ /**
212
+ * `current` with dek's block set to `body`. The block is replaced where it is, the first complete
213
+ * begin-end pair counting from the begin nearest its end, so a stray marker never swallows the
214
+ * author's lines. A file that is exactly `body` without markers, as dek wrote it before it used
215
+ * them, is dek's to replace; any other file keeps every line and gets the block appended.
216
+ */
217
+ function withAgentsBlock(current: string | undefined, body: string): string {
218
+ const block = `${AGENTS_BEGIN}\n${body}${AGENTS_END}\n`;
219
+ if (current === undefined || current.trim() === "" || current === body) {
220
+ return block;
221
+ }
222
+ const lines = current.split(/(?<=\n)/);
223
+ let begin: number | undefined;
224
+ for (const [index, line] of lines.entries()) {
225
+ if (line.startsWith("<!-- dek:begin")) {
226
+ begin = index;
227
+ } else if (begin !== undefined && line.trimEnd() === AGENTS_END) {
228
+ return [...lines.slice(0, begin), block, ...lines.slice(index + 1)].join("");
229
+ }
230
+ }
231
+ return `${current.endsWith("\n") ? current : `${current}\n`}\n${block}`;
232
+ }
233
+
234
+ /**
235
+ * What dek's block in AGENTS.md says. It names nothing a deck's theme decides: each deck owns its
236
+ * theme.css, so a class list read from any one theme is wrong for the decks whose copy has grown,
237
+ * and it would sit in every agent's context. `dekc theme` reads the deck's own.
238
+ */
239
+ function agentsMd(root: string): string {
240
+ return `# dek
241
+
242
+ A build system for talks. Write what you will say; dek builds, measures, and ships the rest.
243
+
244
+ ## Principles
245
+
246
+ - \`script.md\` is the source of truth for order, script, and timing.
247
+ - Each slide is a \`<section class="slide">\` fragment.
248
+ - Conventions are enforced by lint. A deck is not done while \`dekc lint --visual\` fails. Passing it means nothing measurable is wrong, not that the deck is good.
249
+
250
+ ## Conventions
251
+
252
+ - One \`##\` heading is one slide. HTML lives in \`slides/<id>.html\`.
253
+ - Each deck owns its \`theme.css\`. Before writing a slide, run \`dekc theme\` in the deck for the classes, tokens, and layouts it defines, and \`dekc theme <layout>\` for a layout's markup.
254
+ - Shared look lives in \`theme.css\`. Decoration only one slide uses lives in \`slides/<id>.css\`, which is scoped to that slide.
255
+ - Use only classes defined in \`theme.css\` or in that slide's own \`slides/<id>.css\`. The theme holds at most \`max_classes\` in \`dek.toml\`, 40 by default (\`DEK013\`); classes in \`slides/<id>.css\` do not count, so keep a class only one slide uses there.
256
+ - A rule in \`slides/<id>.css\` weighs as if it were written at the end of \`theme.css\`: it beats the theme's \`.slide .x\`, but not a more specific rule such as a layout's \`.slide[data-layout="split"] .x\` or the beat state \`.slide.is-current [data-step]\`. To override one of those, write the same selector.
257
+ - Color, type, space, radius, and motion in either stylesheet use token \`var()\` only. A value only one slide uses can be a token of its own on that slide's \`.slide\` rule in \`slides/<id>.css\`.
258
+ - Do not add \`<style>\`, \`style=\`, \`<script>\`, event handler attributes (\`onclick=\` and the like), or \`javascript:\` URLs inside slide HTML.
259
+ - Motion CSS cannot express lives in \`slides/<id>.ts\`: \`export default { motion: { <step>: ms }, draw(slide, { index, step, t }) {} } satisfies DekSlide\`. \`DekSlide\` is global, from \`.dek/slide.d.ts\`; do not import it. Key the slide's arrival, before its first beat, as \`"0"\`: every \`data-step\` element is hidden there, so draw what the slide shows before anything happens. Draw from \`t\` alone and set everything you touch on every call, with no timers and no imports, so video and screenshots can seek it. In \`draw\`, find elements by data-* attributes from the slide it is given, never by class or through \`document\`: the built deck holds every slide.
260
+ - Keep the deck self-contained: no remote URLs and no paths outside the deck.
261
+ - Every slide carries its place in \`script.md\` as \`--dek-slide-number\` and \`--dek-slide-count\`. Print a folio from them in \`theme.css\`, never by hand: \`.slide { counter-reset: folio var(--dek-slide-number) }\`, then \`content: counter(folio)\`. It follows the script as slides move.
262
+
263
+ ## Checking a slide
264
+
265
+ - \`dekc check <slug> --shot\` lints one slide and screenshots it at its last beat.
266
+ - In \`dekc check <slug> --json\`, \`fill\` says how much of the frame the slide fills at its last beat, and where: \`coverage\`, the \`box\` it lies in, and \`rows\` and \`columns\`, the share of each tenth. It counts what the audience reads or looks at: text, pictures, and painted boxes that hold nothing, as a chart's bars. A card counts by what it holds, so one with its words at the top leaves its lower half empty; decoration under \`aria-hidden\` counts too. Read it before you open the shot to see whether a slide is sparse or leaves a band empty. Judge by \`rows\`, \`columns\`, and \`box\`, where an empty band is a run of zeros: \`coverage\` alone says little, since a chapter door in large type and a dense slide can share it. Whether a band left empty is right for the slide is yours to judge.
267
+ - A shot's path names what it drew, and an edit to the slide or the theme replaces the file. Never reuse a shot's path from before an edit: run \`dekc shot <slug>\` again, which shoots only what changed, and read the path it prints.
268
+ - \`dekc shot --sheet\` tiles every slide on one image: read it to judge the deck's balance in one look, then open a slide's own shot for detail.
269
+ - Mark decoration \`aria-hidden="true"\`: a glow that bleeds off the slide, or a sample of text the talk shows as unreadable. Lint measures neither overflow nor contrast on it, and screen readers skip it, so never mark text the audience should read: \`DEK029\` warns of text under it.
270
+ - When a hint sends a fix to \`theme.css\`, make it there, not in \`slides/<id>.css\`: the theme alone draws it that way, so other slides share the problem, and one change fixes them all.
271
+ - One shot shows no motion. \`dekc shot <slug> --motion\` lays the slide's beats out as rows, each held at moments through everything it moves and ending as the shot does. \`dekc shot <a> --to <b> --at 0.5\` freezes the view transition between any two slides.
272
+
273
+ ## After a rehearsal
274
+
275
+ - \`dekc marks\` lists the beats the speaker marked while rehearsing aloud (\`m\` in the presenter view): the words they stumbled over are \`was\`, at \`line\` in \`script.md\`. Rewrite those beats to be easier to say, and keep each beat's heading so its mark follows it; \`status\` turns \`edited\` once the words changed.
276
+ - Leave \`dekc marks clear\` to the speaker: only saying the new words aloud tells whether they work.
277
+
278
+ ## Before you report a deck as done
279
+
280
+ - \`dekc lint --visual\` passes.
281
+ - You have read \`dekc shot --sheet\` and judged the deck's balance.
282
+ - Say what you could not judge, such as the argument and the timing, and leave it to the author.
283
+ ${referencesSection(root)}
284
+ For commands, run \`dekc help --agent\`.
285
+ `;
286
+ }
287
+
288
+ /**
289
+ * The refs dek.toml pins, one line each, so an agent knows which decks it may
290
+ * read as models. Empty when there are none, leaving AGENTS.md as before. A
291
+ * dek.toml that does not parse leaves it empty too: sync runs on every save
292
+ * in the dev server, and the commands that read dek.toml report the error.
293
+ */
294
+ function referencesSection(root: string): string {
295
+ let pinned: Record<string, string>;
296
+ try {
297
+ pinned = loadConfig(join(root, "dek.toml")).refs ?? {};
298
+ } catch {
299
+ return "";
300
+ }
301
+ const refs = Object.keys(pinned).sort();
302
+ if (refs.length === 0) {
303
+ return "";
304
+ }
305
+ const lines = refs.map((name) => {
306
+ const title = refTitle(refDir(root, name), parseRefSource(name).deck)?.title;
307
+ return title ? `- \`${name}\`: ${title}` : `- \`${name}\``;
308
+ });
309
+ return `
310
+ ## References
311
+
312
+ Other people's decks, pinned in \`dek.toml\` \`[refs]\`, to read as models. They are read-only: read a slide with \`dekc show <ref> <slug>\`, then write your own in this deck's theme.
313
+
314
+ ${lines.join("\n")}
315
+ `;
316
+ }