@litfamily/litopencode 1.0.30 → 1.0.32

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 (270) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/README-Ko-KR.md +6 -6
  3. package/README.md +6 -6
  4. package/dist/activation-probe.js +1 -0
  5. package/dist/activation-prompt-utils.d.ts +1 -1
  6. package/dist/activation-reentry.d.ts +20 -0
  7. package/dist/activation-reentry.js +110 -1
  8. package/dist/activation-routing.d.ts +2 -1
  9. package/dist/activation-routing.js +100 -11
  10. package/dist/activation-workflow-prompts.d.ts +4 -0
  11. package/dist/activation-workflow-prompts.js +30 -0
  12. package/dist/activation.d.ts +1 -1
  13. package/dist/activation.js +44 -5
  14. package/dist/adhd-next.d.ts +51 -0
  15. package/dist/adhd-next.js +150 -0
  16. package/dist/cli/managed-skill-assets.d.ts +1 -1
  17. package/dist/cli/managed-skill-assets.js +7 -1
  18. package/dist/cli/native-skill-integrity.js +1 -1
  19. package/dist/cli.js +7 -1
  20. package/dist/commands.d.ts +28 -1
  21. package/dist/commands.js +48 -1
  22. package/dist/features.d.ts +80 -1
  23. package/dist/features.js +105 -1
  24. package/dist/ledger.js +12 -1
  25. package/dist/secret-shapes.js +5 -5
  26. package/dist/skills.d.ts +22 -1
  27. package/dist/skills.js +36 -0
  28. package/dist/start-work-typed.d.ts +34 -0
  29. package/dist/start-work-typed.js +156 -0
  30. package/dist/state.d.ts +2 -0
  31. package/dist/state.js +2 -0
  32. package/dist/tools.js +22 -2
  33. package/dist/tui/mission-band.d.ts +20 -1
  34. package/dist/tui/mission-band.js +73 -16
  35. package/dist/tui/mission-looks.d.ts +38 -0
  36. package/dist/tui/mission-looks.js +73 -0
  37. package/dist/tui/mission-model.d.ts +2 -0
  38. package/dist/tui/mission-model.js +3 -1
  39. package/dist/tui/mission-rainbow.d.ts +33 -0
  40. package/dist/tui/mission-rainbow.js +90 -0
  41. package/dist/tui/mission-source.d.ts +48 -0
  42. package/dist/tui/mission-source.js +184 -2
  43. package/dist/tui/mission-text.d.ts +2 -0
  44. package/dist/tui/mission-text.js +9 -1
  45. package/dist/tui/mission-threads.d.ts +50 -0
  46. package/dist/tui/mission-threads.js +119 -0
  47. package/dist/tui.js +84 -6
  48. package/dist/uiux-visual-catalog.js +8 -1
  49. package/dist/v2/session-hooks.js +38 -5
  50. package/dist/workflow-stack.d.ts +53 -0
  51. package/dist/workflow-stack.js +196 -0
  52. package/docs/assets/readme/badge-version.svg +1 -1
  53. package/docs/assets/skills/lit-schematic.webp +0 -0
  54. package/docs/reference-Ko-KR.md +30 -2
  55. package/docs/reference.md +34 -2
  56. package/package.json +1 -1
  57. package/skills/frontend-ui-ux/SKILL.md +3 -2
  58. package/skills/frontend-ui-ux/kit/components.css +414 -0
  59. package/skills/frontend-ui-ux/kit/fonts.json +22 -0
  60. package/skills/frontend-ui-ux/kit/layouts/landing.html +164 -0
  61. package/skills/frontend-ui-ux/kit/layouts/settings.html +114 -0
  62. package/skills/frontend-ui-ux/kit/layouts/workspace.html +115 -0
  63. package/skills/frontend-ui-ux/kit/styles/computational-brutalism.json +227 -0
  64. package/skills/frontend-ui-ux/kit/styles/field-notes.json +205 -0
  65. package/skills/frontend-ui-ux/kit/styles/graphite.json +200 -0
  66. package/skills/frontend-ui-ux/kit/styles/meadow.json +199 -0
  67. package/skills/frontend-ui-ux/kit/styles/neo-brutalism.json +222 -0
  68. package/skills/frontend-ui-ux/kit/ui.js +226 -0
  69. package/skills/frontend-ui-ux/references/brand-and-imagery.md +2 -2
  70. package/skills/frontend-ui-ux/references/complete-contract.md +2 -1
  71. package/skills/frontend-ui-ux/references/craft-floor.md +6 -2
  72. package/skills/frontend-ui-ux/references/default-editorial-pixel.json +3 -0
  73. package/skills/frontend-ui-ux/references/house-styles.md +236 -0
  74. package/skills/frontend-ui-ux/references/motion-guide.md +2 -2
  75. package/skills/frontend-ui-ux/references/production.md +3 -1
  76. package/skills/frontend-ui-ux/references/slop-register.md +4 -4
  77. package/skills/frontend-ui-ux/scripts/design-lock.mjs +400 -0
  78. package/skills/frontend-ui-ux/scripts/pixel-art.mjs +60 -0
  79. package/skills/frontend-ui-ux/scripts/probe-lock.mjs +235 -0
  80. package/skills/frontend-ui-ux/scripts/probe-page.mjs +34 -12
  81. package/skills/frontend-ui-ux/scripts/probe-static.mjs +16 -3
  82. package/skills/frontend-ui-ux/scripts/probe-thresholds.json +8 -2
  83. package/skills/frontend-ui-ux/scripts/probe.mjs +35 -7
  84. package/skills/lit-adhd/SKILL.md +208 -0
  85. package/skills/lit-adhd/references/brief-format.md +43 -0
  86. package/skills/lit-adhd/references/card-examples.md +82 -0
  87. package/skills/lit-adhd/references/status-taxonomy.md +29 -0
  88. package/skills/lit-adhd/scripts/rewind.mjs +835 -0
  89. package/skills/lit-diagram-drawer/SKILL.md +4 -3
  90. package/skills/lit-diagram-drawer/references/style-guide.md +20 -11
  91. package/skills/lit-diagram-drawer/references/type-architecture.md +3 -3
  92. package/skills/lit-diagram-drawer/references/type-dependency.md +1 -1
  93. package/skills/lit-diagram-drawer/references/type-tree.md +3 -3
  94. package/skills/lit-diagram-drawer/references/type-wardley.md +1 -1
  95. package/skills/lit-diagram-drawer/references/verifier-guide.md +1 -0
  96. package/skills/lit-diagram-drawer/scripts/craft-thresholds.json +1 -2
  97. package/skills/lit-diagram-drawer/scripts/visual-quality.mjs +1 -2
  98. package/skills/lit-docx/SKILL.md +2 -1
  99. package/skills/lit-handoff/SKILL.md +2 -0
  100. package/skills/lit-hijack/SKILL.md +422 -0
  101. package/skills/lit-hijack/references/capture-and-provenance.md +87 -0
  102. package/skills/lit-hijack/references/clean-room-build.md +111 -0
  103. package/skills/lit-hijack/references/distil-and-licence.md +100 -0
  104. package/skills/lit-hijack/references/gap-and-port-design.md +126 -0
  105. package/skills/lit-hijack/references/parity-and-report.md +110 -0
  106. package/skills/lit-hijack/scripts/hijack.mjs +993 -0
  107. package/skills/lit-plan/SKILL.md +2 -0
  108. package/skills/lit-pptx/SKILL.md +27 -11
  109. package/skills/lit-pptx/fonts/jetbrains-mono/JetBrainsMono-Bold.ttf +0 -0
  110. package/skills/lit-pptx/fonts/jetbrains-mono/JetBrainsMono-Regular.ttf +0 -0
  111. package/skills/lit-pptx/fonts/jetbrains-mono/OFL.txt +93 -0
  112. package/skills/lit-pptx/fonts/jetbrains-mono/SOURCE.md +17 -0
  113. package/skills/lit-pptx/references/density-and-fill.md +9 -3
  114. package/skills/lit-pptx/references/direction-step.md +21 -6
  115. package/skills/lit-pptx/references/examples/04-lecture-chalk-ko.md +3 -6
  116. package/skills/lit-pptx/references/examples/05-research-talk-paper-en.md +1 -5
  117. package/skills/lit-pptx/references/examples/08-reliability-keynote-night-en.md +0 -4
  118. package/skills/lit-pptx/references/examples/09-status-update-ledger-en.md +2 -2
  119. package/skills/lit-pptx/references/examples/11-forecast-review-night-ko.md +4 -7
  120. package/skills/lit-pptx/references/examples/13-engineering-review-computational-brutalism-ko.md +173 -0
  121. package/skills/lit-pptx/references/examples/14-launch-pitch-neo-brutalism-ko.md +154 -0
  122. package/skills/lit-pptx/references/examples/15-design-lecture-bauhaus-ko.md +139 -0
  123. package/skills/lit-pptx/references/examples/16-community-programme-riso-ko.md +158 -0
  124. package/skills/lit-pptx/references/examples/17-public-data-briefing-newsroom-ko.md +179 -0
  125. package/skills/lit-pptx/references/layout-families.md +65 -47
  126. package/skills/lit-pptx/references/title-treatments.md +83 -134
  127. package/skills/lit-pptx/references/tonalities/atlas.md +3 -3
  128. package/skills/lit-pptx/references/tonalities/bauhaus.md +140 -0
  129. package/skills/lit-pptx/references/tonalities/chalk.md +7 -6
  130. package/skills/lit-pptx/references/tonalities/computational-brutalism.md +150 -0
  131. package/skills/lit-pptx/references/tonalities/gazette.md +1 -1
  132. package/skills/lit-pptx/references/tonalities/ledger.md +1 -1
  133. package/skills/lit-pptx/references/tonalities/neo-brutalism.md +161 -0
  134. package/skills/lit-pptx/references/tonalities/newsroom.md +144 -0
  135. package/skills/lit-pptx/references/tonalities/night.md +1 -1
  136. package/skills/lit-pptx/references/tonalities/paper.md +2 -2
  137. package/skills/lit-pptx/references/tonalities/riso.md +137 -0
  138. package/skills/lit-pptx/references/tonalities/signal.md +3 -3
  139. package/skills/lit-pptx/references/tonalities/studio.md +7 -6
  140. package/skills/lit-pptx/run.mjs +2 -1
  141. package/skills/lit-pptx/scripts/compile-deck.js +1 -1
  142. package/skills/lit-pptx/scripts/craft-thresholds.json +4 -8
  143. package/skills/lit-pptx/scripts/craft_extras.py +56 -3
  144. package/skills/lit-pptx/scripts/deck_output.py +75 -90
  145. package/skills/lit-pptx/scripts/embed_fonts.py +13 -1
  146. package/skills/lit-pptx/scripts/layout_inventory.py +12 -5
  147. package/skills/lit-pptx/scripts/lib/grid-resolver.js +101 -110
  148. package/skills/lit-pptx/scripts/lib/layout-resolver.js +9 -15
  149. package/skills/lit-pptx/scripts/lib/render-pack.js +373 -566
  150. package/skills/lit-pptx/scripts/lib/slide-grid.js +104 -0
  151. package/skills/lit-pptx/scripts/lib/template-registry.js +57 -6
  152. package/skills/lit-pptx/scripts/qa_deck.py +10 -11
  153. package/skills/lit-pptx/scripts/render_pages.py +118 -0
  154. package/skills/lit-pptx/templates/tonalities/atlas/pack.yaml +1 -1
  155. package/skills/lit-pptx/templates/tonalities/bauhaus/pack.yaml +98 -0
  156. package/skills/lit-pptx/templates/tonalities/chalk/pack.yaml +1 -1
  157. package/skills/lit-pptx/templates/tonalities/computational-brutalism/pack.yaml +99 -0
  158. package/skills/lit-pptx/templates/tonalities/gazette/pack.yaml +1 -1
  159. package/skills/lit-pptx/templates/tonalities/ledger/pack.yaml +1 -1
  160. package/skills/lit-pptx/templates/tonalities/neo-brutalism/pack.yaml +107 -0
  161. package/skills/lit-pptx/templates/tonalities/newsroom/pack.yaml +99 -0
  162. package/skills/lit-pptx/templates/tonalities/night/pack.yaml +1 -1
  163. package/skills/lit-pptx/templates/tonalities/paper/pack.yaml +1 -1
  164. package/skills/lit-pptx/templates/tonalities/riso/pack.yaml +99 -0
  165. package/skills/lit-pptx/templates/tonalities/signal/pack.yaml +2 -2
  166. package/skills/lit-pptx/templates/tonalities/studio/pack.yaml +1 -1
  167. package/skills/lit-recap/SKILL.md +4 -0
  168. package/skills/lit-schematic/SKILL.md +118 -0
  169. package/skills/lit-schematic/examples/iso-heat-exchanger/build.mjs +38 -0
  170. package/skills/lit-schematic/examples/iso-heat-exchanger/scene.mjs +133 -0
  171. package/skills/lit-schematic/examples/journal-adsorption-rig/build.mjs +87 -0
  172. package/skills/lit-schematic/examples/pid-dosing-skid/build.mjs +85 -0
  173. package/skills/lit-schematic/references/cutaway-outputs.md +70 -0
  174. package/skills/lit-schematic/references/isometric-craft.md +101 -0
  175. package/skills/lit-schematic/references/journal-figures.md +72 -0
  176. package/skills/lit-schematic/references/kit-api.md +134 -0
  177. package/skills/lit-schematic/references/pfd-and-layout.md +53 -0
  178. package/skills/lit-schematic/references/pid-drafting.md +103 -0
  179. package/skills/lit-schematic/references/pid-symbols.md +163 -0
  180. package/skills/lit-schematic/references/verification.md +100 -0
  181. package/skills/lit-schematic/scripts/doctor.mjs +57 -0
  182. package/skills/lit-schematic/scripts/lib/figure.mjs +81 -0
  183. package/skills/lit-schematic/scripts/lib/fonts.mjs +25 -0
  184. package/skills/lit-schematic/scripts/lib/geom.mjs +113 -0
  185. package/skills/lit-schematic/scripts/lib/interactive.mjs +292 -0
  186. package/skills/lit-schematic/scripts/lib/isa.mjs +115 -0
  187. package/skills/lit-schematic/scripts/lib/iso.mjs +719 -0
  188. package/skills/lit-schematic/scripts/lib/legend-sheet.mjs +162 -0
  189. package/skills/lit-schematic/scripts/lib/pid.mjs +937 -0
  190. package/skills/lit-schematic/scripts/lib/png.mjs +132 -0
  191. package/skills/lit-schematic/scripts/lib/qa-core.mjs +658 -0
  192. package/skills/lit-schematic/scripts/lib/route.mjs +218 -0
  193. package/skills/lit-schematic/scripts/lib/sheet.mjs +890 -0
  194. package/skills/lit-schematic/scripts/lib/svg.mjs +82 -0
  195. package/skills/lit-schematic/scripts/lib/theme.mjs +115 -0
  196. package/skills/lit-schematic/scripts/motion.mjs +129 -0
  197. package/skills/lit-schematic/scripts/qa.mjs +92 -0
  198. package/skills/lit-schematic/scripts/render.mjs +233 -0
  199. package/skills/lit-schematic/scripts/scaffold.mjs +60 -0
  200. package/skills/lit-scientific-visualization/SKILL.md +27 -4
  201. package/skills/lit-typographic-motion/SKILL.md +19 -17
  202. package/skills/lit-typographic-motion/THIRD_PARTY_NOTICES +9 -0
  203. package/skills/lit-typographic-motion/engine/accents.mjs +6 -0
  204. package/skills/lit-typographic-motion/engine/assets.mjs +282 -0
  205. package/skills/lit-typographic-motion/engine/blender/runner.py +228 -0
  206. package/skills/lit-typographic-motion/engine/blender.mjs +126 -0
  207. package/skills/lit-typographic-motion/engine/board.mjs +138 -0
  208. package/skills/lit-typographic-motion/engine/cover/assets.mjs +11 -8
  209. package/skills/lit-typographic-motion/engine/cover/capture.mjs +6 -6
  210. package/skills/lit-typographic-motion/engine/cover/render.mjs +20 -7
  211. package/skills/lit-typographic-motion/engine/cover/treatment.mjs +30 -3
  212. package/skills/lit-typographic-motion/engine/fill.mjs +54 -0
  213. package/skills/lit-typographic-motion/engine/gallery.mjs +118 -0
  214. package/skills/lit-typographic-motion/engine/house.mjs +42 -0
  215. package/skills/lit-typographic-motion/engine/look.mjs +65 -8
  216. package/skills/lit-typographic-motion/engine/loudness.mjs +101 -0
  217. package/skills/lit-typographic-motion/engine/scaffold.mjs +219 -0
  218. package/skills/lit-typographic-motion/engine/score.mjs +594 -0
  219. package/skills/lit-typographic-motion/engine/sound.mjs +9 -57
  220. package/skills/lit-typographic-motion/engine/stage/capture.mjs +23 -9
  221. package/skills/lit-typographic-motion/engine/stage/clock.js +8 -2
  222. package/skills/lit-typographic-motion/engine/stage/cover-init.js +12 -1
  223. package/skills/lit-typographic-motion/engine/stage/frame-worker.mjs +15 -7
  224. package/skills/lit-typographic-motion/engine/stage/gate.mjs +48 -4
  225. package/skills/lit-typographic-motion/engine/stage/qa-page.js +16 -5
  226. package/skills/lit-typographic-motion/engine/stage/qa.mjs +148 -7
  227. package/skills/lit-typographic-motion/engine/stage/render.mjs +69 -7
  228. package/skills/lit-typographic-motion/engine/stage/scan.mjs +20 -1
  229. package/skills/lit-typographic-motion/engine/stage/stage-kit.js +85 -4
  230. package/skills/lit-typographic-motion/engine/stills.mjs +1 -1
  231. package/skills/lit-typographic-motion/engine/styles.mjs +221 -0
  232. package/skills/lit-typographic-motion/engine/text.mjs +1 -1
  233. package/skills/lit-typographic-motion/engine/treatment.mjs +190 -6
  234. package/skills/lit-typographic-motion/engine/vendor/three/LICENSE +21 -0
  235. package/skills/lit-typographic-motion/engine/vendor/three/addons/environments/RoomEnvironment.js +144 -0
  236. package/skills/lit-typographic-motion/engine/vendor/three/addons/geometries/RoundedBoxGeometry.js +155 -0
  237. package/skills/lit-typographic-motion/engine/vendor/three/three.module.js +54571 -0
  238. package/skills/lit-typographic-motion/gate.mjs +18 -2
  239. package/skills/lit-typographic-motion/probe.mjs +13 -1
  240. package/skills/lit-typographic-motion/recipes/hero-3d/expected.json +64 -0
  241. package/skills/lit-typographic-motion/recipes/hero-3d/index.html +132 -0
  242. package/skills/lit-typographic-motion/recipes/orbit-loop/expected.json +2 -0
  243. package/skills/lit-typographic-motion/recipes/painted-light/expected.json +2 -0
  244. package/skills/lit-typographic-motion/recipes/weight-tide/expected.json +2 -0
  245. package/skills/lit-typographic-motion/references/cover.md +56 -13
  246. package/skills/lit-typographic-motion/references/craft-loop.md +20 -7
  247. package/skills/lit-typographic-motion/references/depth.md +112 -0
  248. package/skills/lit-typographic-motion/references/runtime.md +21 -6
  249. package/skills/lit-typographic-motion/references/sound.md +76 -0
  250. package/skills/lit-typographic-motion/references/stage.md +50 -7
  251. package/skills/lit-typographic-motion/references/studio.md +162 -0
  252. package/skills/lit-typographic-motion/references/style-bibles.md +18 -0
  253. package/skills/lit-typographic-motion/references/treatment.md +108 -9
  254. package/skills/lit-typographic-motion/render.mjs +60 -3
  255. package/skills/lit-typographic-motion/requirements-audio.txt +3 -3
  256. package/skills/lit-typographic-motion/styles/paper.json +185 -0
  257. package/skills/lit-typographic-motion/styles/swiss-signal.json +188 -0
  258. package/skills/lit-typographic-motion/styles/terminalcore.json +182 -0
  259. package/skills/lit-typographic-motion/styles/tidal.json +187 -0
  260. package/skills/litresearch/SKILL.md +2 -0
  261. package/skills/managed-skill-manifest.json +520 -91
  262. package/skills/readme-studio/SKILL.md +1 -1
  263. package/skills/readme-studio/templates/hyperframes/package-lock.json +3 -3
  264. package/skills/readme-studio/templates/remotion/package-lock.json +3 -3
  265. package/skills/review-work/SKILL.md +2 -0
  266. package/skills/start-work/SKILL.md +8 -0
  267. package/skills/workflow-loop/SKILL.md +4 -0
  268. package/tools/check-pack-payload.mjs +4 -2
  269. package/tools/payload-substance-parity.json +4 -3
  270. package/tools/qa-cover-recipes.mjs +4 -3
