@anvpx/motion-cli 0.0.0-stage → 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 (197) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +121 -2
  3. package/dist/analyze/command.d.ts +2 -0
  4. package/dist/analyze/command.js +190 -0
  5. package/dist/approval/approval.d.ts +32 -0
  6. package/dist/approval/approval.js +141 -0
  7. package/dist/approval/command.d.ts +2 -0
  8. package/dist/approval/command.js +255 -0
  9. package/dist/approval/gate.d.ts +7 -0
  10. package/dist/approval/gate.js +23 -0
  11. package/dist/approval/hash.d.ts +71 -0
  12. package/dist/approval/hash.js +451 -0
  13. package/dist/args.d.ts +30 -0
  14. package/dist/args.js +53 -0
  15. package/dist/assets/command.d.ts +2 -0
  16. package/dist/assets/command.js +165 -0
  17. package/dist/captions/command.d.ts +2 -0
  18. package/dist/captions/command.js +341 -0
  19. package/dist/captions/cues.d.ts +10 -0
  20. package/dist/captions/cues.js +61 -0
  21. package/dist/catalog/command.d.ts +2 -0
  22. package/dist/catalog/command.js +60 -0
  23. package/dist/catalog/format.d.ts +13 -0
  24. package/dist/catalog/format.js +54 -0
  25. package/dist/cli.d.ts +3 -0
  26. package/dist/cli.js +35 -0
  27. package/dist/commands.d.ts +3 -0
  28. package/dist/commands.js +59 -0
  29. package/dist/compare/command.d.ts +4 -0
  30. package/dist/compare/command.js +144 -0
  31. package/dist/contact/argv.d.ts +24 -0
  32. package/dist/contact/argv.js +42 -0
  33. package/dist/contact/command.d.ts +2 -0
  34. package/dist/contact/command.js +79 -0
  35. package/dist/doctor/checks.d.ts +25 -0
  36. package/dist/doctor/checks.js +571 -0
  37. package/dist/doctor/command.d.ts +4 -0
  38. package/dist/doctor/command.js +88 -0
  39. package/dist/doctor/format.d.ts +7 -0
  40. package/dist/doctor/format.js +41 -0
  41. package/dist/doctor/heal.d.ts +14 -0
  42. package/dist/doctor/heal.js +71 -0
  43. package/dist/doctor/pins.d.ts +8 -0
  44. package/dist/doctor/pins.js +28 -0
  45. package/dist/doctor/skills.d.ts +15 -0
  46. package/dist/doctor/skills.js +58 -0
  47. package/dist/doctor/types.d.ts +36 -0
  48. package/dist/doctor/types.js +1 -0
  49. package/dist/doctor/vendored.d.ts +15 -0
  50. package/dist/doctor/vendored.js +90 -0
  51. package/dist/doctor/version.d.ts +8 -0
  52. package/dist/doctor/version.js +11 -0
  53. package/dist/exit-codes.d.ts +16 -0
  54. package/dist/exit-codes.js +15 -0
  55. package/dist/fit/command.d.ts +5 -0
  56. package/dist/fit/command.js +154 -0
  57. package/dist/fit/solve.d.ts +21 -0
  58. package/dist/fit/solve.js +80 -0
  59. package/dist/fit/target.d.ts +18 -0
  60. package/dist/fit/target.js +47 -0
  61. package/dist/generate3d/command.d.ts +3 -0
  62. package/dist/generate3d/command.js +38 -0
  63. package/dist/generate3d/run.d.ts +39 -0
  64. package/dist/generate3d/run.js +77 -0
  65. package/dist/icon/command.d.ts +2 -0
  66. package/dist/icon/command.js +113 -0
  67. package/dist/index.d.ts +18 -0
  68. package/dist/index.js +16 -0
  69. package/dist/install-mk.d.ts +31 -0
  70. package/dist/install-mk.js +168 -0
  71. package/dist/integration/preview.integration.d.ts +1 -0
  72. package/dist/integration/preview.integration.js +53 -0
  73. package/dist/integration/smoke.integration.d.ts +1 -0
  74. package/dist/integration/smoke.integration.js +91 -0
  75. package/dist/main.d.ts +2 -0
  76. package/dist/main.js +4 -0
  77. package/dist/node-io.d.ts +14 -0
  78. package/dist/node-io.js +152 -0
  79. package/dist/optimize/command.d.ts +12 -0
  80. package/dist/optimize/command.js +110 -0
  81. package/dist/packs/command.d.ts +2 -0
  82. package/dist/packs/command.js +109 -0
  83. package/dist/patch/apply.d.ts +36 -0
  84. package/dist/patch/apply.js +186 -0
  85. package/dist/patch/command.d.ts +2 -0
  86. package/dist/patch/command.js +92 -0
  87. package/dist/plugin/install.d.ts +40 -0
  88. package/dist/plugin/install.js +170 -0
  89. package/dist/preview/command.d.ts +10 -0
  90. package/dist/preview/command.js +758 -0
  91. package/dist/preview/manifest.d.ts +94 -0
  92. package/dist/preview/manifest.js +19 -0
  93. package/dist/preview/pack.d.ts +24 -0
  94. package/dist/preview/pack.js +53 -0
  95. package/dist/preview/plan.d.ts +58 -0
  96. package/dist/preview/plan.js +160 -0
  97. package/dist/preview/provenance.d.ts +23 -0
  98. package/dist/preview/provenance.js +38 -0
  99. package/dist/publish/command.d.ts +2 -0
  100. package/dist/publish/command.js +70 -0
  101. package/dist/publish/player.d.ts +25 -0
  102. package/dist/publish/player.js +108 -0
  103. package/dist/publish/static.d.ts +16 -0
  104. package/dist/publish/static.js +78 -0
  105. package/dist/qc.d.ts +2 -0
  106. package/dist/qc.js +192 -0
  107. package/dist/scene/author.d.ts +16 -0
  108. package/dist/scene/author.js +260 -0
  109. package/dist/scene/command.d.ts +2 -0
  110. package/dist/scene/command.js +110 -0
  111. package/dist/scene/ops.d.ts +23 -0
  112. package/dist/scene/ops.js +151 -0
  113. package/dist/state/command.d.ts +2 -0
  114. package/dist/state/command.js +128 -0
  115. package/dist/state/evidence.d.ts +71 -0
  116. package/dist/state/evidence.js +243 -0
  117. package/dist/storyboard/check.d.ts +21 -0
  118. package/dist/storyboard/check.js +39 -0
  119. package/dist/storyboard/containment.d.ts +24 -0
  120. package/dist/storyboard/containment.js +43 -0
  121. package/dist/storyboard/custom.d.ts +15 -0
  122. package/dist/storyboard/custom.js +133 -0
  123. package/dist/storyboard/edit.d.ts +19 -0
  124. package/dist/storyboard/edit.js +39 -0
  125. package/dist/storyboard/load.d.ts +11 -0
  126. package/dist/storyboard/load.js +52 -0
  127. package/dist/storyboard/report.d.ts +5 -0
  128. package/dist/storyboard/report.js +8 -0
  129. package/dist/template/coerce.d.ts +6 -0
  130. package/dist/template/coerce.js +44 -0
  131. package/dist/template/command.d.ts +5 -0
  132. package/dist/template/command.js +163 -0
  133. package/dist/template/env.d.ts +34 -0
  134. package/dist/template/env.js +38 -0
  135. package/dist/template/fork.d.ts +8 -0
  136. package/dist/template/fork.js +156 -0
  137. package/dist/template/format.d.ts +21 -0
  138. package/dist/template/format.js +161 -0
  139. package/dist/template/list.d.ts +15 -0
  140. package/dist/template/list.js +116 -0
  141. package/dist/template/new.d.ts +6 -0
  142. package/dist/template/new.js +302 -0
  143. package/dist/testing/env.d.ts +8 -0
  144. package/dist/testing/env.js +19 -0
  145. package/dist/testing/fakes.d.ts +33 -0
  146. package/dist/testing/fakes.js +95 -0
  147. package/dist/testing/fixtures.d.ts +8 -0
  148. package/dist/testing/fixtures.js +6 -0
  149. package/dist/testing/probe.d.ts +15 -0
  150. package/dist/testing/probe.js +20 -0
  151. package/dist/testing/project.d.ts +22 -0
  152. package/dist/testing/project.js +26 -0
  153. package/dist/tooling/session.d.ts +40 -0
  154. package/dist/tooling/session.js +106 -0
  155. package/dist/types.d.ts +59 -0
  156. package/dist/types.js +1 -0
  157. package/dist/verify/audio-loudness.d.ts +46 -0
  158. package/dist/verify/audio-loudness.js +186 -0
  159. package/dist/verify/command.d.ts +2 -0
  160. package/dist/verify/command.js +161 -0
  161. package/dist/verify/determinism.d.ts +30 -0
  162. package/dist/verify/determinism.js +59 -0
  163. package/dist/verify/expectation.d.ts +15 -0
  164. package/dist/verify/expectation.js +19 -0
  165. package/dist/verify/media-events.d.ts +61 -0
  166. package/dist/verify/media-events.js +174 -0
  167. package/dist/verify/platform.d.ts +33 -0
  168. package/dist/verify/platform.js +59 -0
  169. package/dist/verify/probe.d.ts +14 -0
  170. package/dist/verify/probe.js +23 -0
  171. package/dist/verify/report.d.ts +15 -0
  172. package/dist/verify/report.js +45 -0
  173. package/dist/verify/run.d.ts +27 -0
  174. package/dist/verify/run.js +53 -0
  175. package/dist/voice/command.d.ts +3 -0
  176. package/dist/voice/command.js +49 -0
  177. package/dist/voice/manifest.d.ts +33 -0
  178. package/dist/voice/manifest.js +69 -0
  179. package/dist/voice/run.d.ts +21 -0
  180. package/dist/voice/run.js +181 -0
  181. package/dist/workflow/build.d.ts +2 -0
  182. package/dist/workflow/build.js +25 -0
  183. package/dist/workflow/draft.d.ts +9 -0
  184. package/dist/workflow/draft.js +201 -0
  185. package/dist/workflow/finish.d.ts +23 -0
  186. package/dist/workflow/finish.js +28 -0
  187. package/dist/workflow/project.d.ts +73 -0
  188. package/dist/workflow/project.js +198 -0
  189. package/dist/workflow/provenance.d.ts +18 -0
  190. package/dist/workflow/provenance.js +36 -0
  191. package/dist/workflow/render.d.ts +31 -0
  192. package/dist/workflow/render.js +705 -0
  193. package/dist/workflow/still.d.ts +5 -0
  194. package/dist/workflow/still.js +62 -0
  195. package/dist/workflow/validate.d.ts +2 -0
  196. package/dist/workflow/validate.js +49 -0
  197. package/package.json +38 -3