@@ -0,0 +1,221 @@
1
+ // House styles: the data a film is built from, so two films in one style share palette, faces, type
2
+ // scale, layout zones, motion tokens, transitions and sound. A style lives in `styles/<id>.json`; the
3
+ // treatment names one, the validator checks it is the style the rules choose (named in the request,
4
+ // the project's house style, or the fixed keyword pick), the stage renderer serves it to the page as
5
+ // /lit/style.css and /lit/style.js, and the text QA judges copy against it. Layout zones are one grid
6
+ // for every style, named per shot and mapped per format, so a story re-lays itself per format instead
7
+ // of being cropped. The three styles with a `typePreset` mean the same on the type path.
8
+ import { readFileSync } from "node:fs";
9
+ import { fileURLToPath } from "node:url";
10
+ import { keywordMatches } from "./text.mjs";
11
+
12
+ export const STYLE_SCHEMA = "litopencode.motion-style/v1";
13
+ export const STYLE_IDS = Object.freeze(["swiss-signal", "tidal", "terminalcore", "paper"]);
14
+ export const DEFAULT_STYLE = "swiss-signal";
15
+ export const TYPE_ROLES = Object.freeze(["display", "headline", "body", "label"]);
16
+ export const TRANSITIONS = Object.freeze(["cut", "match", "wipe", "push", "iris", "dissolve", "morph"]);
17
+ // Copy may sit this far (a share of the frame side) outside its zone, and this far off a scale step.
18
+ export const ZONE_TOLERANCE = 0.012;
19
+ export const SIZE_TOLERANCE = 0.04;
20
+
21
+ // [x, y, w, h] as fractions of the frame. `type` is where copy may sit; `subject` is where the drawing
22
+ // leads (a `title` or `full` subject spans the frame as a backdrop). Every type zone lies inside the
23
+ // central 90 % title-safe area.
24
+ export const LAYOUTS = Object.freeze({
25
+ "16:9": Object.freeze({
26
+ title: { type: [0.08, 0.2, 0.84, 0.6], subject: [0, 0, 1, 1] },
27
+ split: { type: [0.08, 0.16, 0.36, 0.68], subject: [0.48, 0.08, 0.46, 0.84] },
28
+ "split-alt": { type: [0.56, 0.16, 0.36, 0.68], subject: [0.06, 0.08, 0.46, 0.84] },
29
+ stage: { type: [0.08, 0.75, 0.84, 0.18], subject: [0.06, 0.07, 0.88, 0.64] },
30
+ hero: { type: [0.08, 0.76, 0.84, 0.17], subject: [0.03, 0.03, 0.94, 0.94] },
31
+ full: { type: null, subject: [0, 0, 1, 1] }
32
+ }),
33
+ "9:16": Object.freeze({
34
+ title: { type: [0.09, 0.26, 0.82, 0.48], subject: [0, 0, 1, 1] },
35
+ split: { type: [0.09, 0.08, 0.82, 0.26], subject: [0.06, 0.36, 0.88, 0.56] },
36
+ "split-alt": { type: [0.09, 0.66, 0.82, 0.26], subject: [0.06, 0.06, 0.88, 0.56] },
37
+ stage: { type: [0.09, 0.79, 0.82, 0.13], subject: [0.06, 0.06, 0.88, 0.68] },
38
+ hero: { type: [0.09, 0.8, 0.82, 0.12], subject: [0.03, 0.03, 0.94, 0.94] },
39
+ full: { type: null, subject: [0, 0, 1, 1] }
40
+ })
41
+ });
42
+ export const LAYOUT_IDS = Object.freeze(Object.keys(LAYOUTS["16:9"]));
43
+ const FRAME = Object.freeze({ "16:9": [1920, 1080], "9:16": [1080, 1920] });
44
+
45
+ // With no name, house file or keyword, the genre picks the style where one reads better than the
46
+ // default: an explainer on the warm, legible paper ground (stage only), a brand mood on the tidal flow.
47
+ export const GENRE_STYLE = Object.freeze({ explainer: "paper", "brand-mood": "tidal" });
48
+
49
+ // Checked in order; the first row with a whole-word hit wins. A style the request names outright wins
50
+ // over every row. `paper` has no type-engine preset, so the type path skips its row.
51
+ export const STYLE_PICK = Object.freeze([
52
+ { style: "terminalcore", korean: ["터미널", "CRT", "해커"], latin: ["terminal", "hacker", "crt"] },
53
+ { style: "tidal", korean: ["물결", "파도", "잔잔한", "흐름"], latin: ["gradient", "wave", "tide", "calm"] },
54
+ { style: "paper", korean: ["따뜻한", "밝은", "종이", "손그림", "포근한", "아기자기"], latin: ["warm", "cozy", "bright", "paper", "handmade", "hand-drawn"] }
55
+ ]);
56
+ const NAMED = Object.freeze({
57
+ "swiss-signal": ["swiss-signal", "swiss signal", "스위스 시그널"],
58
+ tidal: ["tidal", "타이달"],
59
+ terminalcore: ["terminalcore", "터미널코어"],
60
+ paper: ["paper style", "paper look", "페이퍼 스타일", "페이퍼"]
61
+ });
62
+
63
+ export class StyleError extends Error {}
64
+
65
+ const HEX = /^#[0-9a-f]{6}$/iu;
66
+ const isText = (v) => typeof v === "string" && v.trim().length > 0;
67
+
68
+ // WCAG relative luminance and contrast ratio of #rrggbb colours.
69
+ export function relativeLuminance(hex) {
70
+ const channel = (i) => {
71
+ const c = parseInt(hex.slice(1 + 2 * i, 3 + 2 * i), 16) / 255;
72
+ return c <= 0.03928 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4;
73
+ };
74
+ return 0.2126 * channel(0) + 0.7152 * channel(1) + 0.0722 * channel(2);
75
+ }
76
+ export function contrastRatio(a, b) {
77
+ const hi = Math.max(a, b), lo = Math.min(a, b);
78
+ return (hi + 0.05) / (lo + 0.05);
79
+ }
80
+ const lum = relativeLuminance;
81
+
82
+ // Validate a style document; returns it or throws StyleError naming the first bad field.
83
+ export function validateStyle(style) {
84
+ const fail = (field, message) => { throw new StyleError(`style ${style?.id ?? "?"}: ${field}: ${message}`); };
85
+ if (!style || typeof style !== "object") fail("style", "must be an object");
86
+ if (style.schema !== STYLE_SCHEMA) fail("schema", `must be ${STYLE_SCHEMA}`);
87
+ if (!STYLE_IDS.includes(style.id)) fail("id", `must be one of ${STYLE_IDS.join(", ")}`);
88
+ for (const k of ["name", "summary"]) if (!isText(style[k])) fail(k, "must be a non-empty string");
89
+ const p = style.palette ?? {};
90
+ for (const role of ["ground", "surface", "ink", "muted", "signal", "line"]) if (!HEX.test(p[role] ?? "")) fail(`palette.${role}`, "must be #rrggbb");
91
+ if (!Array.isArray(style.textRoles) || style.textRoles.some((r) => !HEX.test(p[r] ?? ""))) fail("textRoles", "every text role names a palette colour");
92
+ const ground = lum(p.ground);
93
+ if (contrastRatio(lum(p.ink), ground) < 7) fail("palette.ink", "ink on ground needs 7:1");
94
+ if (contrastRatio(lum(p.muted), ground) < 4.5) fail("palette.muted", "muted on ground needs 4.5:1");
95
+ for (const role of ["signal", "accent"]) if (p[role] && contrastRatio(lum(p[role]), ground) < 3) fail(`palette.${role}`, `${role} on ground needs 3:1 (large type)`);
96
+ if (!Array.isArray(style.accents) || style.accents.length < 3) fail("accents", "names at least three role accents a rich film may add");
97
+ style.accents.forEach((a, i) => {
98
+ if (!isText(a?.role)) fail(`accents[${i}].role`, "says what the colour marks");
99
+ if (!HEX.test(a?.color ?? "")) fail(`accents[${i}].color`, "must be #rrggbb");
100
+ if (contrastRatio(lum(a.color), ground) < 3) fail(`accents[${i}].color`, "an accent on ground needs 3:1 (large type)");
101
+ });
102
+ for (const role of TYPE_ROLES) if (!isText(style.faces?.[role]?.family)) fail(`faces.${role}`, "needs a family");
103
+ for (const format of Object.keys(FRAME)) {
104
+ const s = style.scale?.[format];
105
+ if (!s || TYPE_ROLES.some((r) => !(s[r] > 0))) fail(`scale.${format}`, "needs a size per type role");
106
+ if (!(s.display > s.headline && s.headline > s.body && s.body > s.label)) fail(`scale.${format}`, "steps must descend display > headline > body > label");
107
+ }
108
+ const m = style.motion ?? {};
109
+ for (const k of ["enter", "move", "exit"]) if (!Array.isArray(m.ease?.[k]) || m.ease[k].length !== 4) fail(`motion.ease.${k}`, "is a cubic-bezier [x1, y1, x2, y2]");
110
+ for (const k of ["micro", "move", "hero", "exit"]) if (!(m.dur?.[k] > 0)) fail(`motion.dur.${k}`, "seconds > 0");
111
+ if (!(m.dur.exit <= m.dur.hero)) fail("motion.dur.exit", "an exit is never longer than the hero move");
112
+ if (!Array.isArray(style.transitions) || !style.transitions.length || style.transitions.some((x) => !TRANSITIONS.includes(x))) fail("transitions", `a subset of ${TRANSITIONS.join(", ")}`);
113
+ if (!isText(style.score?.genre)) fail("score.genre", "names the genre a film's score takes by default");
114
+ if (!style.sound || !isText(style.sound.palette)) fail("sound", "needs the generated bed's palette");
115
+ const bedOk = (b) => isText(b?.palette) && isText(b?.key) && b?.tempo > 0;
116
+ if (!Array.isArray(style.beds) || style.beds.length < 2 || !style.beds.every(bedOk)) fail("beds", "lists at least two beds a film may pick, each { palette, key, tempo }");
117
+ const same = (a, b) => ["palette", "key", "tempo"].every((k) => String(a[k]).toLowerCase() === String(b[k]).toLowerCase());
118
+ if (!style.beds.some((b) => same(b, style.sound))) fail("beds", "must include the default bed (sound)");
119
+ if (!Array.isArray(style.bans) || style.bans.length < 3) fail("bans", "names at least three defaults this style never uses");
120
+ return style;
121
+ }
122
+
123
+ const cache = new Map();
124
+ // Load a shipped style by id (validated, frozen).
125
+ export function readStyle(id) {
126
+ if (!STYLE_IDS.includes(id)) throw new StyleError(`unknown style: ${id} (choose ${STYLE_IDS.join(", ")})`);
127
+ if (!cache.has(id)) cache.set(id, Object.freeze(validateStyle(JSON.parse(readFileSync(fileURLToPath(new URL(`../styles/${id}.json`, import.meta.url)), "utf8")))));
128
+ return cache.get(id);
129
+ }
130
+
131
+ // Every font family copy may use in this style (the role faces, the Hangul face, Pretendard).
132
+ export const styleFamilies = (style) => new Set([...TYPE_ROLES.map((r) => style.faces[r].family), style.hangul?.family ?? "Pretendard", "Pretendard"]);
133
+ // The hex colours copy may use.
134
+ export const styleTextColors = (style) => style.textRoles.map((r) => style.palette[r].toLowerCase());
135
+ // The beds a film in this style may pick from, the default first.
136
+ export const styleBeds = (style) => style.beds;
137
+ // The role accents a rich film may add to the core palette, each { role, color }.
138
+ export const styleAccents = (style) => style.accents;
139
+ // All palette colours of the style (what a stage treatment's palette may list); rich adds the accents.
140
+ export const styleColors = (style, { rich = false } = {}) => [...Object.values(style.palette), ...(rich ? style.accents.map((a) => a.color) : [])].map((c) => c.toLowerCase());
141
+
142
+ // Which style a film takes. A style the request names wins; then the project's house style; then the
143
+ // keyword pick; then the genre's style; then the default. Returns { id, source, reason }.
144
+ export function pickStyle({ request = "", copy = [], path = "stage", house = null, genre = null } = {}) {
145
+ const text = String(request);
146
+ for (const [id, names] of Object.entries(NAMED)) {
147
+ if (path === "type" && !readStyle(id).typePreset) continue;
148
+ const hit = names.find((n) => keywordMatches(text, n));
149
+ if (hit) return { id, source: "request", reason: `the request names the ${id} style ("${hit}")` };
150
+ }
151
+ if (house?.style && STYLE_IDS.includes(house.style) && (path !== "type" || readStyle(house.style).typePreset)) {
152
+ return { id: house.style, source: "house", reason: `the project's house style is ${house.style} (motion-house.json)` };
153
+ }
154
+ const haystack = [text, ...copy].join("\n");
155
+ for (const row of STYLE_PICK) {
156
+ if (path === "type" && !readStyle(row.style).typePreset) continue;
157
+ for (const k of [...row.korean, ...row.latin]) if (keywordMatches(haystack, k)) return { id: row.style, source: "auto", reason: `the request mentions "${k}"` };
158
+ }
159
+ const byGenre = GENRE_STYLE[genre];
160
+ if (byGenre && (path !== "type" || readStyle(byGenre).typePreset)) return { id: byGenre, source: "genre", reason: `no style is named and no keyword picks one, and a ${genre} film takes ${byGenre}` };
161
+ return { id: DEFAULT_STYLE, source: "default", reason: `no style is named and no keyword picks one, so the default ${DEFAULT_STYLE}` };
162
+ }
163
+
164
+ // A layout's zones in pixels for a format: { type: {x, y, w, h} | null, subject: {...} }.
165
+ export function zonesPx(format) {
166
+ const [W, H] = FRAME[format];
167
+ const px = (r) => (r ? { x: Math.round(r[0] * W), y: Math.round(r[1] * H), w: Math.round(r[2] * W), h: Math.round(r[3] * H) } : null);
168
+ return Object.fromEntries(Object.entries(LAYOUTS[format]).map(([id, z]) => [id, { type: px(z.type), subject: px(z.subject) }]));
169
+ }
170
+
171
+ // The type step a rendered size sits on, or null when it is off the style's scale.
172
+ export function scaleStep(style, format, sizePx) {
173
+ for (const role of TYPE_ROLES) {
174
+ const step = style.scale[format][role];
175
+ if (Math.abs(sizePx - step) <= step * SIZE_TOLERANCE) return role;
176
+ }
177
+ return null;
178
+ }
179
+
180
+ const familyStack = (style, role) => {
181
+ const hangul = style.hangul?.family ?? "Pretendard";
182
+ return [style.faces[role].family, hangul, "Pretendard"].filter((x, i, a) => a.indexOf(x) === i).map((x) => `"${x}"`).join(", ");
183
+ };
184
+
185
+ // /lit/style.css for one style and format: custom properties, type-role classes, colour classes.
186
+ export function styleCss(style, format) {
187
+ const s = style.scale[format];
188
+ const vars = [
189
+ ...Object.entries(style.palette).map(([k, v]) => ` --lit-${k}: ${v};`),
190
+ ...TYPE_ROLES.map((r) => ` --lit-size-${r}: ${s[r]}px;`)
191
+ ];
192
+ const roles = TYPE_ROLES.map((r) => {
193
+ const f = style.faces[r];
194
+ return `.lit-${r} { font-family: ${familyStack(style, r)}; font-size: ${s[r]}px; font-weight: ${f.weight}; ${f.stretch ? `font-stretch: ${f.stretch}; ` : ""}line-height: ${style.leading?.[r] ?? 1.3}; letter-spacing: 0; margin: 0; }`;
195
+ });
196
+ const colours = style.textRoles.map((r) => `.lit-${r === "ground" ? "knockout" : r} { color: var(--lit-${r}); }`);
197
+ return [
198
+ `/* lit-typographic-motion style: ${style.id} (${format}) */`,
199
+ `:root {\n${vars.join("\n")}\n}`,
200
+ // Korean breaks only between eojeol, never inside a word (type-craft.md).
201
+ `html, body { margin: 0; overflow: hidden; background: var(--lit-ground); color: var(--lit-ink); font-family: ${familyStack(style, "body")}; word-break: keep-all; overflow-wrap: break-word; }`,
202
+ ...roles,
203
+ ...colours,
204
+ ".lit-zone { position: absolute; box-sizing: border-box; }",
205
+ ""
206
+ ].join("\n");
207
+ }
208
+
209
+ // /lit/style.js: window.LitStyle (tokens and pixel zones) and window.LitTimeline (the treatment's shots).
210
+ export function styleScript(style, format, treatment) {
211
+ const timeline = {
212
+ format, fps: treatment.fps ?? 60, duration: treatment.durationSec,
213
+ beats: (treatment.beats ?? []).map((b, i) => ({ index: i, t0: b.t0, t1: b.t1, layout: b.layout ?? null, transition: b.transition ?? null }))
214
+ };
215
+ const data = {
216
+ id: style.id, name: style.name, format, palette: style.palette, textRoles: style.textRoles,
217
+ faces: style.faces, hangul: style.hangul ?? null, scale: style.scale[format], leading: style.leading ?? {},
218
+ motion: style.motion, transitions: style.transitions, ground: style.ground, zones: zonesPx(format)
219
+ };
220
+ return `window.LitStyle = Object.freeze(${JSON.stringify(data)});\nwindow.LitTimeline = Object.freeze(${JSON.stringify(timeline)});\n`;
221
+ }
@@ -107,7 +107,7 @@ export function cpsFloor(text, kind = "line") {
107
107
  return unitCounts(text).C / limits.latinCharsPerSecond;
108
108
  }
109
109
 
110
- function keywordMatches(text, word) {
110
+ export function keywordMatches(text, word) {
111
111
  if (/[a-z]/iu.test(word)) return new RegExp(`\\b${word}\\b`, "iu").test(text);
112
112
  return new RegExp(`(?:^|[^\\uAC00-\\uD7A3\\u1100-\\u11FF\\u3130-\\u318F])${word}`, "u").test(text);
113
113
  }
@@ -5,16 +5,26 @@
5
5
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
6
6
  import path from "node:path";
7
7
  import { fileURLToPath } from "node:url";
8
+ import { readHouse } from "./house.mjs";
9
+ import { LAYOUT_IDS, STYLE_IDS, pickStyle, readStyle, styleBeds, styleColors, styleFamilies } from "./styles.mjs";
8
10
 
9
11
  export const TREATMENT_EXIT = 16;
10
12
  export const genres = Object.freeze(["announcement", "brand-mood", "event", "explainer", "motion-graphics", "type-led", "other"]);
11
13
  export const arcStages = Object.freeze({ announcement: 5, event: 4, explainer: 4, "brand-mood": 4, "motion-graphics": 5, other: 3 });
12
- export const deviceKinds = Object.freeze(["illustration", "diagram", "chart", "icon", "shape", "path", "mask", "depth3d", "particles", "grid", "gradient", "photo-texture"]);
14
+ export const deviceKinds = Object.freeze(["illustration", "diagram", "chart", "icon", "shape", "path", "mask", "depth3d", "screen", "particles", "grid", "gradient", "photo-texture"]);
13
15
  export const textureKinds = Object.freeze(["grid", "gradient", "particles", "photo-texture"]);
14
- export const soundModes = Object.freeze(["generated", "supplied", "authored", "none"]);
16
+ export const soundModes = Object.freeze(["score", "generated", "supplied", "authored", "none"]);
17
+ // The genres a rich film's score may take (engine/score.mjs); a style names its default.
18
+ export const scoreGenres = Object.freeze(["house", "tech-house", "deep-house", "garage"]);
19
+ // How full a film is. Rich is the default; minimal only when the request asks for calm or minimal.
20
+ export const registers = Object.freeze(["rich", "minimal"]);
15
21
  export const soundPalettes = Object.freeze(["glass", "warm", "pulse", "air"]);
16
22
  // The product's own verified faces: the families `/lit/fonts.css` serves and the type engine draws.
17
23
  export const productFaces = Object.freeze(["Archivo", "Pretendard", "VT323", "Silkscreen", "MesloLGS NF", "Galmuri9"]);
24
+ export const protagonistKinds = Object.freeze(["character", "object"]);
25
+ // What a product asset is; a `screen` device needs one of the last three.
26
+ export const assetRoles = Object.freeze(["logo", "wordmark", "icon", "photo", "screenshot", "ui", "readme-image"]);
27
+ const screenRoles = new Set(["screenshot", "ui", "readme-image"]);
18
28
  const floorTenGenres = new Set(["announcement", "event", "explainer", "motion-graphics"]);
19
29
 
20
30
  // NFC, lowercase, then no whitespace and no punctuation.
@@ -64,6 +74,19 @@ const placeholder = /<[^<>\n]{1,80}>/u;
64
74
  const sentences = (text) => String(text).split(/(?<=[.!?。…])\s+/u).map((part) => part.trim()).filter(Boolean);
65
75
  const lengthAsked = /\d+(?:\.\d+)?\s*(?:s|sec|secs|seconds?|min|mins|minutes?|초|분)(?![a-z])/iu;
66
76
  const silenceAsked = /(?<![a-z])(?:silent|silence|no (?:sound|music|audio)|without (?:sound|music|audio)|muted?)(?![a-z])|무음|소리\s?없|음악\s?없|소리\s?빼/iu;
77
+ const fpsAsked = /30\s?(?:fps|프레임|frames?\s+(?:a|per)\s+second)|\b30\s?p\b/iu;
78
+ const previewAsked = /미리\s?보기|스토리보드|콘티|\bpreview\b|\bstoryboard\b/iu;
79
+ const calmAsked = /미니멀|잔잔|차분|조용|심플|절제|고요|\bminimal(?:ist|ism)?\b|\bcalm\b|\bquiet\b|\bunderstated\b|\brestrained\b|\bsparse\b|\bserene\b/iu;
80
+ const pathTracedAsked = /고퀄|고품질|하이엔드|시네마틱|실사|포토리얼|레이\s?트레이싱|블렌더|\b(?:high[- ]?(?:quality|end)|cinematic|photo-?real(?:istic)?|path[- ]?trac\w*|ray[- ]?trac\w*|blender|cycles)\b/iu;
81
+ export const asksCalm = (request) => calmAsked.test(String(request ?? ""));
82
+ export const asksPathTraced = (request) => pathTracedAsked.test(String(request ?? ""));
83
+ export const registerOf = (t) => t?.register ?? "rich";
84
+ export const asks30fps = (request) => fpsAsked.test(String(request ?? ""));
85
+ export const asksPreview = (request) => previewAsked.test(String(request ?? ""));
86
+ // A link in the request names a real product, repository or site: the film uses its real assets.
87
+ const urlInRequest = /\bhttps?:\/\/[^\s"'<>)\]]+/giu;
88
+ export const normUrl = (url) => String(url ?? "").trim().replace(/[.,;:!?]+$/u, "").replace(/\/+$/u, "").replace(/\.git$/u, "").toLowerCase();
89
+ export const requestUrls = (request) => [...String(request ?? "").matchAll(urlInRequest)].map((match) => normUrl(match[0]));
67
90
  const keyPattern = /^[A-G][#b]? (?:major|minor)$/u;
68
91
 
69
92
  class Invalid extends Error {
@@ -83,7 +106,10 @@ function stringLeaves(value, at = "", out = []) {
83
106
  // The free-text leaves the copied-example rule compares.
84
107
  export function freeTextLeaves(t) {
85
108
  const leaves = [t?.idea, t?.audience, t?.channel, t?.ambition];
86
- for (const beat of Array.isArray(t?.beats) ? t.beats : []) leaves.push(beat?.purpose, beat?.onScreen, beat?.motion, beat?.sound);
109
+ for (const beat of Array.isArray(t?.beats) ? t.beats : []) leaves.push(beat?.purpose, beat?.onScreen, beat?.motion, beat?.sound, beat?.hero);
110
+ for (const key of ["expect", "turn", "payoff"]) leaves.push(t?.story?.[key]);
111
+ leaves.push(t?.protagonist?.look);
112
+ for (const act of Array.isArray(t?.protagonist?.acts) ? t.protagonist.acts : []) leaves.push(act?.action);
87
113
  for (const line of Array.isArray(t?.copy?.lines) ? t.copy.lines : []) leaves.push(line);
88
114
  for (const entry of Array.isArray(t?.palette) ? t.palette : []) leaves.push(entry?.role);
89
115
  return leaves.filter((leaf) => typeof leaf === "string").map(normalize).filter(Boolean);
@@ -114,6 +140,7 @@ function checkBeats(t) {
114
140
  need(beat && typeof beat === "object", "beats", `beat ${i} must be an object`);
115
141
  for (const key of ["purpose", "onScreen", "motion", "sound"]) need(text(beat[key]), "beats", `beat ${i} needs ${key}`);
116
142
  need(Number.isFinite(beat.t0) && Number.isFinite(beat.t1), "beats", `beat ${i} needs numeric t0 and t1`);
143
+ if (t.path === "stage") checkShot(t, beat, i);
117
144
  need(beat.t1 - beat.t0 >= 1.2 - 1e-9, "beats", `beat ${i} lasts ${(beat.t1 - beat.t0).toFixed(2)} s; every beat is at least 1.2 s`);
118
145
  if (i > 0) {
119
146
  need(beat.t0 >= t.beats[i - 1].t0, "beats", `beat ${i} starts before beat ${i - 1}`);
@@ -126,13 +153,140 @@ function checkBeats(t) {
126
153
  need(t.beats.length >= stages, "beats", `${t.genre} needs at least ${stages} beats (its arc), found ${t.beats.length}`);
127
154
  }
128
155
 
156
+ // A stage shot sits on the style's layout grid: where the drawing leads, where the copy may sit, the
157
+ // one move the eye follows, and how the shot hands off to the next.
158
+ function checkShot(t, beat, i) {
159
+ const style = readStyle(t.style);
160
+ const at = `beats[${i}]`;
161
+ need(LAYOUT_IDS.includes(beat.layout), "beats", `${at}.layout must be one of ${LAYOUT_IDS.join(", ")}`);
162
+ need(text(beat.hero) && !placeholder.test(beat.hero), "beats", `${at}.hero names the one move the eye follows in this shot`);
163
+ const last = i === t.beats.length - 1;
164
+ if (last) need(beat.transition === "end", "beats", `${at}.transition is end for the last shot`);
165
+ else need(style.transitions.includes(beat.transition), "beats", `${at}.transition: the ${style.id} style hands off with ${style.transitions.join(", ")}`);
166
+ if ("lines" in beat) {
167
+ const count = Array.isArray(t.copy?.lines) ? t.copy.lines.length : 0;
168
+ need(Array.isArray(beat.lines) && beat.lines.every((k) => Number.isInteger(k) && k >= 0 && k < count), "beats", `${at}.lines lists the indices of the copy lines this shot shows`);
169
+ need(!(beat.layout === "full" && beat.lines.length), "beats", `${at}: a full shot has no type zone, so it shows no copy`);
170
+ }
171
+ }
172
+
173
+ // Composition changes with the story: two adjacent shots never share a layout, and a film of four or
174
+ // more shots uses at least three, so the eye is not parked on one split for the whole film.
175
+ function checkVariety(t) {
176
+ for (let i = 1; i < t.beats.length; i++) {
177
+ need(t.beats[i].layout !== t.beats[i - 1].layout, "beats", `shots ${i} and ${i + 1} both use ${t.beats[i].layout}; adjacent shots change layout`);
178
+ }
179
+ const used = new Set(t.beats.map((beat) => beat.layout));
180
+ need(!(t.beats.length >= 4 && used.size < 3), "beats", `${t.beats.length} shots use only ${[...used].join(" and ")}; a film of four or more shots uses at least three layouts`);
181
+ }
182
+
183
+ // The story: what the viewer expects, the turn they do not, the shot where it lands, and the payoff.
184
+ // A genre arc alone is an information order, not a story. Stage films only; on the type path the words are the film.
185
+ function checkStory(t) {
186
+ if (t.path !== "stage") return;
187
+ need("story" in t, "story", "missing: { expect, turn, beat, payoff }: what the viewer expects, what happens instead, the shot where it lands, what the ending gives");
188
+ const story = t.story;
189
+ need(story && typeof story === "object" && !Array.isArray(story), "story", "story must be an object { expect, turn, beat, payoff }");
190
+ for (const key of ["expect", "turn", "payoff"]) {
191
+ need(text(story[key]), `story.${key}`, `story.${key} must be a non-empty string`);
192
+ need(sentences(story[key]).length <= 2, `story.${key}`, `story.${key} is at most 2 sentences`);
193
+ }
194
+ need(normalize(story.turn) !== normalize(story.expect), "story.turn", "the turn is what the viewer does not expect; it cannot equal story.expect");
195
+ need(Number.isInteger(story.beat) && story.beat >= 1 && story.beat < t.beats.length, "story.beat", `story.beat is the index of the shot where the turn lands: after the opening, before the end (1-${t.beats.length - 1})`);
196
+ }
197
+
198
+ // The protagonist: one drawn character or hero object that acts across the film.
199
+ function checkProtagonist(t) {
200
+ if (t.path !== "stage") return;
201
+ need("protagonist" in t, "protagonist", "missing: { name, kind, look, acts[] }: the character or hero object that acts across the film");
202
+ const p = t.protagonist;
203
+ need(p && typeof p === "object" && !Array.isArray(p), "protagonist", "protagonist must be an object { name, kind, look, acts }");
204
+ need(text(p.name), "protagonist.name", "protagonist.name must name it");
205
+ need(protagonistKinds.includes(p.kind), "protagonist.kind", `protagonist.kind must be ${protagonistKinds.join(" or ")}`);
206
+ need(text(p.look), "protagonist.look", "protagonist.look says what it looks like on screen: shape, material, colour role, scale in the frame");
207
+ need(Array.isArray(p.acts) && p.acts.length > 0, "protagonist.acts", "list what it does, shot by shot: [{ beat, action }]");
208
+ const seen = new Set();
209
+ p.acts.forEach((act, i) => {
210
+ const at = `protagonist.acts[${i}]`;
211
+ need(Number.isInteger(act?.beat) && act.beat >= 0 && act.beat < t.beats.length, `${at}.beat`, `${at}.beat must be a beat index`);
212
+ need(!seen.has(act.beat), `${at}.beat`, `${at}: one act per shot`);
213
+ seen.add(act.beat);
214
+ need(text(act.action), `${at}.action`, `${at}.action says what it does in this shot, as a viewer would see it`);
215
+ });
216
+ const covered = beatSeconds(t, [...seen]);
217
+ need(covered >= 0.5 * t.durationSec - 1e-9, "protagonist.acts", `it acts for ${covered.toFixed(1)} s, under half the film; it carries the film, not one shot`);
218
+ need(seen.has(t.story.beat), "protagonist.acts", `the turn lands at shot ${t.story.beat + 1}, and the protagonist has no act there`);
219
+ }
220
+
221
+ // A named product, repository or site: its real logo, images, UI and words, never invented ones. The
222
+ // request's link makes it required; `render.mjs assets` fetches what it can reach.
223
+ function checkProduct(t) {
224
+ const urls = requestUrls(t.request);
225
+ if (!("product" in t)) {
226
+ need(urls.length === 0, "product", `missing: the request links ${urls[0]}; fetch its real assets with render.mjs assets --out <dir> --url <link> and list them`);
227
+ return;
228
+ }
229
+ const p = t.product;
230
+ need(p && typeof p === "object" && !Array.isArray(p), "product", "product must be an object { name, url, assets }");
231
+ need(text(p.name), "product.name", "product.name must name the product");
232
+ need(urls.length === 0 || urls.includes(normUrl(p.url)), "product.url", `product.url must be the link the request gives (${urls.join(", ")})`);
233
+ if ("unreachable" in p) {
234
+ need(text(p.unreachable), "product.unreachable", "say what could not be reached and why");
235
+ return;
236
+ }
237
+ need(Array.isArray(p.assets) && p.assets.length > 0, "product.assets", "list the real assets the film uses: [{ path, role, source, sha256 }]; when none can be reached set unreachable and ask the user");
238
+ p.assets.forEach((asset, i) => {
239
+ const at = `product.assets[${i}]`;
240
+ need(typeof asset?.path === "string" && asset.path.startsWith("stage/assets/") && !asset.path.split("/").includes(".."), `${at}.path`, `${at}.path must be a file under stage/assets/, relative to the output directory`);
241
+ need(assetRoles.includes(asset.role), `${at}.role`, `${at}.role must be one of ${assetRoles.join(", ")}`);
242
+ need(["fetched", "user"].includes(asset.source), `${at}.source`, `${at}.source must be fetched (by render.mjs assets) or user (supplied)`);
243
+ need(/^[0-9a-f]{64}$/u.test(asset.sha256 ?? ""), `${at}.sha256`, `${at}.sha256 must be the file's SHA-256`);
244
+ });
245
+ need(t.subject?.source !== "invented", "subject.source", "the product is real: the subject is the product (source user), its specifics from its own words");
246
+ }
247
+
248
+ // The register is the request's, like the format: rich unless it asks for a calm, quiet or minimal film.
249
+ // A stage-path field; on the type path the words are the film and there is no register to choose.
250
+ function checkRegister(t) {
251
+ if (t.path !== "stage") {
252
+ need(!("register" in t), "register", "register belongs to a stage film; the type path takes none");
253
+ return;
254
+ }
255
+ const register = registerOf(t);
256
+ need(registers.includes(register), "register", "rich (the default) or minimal");
257
+ const calm = asksCalm(t.request);
258
+ need(register !== "minimal" || calm, "register", "minimal only when the request asks for a calm, quiet or minimal film; the default is rich (references/studio.md, Register)");
259
+ need(register !== "rich" || !calm, "register", "the request asks for a calm or minimal film: register minimal");
260
+ }
261
+
262
+ // The 3D route: a depth3d subject is drawn with the pinned three.js on the GPU rung (references/depth.md).
263
+ function checkDepth(t) {
264
+ need(!("depth" in t) || ["2d", "3d"].includes(t.depth), "depth", "depth is 2d (the default) or 3d");
265
+ need(!("render3d" in t) || ["three", "blender"].includes(t.render3d), "render3d", "render3d is three (the GPU rung, the default) or blender (the offline Cycles rung)");
266
+ if (t.path !== "stage") {
267
+ need(t.depth !== "3d", "depth", "3d is a stage-path route");
268
+ return;
269
+ }
270
+ const solid = t.visualDevices.some((device) => device.kind === "depth3d" && device.role === "subject");
271
+ need(!solid || t.depth === "3d", "depth", "the subject is a depth3d device: set depth to 3d, so the film takes the 3D route");
272
+ need(t.depth !== "3d" || solid, "depth", "3d needs the subject drawn as a depth3d device (role subject)");
273
+ need(!("render3d" in t) || t.depth === "3d", "depth", "render3d belongs to a depth 3d film");
274
+ }
275
+
276
+ function checkStyle(t, house) {
277
+ need("style" in t, "style", `missing: one of ${STYLE_IDS.join(", ")}`);
278
+ need(STYLE_IDS.includes(t.style), "style", `must be one of ${STYLE_IDS.join(", ")}`);
279
+ const pick = pickStyle({ request: t.request, copy: Array.isArray(t.copy?.lines) ? t.copy.lines : [], path: t.path, house, genre: t.genre });
280
+ need(t.style === pick.id, "style", `${pick.reason}; this film takes the ${pick.id} style`);
281
+ }
282
+
129
283
  function beatSeconds(t, indices) {
130
284
  const covered = new Set(indices);
131
285
  return [...covered].reduce((sum, i) => sum + (t.beats[i].t1 - t.beats[i].t0), 0);
132
286
  }
133
287
 
134
288
  // Returns { ok: true } or { ok: false, field, detail }.
135
- export function validateTreatment(t, { examples } = {}) {
289
+ export function validateTreatment(t, { examples, house = null } = {}) {
136
290
  try {
137
291
  need(t && typeof t === "object" && !Array.isArray(t), "treatment", "treatment.json must hold a JSON object");
138
292
  const leaf = stringLeaves(t).find(([, value]) => placeholder.test(value));
@@ -141,6 +295,7 @@ export function validateTreatment(t, { examples } = {}) {
141
295
  need(genres.includes(t.genre), "genre", `genre must be one of ${genres.join(", ")}`);
142
296
  need(["type", "stage"].includes(t.path), "path", "path must be type or stage");
143
297
  need(text(t.pathReason), "pathReason", "pathReason must say why this path fits");
298
+ checkStyle(t, house);
144
299
  need(text(t.idea), "idea", "idea must be one sentence");
145
300
  need(sentences(t.idea).length === 1, "idea", "idea must be exactly one sentence");
146
301
  need(!restates(t.idea, t.request), "idea", "idea restates the request; say the film's own idea");
@@ -154,18 +309,28 @@ export function validateTreatment(t, { examples } = {}) {
154
309
  need(["user", "invented"].includes(t.copy.source), "copy", "copy.source must be user or invented");
155
310
  need(Array.isArray(t.copy.lines) && t.copy.lines.length > 0 && t.copy.lines.every(text), "copy", "copy.lines must be non-empty strings");
156
311
  checkBeats(t);
312
+ if (t.path === "stage") checkVariety(t);
157
313
  need(t.subject && typeof t.subject === "object", "subject", "subject must be { name, source, specifics[] }");
158
314
  need(text(t.subject.name), "subject", "subject.name is required");
159
315
  need(["user", "invented"].includes(t.subject.source), "subject", "subject.source must be user or invented");
160
316
  need(Array.isArray(t.subject.specifics) && t.subject.specifics.filter(text).length >= 2, "subject", "subject.specifics needs at least 2 concrete specifics");
161
317
  if (t.subject.source === "user") need(normalize(t.request).includes(normalize(t.subject.name)), "subject", "subject.source is user, but the request does not name it; mark it invented");
318
+ checkRegister(t);
319
+ checkStory(t);
320
+ checkProtagonist(t);
321
+ checkProduct(t);
162
322
  need(Array.isArray(t.visualDevices), "visualDevices", "visualDevices must be an array");
163
323
  t.visualDevices.forEach((device, i) => {
164
324
  need(device && deviceKinds.includes(device.kind), "visualDevices", `device ${i}: kind must be one of ${deviceKinds.join(", ")}`);
165
325
  need(["subject", "support", "texture"].includes(device.role), "visualDevices", `device ${i}: role must be subject, support or texture`);
166
326
  need(!textureKinds.includes(device.kind) || device.role === "texture", "visualDevices", `device ${i}: ${device.kind} is always a texture`);
167
327
  need(Array.isArray(device.beats) && device.beats.length > 0 && device.beats.every((b) => Number.isInteger(b) && b >= 0 && b < t.beats.length), "visualDevices", `device ${i}: beats must list beat indices`);
328
+ if (device.kind === "screen") {
329
+ const real = (Array.isArray(t.product?.assets) ? t.product.assets : []).find((asset) => asset.path === device.asset);
330
+ need(real && screenRoles.has(real.role), "visualDevices", `device ${i}: a product screen shows the product's real screenshot, UI or README image: name one listed in product.assets; never draw an invented one`);
331
+ }
168
332
  });
333
+ checkDepth(t);
169
334
  if (t.path === "stage") {
170
335
  const subjects = t.visualDevices.filter((device) => device.role === "subject");
171
336
  need(subjects.length > 0, "visualDevices", "the stage path needs at least one role: subject device, a drawn depiction of what the film is about");
@@ -177,16 +342,32 @@ export function validateTreatment(t, { examples } = {}) {
177
342
  need(t.path !== "type" || t.copy.source === "user" || typeLedCue(t.request), "path", "the type path needs the user's own words or a type-led cue in the request");
178
343
  need(t.typePlan && typeof t.typePlan === "object", "typePlan", "typePlan must be { faces[], hierarchy, maxWordsOnScreen }");
179
344
  need(Array.isArray(t.typePlan.faces) && t.typePlan.faces.length > 0 && t.typePlan.faces.every((face) => productFaces.includes(face)), "typePlan", `typePlan.faces must come from ${productFaces.join(", ")}`);
345
+ if (t.path === "stage") {
346
+ const families = styleFamilies(readStyle(t.style));
347
+ const off = t.typePlan.faces.find((face) => !families.has(face));
348
+ need(off === undefined, "typePlan", `${off} is not a face of the ${t.style} style (${[...families].join(", ")})`);
349
+ }
180
350
  need(text(t.typePlan.hierarchy), "typePlan", "typePlan.hierarchy is required");
181
351
  need(Number.isInteger(t.typePlan.maxWordsOnScreen) && t.typePlan.maxWordsOnScreen >= 1, "typePlan", "typePlan.maxWordsOnScreen must be a positive integer");
182
- need(Array.isArray(t.palette) && t.palette.length >= 3 && t.palette.length <= 6, "palette", "palette needs 3-6 colours");
352
+ need(Array.isArray(t.palette) && t.palette.length >= 3 && t.palette.length <= (t.path === "stage" && registerOf(t) === "rich" ? 14 : 6), "palette", `palette needs 3-${t.path === "stage" && registerOf(t) === "rich" ? 14 : 6} colours`);
183
353
  t.palette.forEach((entry, i) => need(entry && /^#[0-9a-f]{6}$/iu.test(entry.hex ?? "") && text(entry.role), "palette", `palette ${i} must be { hex: "#rrggbb", role }`));
354
+ if (t.path === "stage") {
355
+ const rich = registerOf(t) === "rich";
356
+ const allowed = new Set(styleColors(readStyle(t.style), { rich }));
357
+ const off = t.palette.find((entry) => !allowed.has(entry.hex.toLowerCase()));
358
+ need(off === undefined, "palette", `${off?.hex} is not a colour of the ${t.style} style${rich ? " or one of its role accents" : " (a minimal film keeps to the core palette)"}; the stage palette comes from the style`);
359
+ }
184
360
  need(t.sound && soundModes.includes(t.sound.mode), "sound", `sound.mode must be one of ${soundModes.join(", ")}`);
185
361
  need(text(t.sound.plan), "sound", "sound.plan is required");
362
+ need(!(t.sound.mode === "generated" && t.path === "stage" && registerOf(t) === "rich"), "sound", "a rich film scores its own music (sound.mode score); the style's preset beds are for minimal films (references/sound.md)");
363
+ need(!("genre" in t.sound) || scoreGenres.includes(t.sound.genre), "sound", `sound.genre must be one of ${scoreGenres.join(", ")}`);
186
364
  if (t.sound.mode === "generated") {
187
365
  need(soundPalettes.includes(t.sound.palette), "sound", `sound.palette must be one of ${soundPalettes.join(", ")}`);
188
366
  need(Number.isFinite(t.sound.tempo) && t.sound.tempo >= 60 && t.sound.tempo <= 180, "sound", "sound.tempo must be 60-180 BPM");
189
367
  need(keyPattern.test(t.sound.key ?? ""), "sound", "sound.key must look like \"D minor\" or \"F# major\"");
368
+ const beds = styleBeds(readStyle(t.style));
369
+ const same = (bed) => bed.palette === t.sound.palette && bed.key.toLowerCase() === t.sound.key.toLowerCase() && bed.tempo === t.sound.tempo;
370
+ need(beds.some(same), "sound", `pick one of the ${t.style} style's beds: ${beds.map((bed) => `${bed.palette} ${bed.key} ${bed.tempo} BPM`).join("; ")}`);
190
371
  }
191
372
  if (t.sound.mode === "supplied" || t.sound.mode === "authored") need(text(t.sound.file), "sound", `sound.file is required for ${t.sound.mode} sound`);
192
373
  if (t.sound.mode === "none") need(silenceAsked.test(t.request) || t.sound.mutedByDesign === true, "sound", "silence needs a user request for it, or sound.mutedByDesign for a channel that plays muted");
@@ -206,6 +387,9 @@ export function validateTreatment(t, { examples } = {}) {
206
387
  need(text(t.ambition) && sentences(t.ambition).length <= 2, "ambition", "ambition must be 1-2 sentences in craft terms");
207
388
  if (t.seed !== undefined) need(Number.isSafeInteger(t.seed), "seed", "seed must be an integer");
208
389
  if (t.fps !== undefined) need(t.fps === 60 || t.fps === 30, "fps", "fps must be 60 or 30");
390
+ need(t.fps !== 30 || asks30fps(t.request), "fps", "fps is 60; 30 only when the request asks for 30 fps");
391
+ if (t.preview !== undefined) need(typeof t.preview === "boolean", "preview", "preview must be true or false");
392
+ need(t.preview !== true || asksPreview(t.request), "preview", "preview is true only when the request asks for a preview or storyboard first");
209
393
  need(!copiesExample(t, examples), "copiedExample", "half or more of the free-text fields equal a shipped example; write this film's own treatment");
210
394
  return { ok: true };
211
395
  } catch (error) {
@@ -230,7 +414,7 @@ export function loadTreatment(out) {
230
414
  let parsed;
231
415
  try { parsed = JSON.parse(readFileSync(file, "utf8")); } catch (error) { throw fail("treatment", `treatment.json is not JSON (${error.message})`); }
232
416
  if (parsed?.mode === "cover") throw fail("mode", "this is a cover treatment; render it with `render.mjs cover --out <dir>` (references/cover.md)");
233
- const result = validateTreatment(parsed);
417
+ const result = validateTreatment(parsed, { house: readHouse(out) });
234
418
  if (!result.ok) throw fail(result.field, result.detail);
235
419
  return { treatment: parsed, file };
236
420
  }
@@ -0,0 +1,21 @@
1
+ The MIT License
2
+
3
+ Copyright © 2010-2024 three.js authors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.