@@ -0,0 +1,255 @@
1
+ import { createHash } from "node:crypto";
2
+ import { resolve } from "node:path";
3
+ import { parseFlags, requireStoryboard, textFlag } from "../args.js";
4
+ import { EXIT } from "../exit-codes.js";
5
+ import { defaultOut, previewManifestPath } from "../preview/manifest.js";
6
+ import { writeApproval } from "./approval.js";
7
+ import { buildApprovalInputManifest, hashApprovalInput, } from "./hash.js";
8
+ const USAGE = `mk approve <storyboard.json> [--note "<user words>"] [--out <dir>] [--assets <dir>] [--ledger file] [--audio manifest] [--music <bed.wav>] [--music-volume <n>] [--music-license <file>]
9
+
10
+ Record the user's approval of the CURRENT inputs (visual, audiovisual or audiovisual+music) in <storyboard dir>/.mk/<name>/approval.json.
11
+ Run it only after the user has seen the preview pack and said an explicit yes. Opt-in music is covered by the same gate (no parallel gate).
12
+ Refuses (exit 2) when <out>/<name>.preview/preview.json does not exist for the current inputs: run \`mk preview\` first.
13
+ Every preview input (assets, ledger, audio, music, volume, license) is inferred from the pack; an explicit flag
14
+ must match the pack or the command exits 2 naming the difference.
15
+ Any later edit to the storyboard, its themes, its referenced assets or its ledger, or (for audiovisual) its audio manifest, wavs, mix, bed, volume or license receipt re-arms \`mk render --gated\` (exit 4).
16
+ --note the user's words, stored with the approval
17
+ --out d the --out used for \`mk preview\` (default <storyboard dir>/out)
18
+ --assets d the --assets used for \`mk preview\` (default: none)
19
+ --ledger f the --ledger used for preview (default: none)
20
+ --audio manifest the --audio used for preview (default: none; mismatch exits 2)
21
+ --music <bed.wav> the --music bed used for preview (needs --audio; mismatch exits 2)
22
+ --music-volume <n> the --music-volume used for preview (within (0, 1])
23
+ --music-license <file> the --music-license used for preview (hash only)
24
+ `;
25
+ const previewed = (text) => {
26
+ try {
27
+ const parsed = JSON.parse(text);
28
+ if (parsed.inputManifest !== undefined && parsed.inputManifestSha256 !== undefined) {
29
+ return {
30
+ inputManifest: parsed.inputManifest,
31
+ inputManifestSha256: parsed.inputManifestSha256,
32
+ audioMix: parsed.audioMix,
33
+ clip: parsed.clip,
34
+ inputs: parsed.inputs,
35
+ };
36
+ }
37
+ return undefined;
38
+ }
39
+ catch {
40
+ return undefined;
41
+ }
42
+ };
43
+ const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
44
+ /** The pack's recorded invocation inputs; absent only when the pack predates A4. */
45
+ const recordedInputs = (value) => {
46
+ if (!isRecord(value))
47
+ return undefined;
48
+ const path = (key) => typeof value[key] === "string" ? value[key] : null;
49
+ return {
50
+ assetsDir: path("assetsDir"),
51
+ ledger: path("ledger"),
52
+ audioManifest: path("audioManifest"),
53
+ music: path("music"),
54
+ musicVolume: typeof value.musicVolume === "number" ? value.musicVolume : null,
55
+ musicLicense: path("musicLicense"),
56
+ };
57
+ };
58
+ /**
59
+ * A4: an explicit flag must agree with what the pack recorded; when omitted, the
60
+ * recorded input is reused. Returns the value to resolve with (or an exit code).
61
+ */
62
+ const pickInput = (name, flag, recorded, io) => {
63
+ if (flag === undefined)
64
+ return recorded ?? undefined;
65
+ if (recorded !== null && resolve(flag) !== recorded) {
66
+ io.stderr(`mk approve: --${name} differs from the preview pack (${recorded}); drop the flag to use the previewed input, or run \`mk preview\` again with this value\n`);
67
+ return EXIT.USAGE;
68
+ }
69
+ return flag;
70
+ };
71
+ const pickVolume = (flag, recorded, io) => {
72
+ if (flag === undefined)
73
+ return { ok: true, value: recorded ?? undefined };
74
+ if (recorded !== null && flag !== recorded) {
75
+ io.stderr(`mk approve: --music-volume differs from the preview pack (${recorded}); drop the flag to use the previewed level, or run \`mk preview\` again\n`);
76
+ return { ok: false, code: EXIT.USAGE };
77
+ }
78
+ return { ok: true, value: flag };
79
+ };
80
+ const resolveInputs = async (io, storyboard, assetsDir, ledgerPath, audioManifest, musicPath, musicVolume, musicLicensePath) => {
81
+ try {
82
+ const manifest = await buildApprovalInputManifest(io.host, storyboard, {
83
+ ...(assetsDir === undefined ? {} : { assetsDir: resolve(assetsDir) }),
84
+ ...(ledgerPath === undefined ? {} : { ledgerPath }),
85
+ ...(audioManifest === undefined ? {} : { audioManifest }),
86
+ ...(musicPath === undefined ? {} : { music: musicPath }),
87
+ ...(musicVolume === undefined ? {} : { musicVolume }),
88
+ ...(musicLicensePath === undefined ? {} : { musicLicense: musicLicensePath }),
89
+ });
90
+ return { manifest, hash: hashApprovalInput(manifest) };
91
+ }
92
+ catch (error) {
93
+ io.stderr(`mk approve: cannot resolve approval inputs: ${error.message}\n`);
94
+ return EXIT.USAGE;
95
+ }
96
+ };
97
+ const stalePreview = (io, storyboard, reason) => {
98
+ io.stderr(`mk approve: ${reason}; run \`mk preview ${storyboard}\` again and show it to the user\n`);
99
+ return EXIT.USAGE;
100
+ };
101
+ /** Verifies a preview binding; returns its sha or an exit code. */
102
+ const verifyBinding = (io, storyboard, inputManifest, inputManifestSha256) => {
103
+ if (typeof inputManifestSha256 !== "string" || !/^[a-f0-9]{64}$/.test(inputManifestSha256)) {
104
+ return stalePreview(io, storyboard, "the preview binding is malformed");
105
+ }
106
+ try {
107
+ const recomputed = hashApprovalInput(inputManifest);
108
+ if (recomputed !== inputManifestSha256) {
109
+ return stalePreview(io, storyboard, "the preview binding is tampered (sha mismatch)");
110
+ }
111
+ }
112
+ catch {
113
+ return stalePreview(io, storyboard, "the preview binding is malformed");
114
+ }
115
+ return inputManifestSha256;
116
+ };
117
+ const approvePreview = async (io, resolved) => {
118
+ const preview = previewed(resolved.manifestText);
119
+ if (preview === undefined) {
120
+ return stalePreview(io, resolved.storyboard, "the preview is missing the visual input binding");
121
+ }
122
+ // RA-02: an audiovisual approval is only valid after the user heard a
123
+ // playable mixed clip. Silent or skipped clips never authorize audio;
124
+ // visual-only approvals are unaffected. AU-01: a music receipt is only valid
125
+ // after an audible clip mixed with the same bed (musicSha/volume/license).
126
+ if (resolved.inputs.manifest.scope === "audiovisual") {
127
+ const mix = preview.audioMix;
128
+ const clip = preview.clip;
129
+ const previewScope = preview.inputManifest?.scope;
130
+ const heard = mix?.audible === true && clip?.status === "ok" && previewScope === "audiovisual";
131
+ if (!heard) {
132
+ const detail = previewScope !== "audiovisual"
133
+ ? "the preview is silent (scope visual)"
134
+ : clip?.status !== "ok"
135
+ ? `the preview clip was ${typeof clip?.status === "string" ? clip.status : "missing"}${typeof clip?.reason === "string" ? ` (${clip.reason})` : ""}`
136
+ : "the preview is not audible (audible:false, unheard mix)";
137
+ io.stderr(`mk approve: refusing audiovisual approval: ${detail}; regenerate an audible preview with \`mk preview ${resolved.storyboard} --audio <manifest>\` and show clip.mp4 to the user before approving\n`);
138
+ return EXIT.USAGE;
139
+ }
140
+ const wantMusic = resolved.inputs.manifest.music;
141
+ if (wantMusic !== undefined) {
142
+ const heardMusic = mix?.musicSha === wantMusic.sha256 &&
143
+ mix?.musicVolume === wantMusic.volume &&
144
+ mix?.musicLicenseSha === wantMusic.licenseEvidenceSha256;
145
+ if (!heardMusic) {
146
+ io.stderr(`mk approve: refusing music approval: the preview clip was not mixed with this bed/volume/license; regenerate an audible preview with \`mk preview ${resolved.storyboard} --audio <manifest> --music <bed>\` and show clip.mp4 to the user before approving\n`);
147
+ return EXIT.USAGE;
148
+ }
149
+ }
150
+ else {
151
+ const previewMusic = preview.inputManifest?.music;
152
+ if (previewMusic !== undefined) {
153
+ return stalePreview(io, resolved.storyboard, "the preview is for an older version of the inputs (music mismatch)");
154
+ }
155
+ }
156
+ }
157
+ const verified = verifyBinding(io, resolved.storyboard, preview.inputManifest, preview.inputManifestSha256);
158
+ if (typeof verified === "number")
159
+ return verified;
160
+ if (resolved.inputs.hash !== verified) {
161
+ return stalePreview(io, resolved.storyboard, "the preview is for an older version of the inputs (edited since, or --audio mismatch)");
162
+ }
163
+ await writeApproval(io.host, resolved.storyboard, {
164
+ inputManifest: resolved.inputs.manifest,
165
+ inputManifestSha256: resolved.inputs.hash,
166
+ previewManifestSha: resolved.previewManifestSha,
167
+ approvedAt: new Date().toISOString(),
168
+ note: resolved.note,
169
+ });
170
+ io.stdout(`approved ${resolved.inputs.hash.slice(0, 12)} | mk render ${resolved.storyboard} --gated\n`);
171
+ return EXIT.OK;
172
+ };
173
+ export const approveCommand = {
174
+ name: "approve",
175
+ summary: "record the user's approval of the previewed storyboard",
176
+ async run(args, io) {
177
+ const flags = {
178
+ note: { type: "string" },
179
+ out: { type: "string" },
180
+ assets: { type: "string" },
181
+ ledger: { type: "string" },
182
+ audio: { type: "string" },
183
+ music: { type: "string" },
184
+ "music-volume": { type: "string" },
185
+ "music-license": { type: "string" },
186
+ };
187
+ const parsed = parseFlags("approve", USAGE, args, flags, io);
188
+ if (parsed.kind === "exit")
189
+ return parsed.code;
190
+ const storyboard = requireStoryboard("approve", USAGE, parsed.positionals, io);
191
+ if (typeof storyboard !== "string")
192
+ return storyboard.code;
193
+ const musicRaw = textFlag(parsed.flags, "music-volume");
194
+ let musicVolume;
195
+ if (musicRaw !== undefined) {
196
+ musicVolume = Number(musicRaw);
197
+ if (!Number.isFinite(musicVolume) || musicVolume <= 0 || musicVolume > 1) {
198
+ io.stderr("mk approve: --music-volume must be within (0, 1]\n");
199
+ return EXIT.USAGE;
200
+ }
201
+ }
202
+ const out = resolve(textFlag(parsed.flags, "out") ?? defaultOut(storyboard));
203
+ const manifestFile = previewManifestPath(out, storyboard);
204
+ let manifestText;
205
+ try {
206
+ manifestText = await io.host.readText(manifestFile);
207
+ }
208
+ catch {
209
+ io.stderr(`mk approve: no preview for this storyboard (${manifestFile} is missing); run \`mk preview ${storyboard}\` and show it to the user first\n`);
210
+ return EXIT.USAGE;
211
+ }
212
+ const previewManifestSha = createHash("sha256").update(manifestText).digest("hex");
213
+ // A4: the pack records the inputs it was made with; omitted flags reuse them.
214
+ const recorded = recordedInputs(previewed(manifestText)?.inputs);
215
+ const assetsDir = pickInput("assets", textFlag(parsed.flags, "assets"), recorded?.assetsDir ?? null, io);
216
+ if (typeof assetsDir === "number")
217
+ return assetsDir;
218
+ const ledgerPath = pickInput("ledger", textFlag(parsed.flags, "ledger"), recorded?.ledger ?? null, io);
219
+ if (typeof ledgerPath === "number")
220
+ return ledgerPath;
221
+ const audioFlag = pickInput("audio", textFlag(parsed.flags, "audio"), recorded?.audioManifest ?? null, io);
222
+ if (typeof audioFlag === "number")
223
+ return audioFlag;
224
+ const musicPath = pickInput("music", textFlag(parsed.flags, "music"), recorded?.music ?? null, io);
225
+ if (typeof musicPath === "number")
226
+ return musicPath;
227
+ const mergedVolume = pickVolume(musicVolume, recorded?.musicVolume ?? null, io);
228
+ if (!mergedVolume.ok)
229
+ return mergedVolume.code;
230
+ musicVolume = mergedVolume.value;
231
+ const musicLicensePath = pickInput("music-license", textFlag(parsed.flags, "music-license"), recorded?.musicLicense ?? null, io);
232
+ if (typeof musicLicensePath === "number")
233
+ return musicLicensePath;
234
+ if (musicPath !== undefined && audioFlag === undefined) {
235
+ io.stderr("mk approve: --music needs --audio\n");
236
+ return EXIT.USAGE;
237
+ }
238
+ if ((musicVolume !== undefined || musicLicensePath !== undefined) && musicPath === undefined) {
239
+ io.stderr("mk approve: --music-volume/--music-license need --music\n");
240
+ return EXIT.USAGE;
241
+ }
242
+ const inputs = await resolveInputs(io, storyboard, assetsDir, ledgerPath, audioFlag, musicPath, musicVolume, musicLicensePath);
243
+ if (typeof inputs === "number")
244
+ return inputs;
245
+ const note = textFlag(parsed.flags, "note") ?? "";
246
+ const resolved = {
247
+ storyboard,
248
+ inputs,
249
+ manifestText,
250
+ previewManifestSha,
251
+ note,
252
+ };
253
+ return approvePreview(io, resolved);
254
+ },
255
+ };
@@ -0,0 +1,7 @@
1
+ import type { Io } from "../types.ts";
2
+ /**
3
+ * `--gated`: undefined when the current storyboard, themes and authored scenes are approved, else EXIT.AWAITING_APPROVAL
4
+ * with a message that tells the agent to ask the user (not an error). An unreadable storyboard is
5
+ * left to the normal build path to report.
6
+ */
7
+ export declare function approvalGate(io: Io, storyboard: string): Promise<number | undefined>;
@@ -0,0 +1,23 @@
1
+ import { EXIT } from "../exit-codes.js";
2
+ import { approvalState } from "./approval.js";
3
+ /**
4
+ * `--gated`: undefined when the current storyboard, themes and authored scenes are approved, else EXIT.AWAITING_APPROVAL
5
+ * with a message that tells the agent to ask the user (not an error). An unreadable storyboard is
6
+ * left to the normal build path to report.
7
+ */
8
+ export async function approvalGate(io, storyboard) {
9
+ let state;
10
+ try {
11
+ state = await approvalState(io.host, storyboard);
12
+ }
13
+ catch {
14
+ return undefined;
15
+ }
16
+ if (state.state === "approved")
17
+ return undefined;
18
+ const why = state.state === "stale"
19
+ ? "the storyboard, its themes or its authored scene files changed since it was approved"
20
+ : "no approval recorded";
21
+ io.stderr(`mk: AWAITING_APPROVAL (${state.hash.slice(0, 12)}): ${why}. Run \`mk preview ${storyboard}\`, show the user the pack, and only after an explicit yes run \`mk approve ${storyboard}\`.\n`);
22
+ return EXIT.AWAITING_APPROVAL;
23
+ }
@@ -0,0 +1,71 @@
1
+ import type { Host } from "../types.ts";
2
+ export declare const MK_DIR = ".mk";
3
+ /** File name without its extension: the namespace of everything a storyboard owns next to itself. */
4
+ export declare const storyboardStem: (storyboard: string) => string;
5
+ /** `<storyboard dir>/.mk/<stem>`: provenance and approval, so two storyboards in one folder never share state. */
6
+ export declare const mkDir: (storyboard: string) => string;
7
+ /**
8
+ * Reads a sidecar file of `storyboard`. Projects made before the per-storyboard namespace kept it
9
+ * in `<dir>/.mk/`; that location is still read, and only for a storyboard named `storyboard.json`.
10
+ */
11
+ export declare function readSidecar(host: Host, storyboard: string, file: string): Promise<string>;
12
+ /**
13
+ * The approval hash: sha256 over the storyboard bytes, every vendored theme (name + bytes,
14
+ * sorted by name) and — when the storyboard has authored scenes — every file of those scene
15
+ * directories. Any edit to any of them changes the hash, which re-arms the gate. Authored
16
+ * scenes are media-producing input: leaving them out would let `scene.js` change under an
17
+ * approval and still render.
18
+ */
19
+ export declare function combineHash(storyboardText: string, themes: readonly (readonly [name: string, text: string])[], scenes?: readonly (readonly [name: string, text: string])[]): string;
20
+ /** Reads the storyboard, `<dir>/themes/*.json` and any authored scene files; rejects when the storyboard is unreadable. */
21
+ export declare function storyboardHash(host: Host, storyboard: string): Promise<string>;
22
+ export interface ManifestRef {
23
+ readonly path: string;
24
+ readonly sha256: string;
25
+ }
26
+ export interface MixClipRef {
27
+ readonly id: string;
28
+ readonly start: number;
29
+ readonly wavSha: string;
30
+ }
31
+ export interface MixInfo {
32
+ readonly mode: "single" | "concat" | "adelay";
33
+ readonly clips: readonly MixClipRef[];
34
+ }
35
+ export interface MusicInfo {
36
+ readonly path: string;
37
+ readonly sha256: string;
38
+ readonly volume: number;
39
+ readonly licenseEvidenceSha256: string | null;
40
+ }
41
+ export interface ApprovalInputManifest {
42
+ readonly scope: "visual" | "audiovisual";
43
+ readonly storyboard: ManifestRef;
44
+ readonly themes: readonly ManifestRef[];
45
+ readonly assets: readonly ManifestRef[];
46
+ readonly ledgerSha256?: string;
47
+ readonly audio: readonly ManifestRef[];
48
+ /** Files of authored (`type: "custom"`) scenes; omitted when the storyboard has none. */
49
+ readonly scenes?: readonly ManifestRef[];
50
+ readonly audioManifest?: ManifestRef;
51
+ readonly mix?: MixInfo;
52
+ readonly music?: MusicInfo;
53
+ }
54
+ export interface ApprovalBuildOptions {
55
+ readonly assetsDir?: string;
56
+ readonly ledgerPath?: string;
57
+ readonly audioManifest?: string;
58
+ readonly music?: string;
59
+ readonly musicVolume?: number;
60
+ readonly musicLicense?: string;
61
+ }
62
+ export declare function buildApprovalInputManifest(host: Host, storyboard: string, opts?: ApprovalBuildOptions): Promise<ApprovalInputManifest>;
63
+ /**
64
+ * Canonical hash of the manifest. Sorts themes/assets/audio by UTF-16
65
+ * code units so enumeration order never changes the digest. No timestamp
66
+ * or machine path enters the hash. Audiovisual fields (audioManifest + mix)
67
+ * are covered so wav/manifest/mix changes re-arm the gate. AU-01 music
68
+ * {path,sha256,volume,licenseEvidenceSha256} is covered so bed/volume/license
69
+ * changes re-arm the gate.
70
+ */
71
+ export declare function hashApprovalInput(manifest: ApprovalInputManifest): string;