@litfamily/litopencode 1.0.31 → 1.0.33

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 (282) hide show
  1. package/CHANGELOG.md +38 -0
  2. package/README-Ko-KR.md +6 -6
  3. package/README.md +6 -6
  4. package/TRADEMARK.md +12 -0
  5. package/dist/activation-primary-prompts.js +5 -1
  6. package/dist/activation-probe.js +1 -0
  7. package/dist/activation-prompt-utils.d.ts +1 -1
  8. package/dist/activation-reentry.d.ts +20 -0
  9. package/dist/activation-reentry.js +110 -1
  10. package/dist/activation-routing.d.ts +2 -1
  11. package/dist/activation-routing.js +100 -11
  12. package/dist/activation-workflow-prompts.d.ts +4 -0
  13. package/dist/activation-workflow-prompts.js +30 -0
  14. package/dist/activation.d.ts +1 -1
  15. package/dist/activation.js +44 -5
  16. package/dist/adhd-next.d.ts +51 -0
  17. package/dist/adhd-next.js +150 -0
  18. package/dist/agents/defaults.js +1 -1
  19. package/dist/cli/managed-skill-assets.d.ts +1 -1
  20. package/dist/cli/managed-skill-assets.js +7 -1
  21. package/dist/cli/native-skill-integrity.js +1 -1
  22. package/dist/cli.js +7 -1
  23. package/dist/commands.d.ts +28 -1
  24. package/dist/commands.js +58 -5
  25. package/dist/durable-plan.d.ts +5 -1
  26. package/dist/durable-plan.js +14 -2
  27. package/dist/features.d.ts +80 -1
  28. package/dist/features.js +105 -1
  29. package/dist/hooks.js +3 -0
  30. package/dist/ledger.js +12 -1
  31. package/dist/plan-scope-roots.d.ts +23 -0
  32. package/dist/plan-scope-roots.js +116 -0
  33. package/dist/restart-notice.d.ts +7 -0
  34. package/dist/restart-notice.js +40 -0
  35. package/dist/secret-shapes.js +5 -5
  36. package/dist/server.js +4 -0
  37. package/dist/session-liveness.d.ts +20 -0
  38. package/dist/session-liveness.js +99 -0
  39. package/dist/skills.d.ts +22 -1
  40. package/dist/skills.js +36 -0
  41. package/dist/start-work-typed.d.ts +34 -0
  42. package/dist/start-work-typed.js +167 -0
  43. package/dist/state.d.ts +3 -0
  44. package/dist/state.js +3 -0
  45. package/dist/tools.js +22 -2
  46. package/dist/tui/mission-band.d.ts +22 -1
  47. package/dist/tui/mission-band.js +75 -16
  48. package/dist/tui/mission-looks.d.ts +38 -0
  49. package/dist/tui/mission-looks.js +73 -0
  50. package/dist/tui/mission-model.d.ts +2 -0
  51. package/dist/tui/mission-model.js +3 -1
  52. package/dist/tui/mission-rainbow.d.ts +33 -0
  53. package/dist/tui/mission-rainbow.js +90 -0
  54. package/dist/tui/mission-source.d.ts +49 -0
  55. package/dist/tui/mission-source.js +217 -2
  56. package/dist/tui/mission-text.d.ts +2 -0
  57. package/dist/tui/mission-text.js +9 -1
  58. package/dist/tui/mission-threads.d.ts +58 -0
  59. package/dist/tui/mission-threads.js +128 -0
  60. package/dist/tui.js +112 -7
  61. package/dist/uiux-visual-catalog.js +8 -1
  62. package/dist/v2/events.js +3 -0
  63. package/dist/v2/session-hooks.js +38 -5
  64. package/dist/workflow-stack.d.ts +53 -0
  65. package/dist/workflow-stack.js +196 -0
  66. package/docs/assets/readme/badge-version.svg +1 -1
  67. package/docs/assets/skills/lit-schematic.webp +0 -0
  68. package/docs/reference-Ko-KR.md +48 -3
  69. package/docs/reference.md +54 -3
  70. package/package.json +1 -1
  71. package/skills/frontend-ui-ux/SKILL.md +3 -2
  72. package/skills/frontend-ui-ux/kit/components.css +414 -0
  73. package/skills/frontend-ui-ux/kit/fonts.json +22 -0
  74. package/skills/frontend-ui-ux/kit/layouts/landing.html +164 -0
  75. package/skills/frontend-ui-ux/kit/layouts/settings.html +114 -0
  76. package/skills/frontend-ui-ux/kit/layouts/workspace.html +115 -0
  77. package/skills/frontend-ui-ux/kit/styles/computational-brutalism.json +227 -0
  78. package/skills/frontend-ui-ux/kit/styles/field-notes.json +205 -0
  79. package/skills/frontend-ui-ux/kit/styles/graphite.json +200 -0
  80. package/skills/frontend-ui-ux/kit/styles/meadow.json +199 -0
  81. package/skills/frontend-ui-ux/kit/styles/neo-brutalism.json +222 -0
  82. package/skills/frontend-ui-ux/kit/ui.js +226 -0
  83. package/skills/frontend-ui-ux/references/brand-and-imagery.md +2 -2
  84. package/skills/frontend-ui-ux/references/complete-contract.md +2 -1
  85. package/skills/frontend-ui-ux/references/craft-floor.md +6 -2
  86. package/skills/frontend-ui-ux/references/default-editorial-pixel.json +3 -0
  87. package/skills/frontend-ui-ux/references/house-styles.md +236 -0
  88. package/skills/frontend-ui-ux/references/motion-guide.md +2 -2
  89. package/skills/frontend-ui-ux/references/production.md +3 -1
  90. package/skills/frontend-ui-ux/references/slop-register.md +4 -4
  91. package/skills/frontend-ui-ux/scripts/design-lock.mjs +400 -0
  92. package/skills/frontend-ui-ux/scripts/pixel-art.mjs +60 -0
  93. package/skills/frontend-ui-ux/scripts/probe-lock.mjs +235 -0
  94. package/skills/frontend-ui-ux/scripts/probe-page.mjs +34 -12
  95. package/skills/frontend-ui-ux/scripts/probe-static.mjs +16 -3
  96. package/skills/frontend-ui-ux/scripts/probe-thresholds.json +8 -2
  97. package/skills/frontend-ui-ux/scripts/probe.mjs +35 -7
  98. package/skills/lit-adhd/SKILL.md +209 -0
  99. package/skills/lit-adhd/references/brief-format.md +46 -0
  100. package/skills/lit-adhd/references/card-examples.md +103 -0
  101. package/skills/lit-adhd/references/status-taxonomy.md +34 -0
  102. package/skills/lit-adhd/scripts/rewind.mjs +933 -0
  103. package/skills/lit-diagram-drawer/SKILL.md +4 -3
  104. package/skills/lit-diagram-drawer/references/style-guide.md +20 -11
  105. package/skills/lit-diagram-drawer/references/type-architecture.md +3 -3
  106. package/skills/lit-diagram-drawer/references/type-dependency.md +1 -1
  107. package/skills/lit-diagram-drawer/references/type-tree.md +3 -3
  108. package/skills/lit-diagram-drawer/references/type-wardley.md +1 -1
  109. package/skills/lit-diagram-drawer/references/verifier-guide.md +1 -0
  110. package/skills/lit-diagram-drawer/scripts/craft-thresholds.json +1 -2
  111. package/skills/lit-diagram-drawer/scripts/visual-quality.mjs +1 -2
  112. package/skills/lit-docx/SKILL.md +2 -1
  113. package/skills/lit-handoff/SKILL.md +2 -0
  114. package/skills/lit-hijack/SKILL.md +422 -0
  115. package/skills/lit-hijack/references/capture-and-provenance.md +87 -0
  116. package/skills/lit-hijack/references/clean-room-build.md +111 -0
  117. package/skills/lit-hijack/references/distil-and-licence.md +100 -0
  118. package/skills/lit-hijack/references/gap-and-port-design.md +126 -0
  119. package/skills/lit-hijack/references/parity-and-report.md +110 -0
  120. package/skills/lit-hijack/scripts/hijack.mjs +993 -0
  121. package/skills/lit-plan/SKILL.md +8 -0
  122. package/skills/lit-pptx/SKILL.md +27 -11
  123. package/skills/lit-pptx/fonts/jetbrains-mono/JetBrainsMono-Bold.ttf +0 -0
  124. package/skills/lit-pptx/fonts/jetbrains-mono/JetBrainsMono-Regular.ttf +0 -0
  125. package/skills/lit-pptx/fonts/jetbrains-mono/OFL.txt +93 -0
  126. package/skills/lit-pptx/fonts/jetbrains-mono/SOURCE.md +17 -0
  127. package/skills/lit-pptx/references/density-and-fill.md +9 -3
  128. package/skills/lit-pptx/references/direction-step.md +21 -6
  129. package/skills/lit-pptx/references/examples/04-lecture-chalk-ko.md +3 -6
  130. package/skills/lit-pptx/references/examples/05-research-talk-paper-en.md +1 -5
  131. package/skills/lit-pptx/references/examples/08-reliability-keynote-night-en.md +0 -4
  132. package/skills/lit-pptx/references/examples/09-status-update-ledger-en.md +2 -2
  133. package/skills/lit-pptx/references/examples/11-forecast-review-night-ko.md +4 -7
  134. package/skills/lit-pptx/references/examples/13-engineering-review-computational-brutalism-ko.md +173 -0
  135. package/skills/lit-pptx/references/examples/14-launch-pitch-neo-brutalism-ko.md +154 -0
  136. package/skills/lit-pptx/references/examples/15-design-lecture-bauhaus-ko.md +139 -0
  137. package/skills/lit-pptx/references/examples/16-community-programme-riso-ko.md +158 -0
  138. package/skills/lit-pptx/references/examples/17-public-data-briefing-newsroom-ko.md +179 -0
  139. package/skills/lit-pptx/references/layout-families.md +65 -47
  140. package/skills/lit-pptx/references/title-treatments.md +83 -134
  141. package/skills/lit-pptx/references/tonalities/atlas.md +3 -3
  142. package/skills/lit-pptx/references/tonalities/bauhaus.md +140 -0
  143. package/skills/lit-pptx/references/tonalities/chalk.md +7 -6
  144. package/skills/lit-pptx/references/tonalities/computational-brutalism.md +150 -0
  145. package/skills/lit-pptx/references/tonalities/gazette.md +1 -1
  146. package/skills/lit-pptx/references/tonalities/ledger.md +1 -1
  147. package/skills/lit-pptx/references/tonalities/neo-brutalism.md +161 -0
  148. package/skills/lit-pptx/references/tonalities/newsroom.md +144 -0
  149. package/skills/lit-pptx/references/tonalities/night.md +1 -1
  150. package/skills/lit-pptx/references/tonalities/paper.md +2 -2
  151. package/skills/lit-pptx/references/tonalities/riso.md +137 -0
  152. package/skills/lit-pptx/references/tonalities/signal.md +3 -3
  153. package/skills/lit-pptx/references/tonalities/studio.md +7 -6
  154. package/skills/lit-pptx/run.mjs +2 -1
  155. package/skills/lit-pptx/scripts/compile-deck.js +1 -1
  156. package/skills/lit-pptx/scripts/craft-thresholds.json +4 -8
  157. package/skills/lit-pptx/scripts/craft_extras.py +56 -3
  158. package/skills/lit-pptx/scripts/deck_output.py +75 -90
  159. package/skills/lit-pptx/scripts/embed_fonts.py +13 -1
  160. package/skills/lit-pptx/scripts/layout_inventory.py +12 -5
  161. package/skills/lit-pptx/scripts/lib/grid-resolver.js +101 -110
  162. package/skills/lit-pptx/scripts/lib/layout-resolver.js +9 -15
  163. package/skills/lit-pptx/scripts/lib/render-pack.js +373 -566
  164. package/skills/lit-pptx/scripts/lib/slide-grid.js +104 -0
  165. package/skills/lit-pptx/scripts/lib/template-registry.js +57 -6
  166. package/skills/lit-pptx/scripts/qa_deck.py +10 -11
  167. package/skills/lit-pptx/scripts/render_pages.py +118 -0
  168. package/skills/lit-pptx/templates/tonalities/atlas/pack.yaml +1 -1
  169. package/skills/lit-pptx/templates/tonalities/bauhaus/pack.yaml +98 -0
  170. package/skills/lit-pptx/templates/tonalities/chalk/pack.yaml +1 -1
  171. package/skills/lit-pptx/templates/tonalities/computational-brutalism/pack.yaml +99 -0
  172. package/skills/lit-pptx/templates/tonalities/gazette/pack.yaml +1 -1
  173. package/skills/lit-pptx/templates/tonalities/ledger/pack.yaml +1 -1
  174. package/skills/lit-pptx/templates/tonalities/neo-brutalism/pack.yaml +107 -0
  175. package/skills/lit-pptx/templates/tonalities/newsroom/pack.yaml +99 -0
  176. package/skills/lit-pptx/templates/tonalities/night/pack.yaml +1 -1
  177. package/skills/lit-pptx/templates/tonalities/paper/pack.yaml +1 -1
  178. package/skills/lit-pptx/templates/tonalities/riso/pack.yaml +99 -0
  179. package/skills/lit-pptx/templates/tonalities/signal/pack.yaml +2 -2
  180. package/skills/lit-pptx/templates/tonalities/studio/pack.yaml +1 -1
  181. package/skills/lit-recap/SKILL.md +4 -0
  182. package/skills/lit-schematic/SKILL.md +118 -0
  183. package/skills/lit-schematic/examples/iso-heat-exchanger/build.mjs +38 -0
  184. package/skills/lit-schematic/examples/iso-heat-exchanger/scene.mjs +133 -0
  185. package/skills/lit-schematic/examples/journal-adsorption-rig/build.mjs +87 -0
  186. package/skills/lit-schematic/examples/pid-dosing-skid/build.mjs +85 -0
  187. package/skills/lit-schematic/references/cutaway-outputs.md +70 -0
  188. package/skills/lit-schematic/references/isometric-craft.md +101 -0
  189. package/skills/lit-schematic/references/journal-figures.md +72 -0
  190. package/skills/lit-schematic/references/kit-api.md +134 -0
  191. package/skills/lit-schematic/references/pfd-and-layout.md +53 -0
  192. package/skills/lit-schematic/references/pid-drafting.md +103 -0
  193. package/skills/lit-schematic/references/pid-symbols.md +163 -0
  194. package/skills/lit-schematic/references/verification.md +100 -0
  195. package/skills/lit-schematic/scripts/doctor.mjs +57 -0
  196. package/skills/lit-schematic/scripts/lib/figure.mjs +81 -0
  197. package/skills/lit-schematic/scripts/lib/fonts.mjs +25 -0
  198. package/skills/lit-schematic/scripts/lib/geom.mjs +113 -0
  199. package/skills/lit-schematic/scripts/lib/interactive.mjs +292 -0
  200. package/skills/lit-schematic/scripts/lib/isa.mjs +115 -0
  201. package/skills/lit-schematic/scripts/lib/iso.mjs +719 -0
  202. package/skills/lit-schematic/scripts/lib/legend-sheet.mjs +162 -0
  203. package/skills/lit-schematic/scripts/lib/pid.mjs +937 -0
  204. package/skills/lit-schematic/scripts/lib/png.mjs +132 -0
  205. package/skills/lit-schematic/scripts/lib/qa-core.mjs +658 -0
  206. package/skills/lit-schematic/scripts/lib/route.mjs +218 -0
  207. package/skills/lit-schematic/scripts/lib/sheet.mjs +890 -0
  208. package/skills/lit-schematic/scripts/lib/svg.mjs +82 -0
  209. package/skills/lit-schematic/scripts/lib/theme.mjs +115 -0
  210. package/skills/lit-schematic/scripts/motion.mjs +129 -0
  211. package/skills/lit-schematic/scripts/qa.mjs +92 -0
  212. package/skills/lit-schematic/scripts/render.mjs +233 -0
  213. package/skills/lit-schematic/scripts/scaffold.mjs +60 -0
  214. package/skills/lit-scientific-visualization/SKILL.md +27 -4
  215. package/skills/lit-typographic-motion/SKILL.md +19 -17
  216. package/skills/lit-typographic-motion/THIRD_PARTY_NOTICES +9 -0
  217. package/skills/lit-typographic-motion/engine/accents.mjs +6 -0
  218. package/skills/lit-typographic-motion/engine/assets.mjs +282 -0
  219. package/skills/lit-typographic-motion/engine/blender/runner.py +228 -0
  220. package/skills/lit-typographic-motion/engine/blender.mjs +126 -0
  221. package/skills/lit-typographic-motion/engine/board.mjs +138 -0
  222. package/skills/lit-typographic-motion/engine/cover/assets.mjs +11 -8
  223. package/skills/lit-typographic-motion/engine/cover/capture.mjs +6 -6
  224. package/skills/lit-typographic-motion/engine/cover/render.mjs +20 -7
  225. package/skills/lit-typographic-motion/engine/cover/treatment.mjs +30 -3
  226. package/skills/lit-typographic-motion/engine/fill.mjs +54 -0
  227. package/skills/lit-typographic-motion/engine/gallery.mjs +118 -0
  228. package/skills/lit-typographic-motion/engine/house.mjs +42 -0
  229. package/skills/lit-typographic-motion/engine/look.mjs +65 -8
  230. package/skills/lit-typographic-motion/engine/loudness.mjs +101 -0
  231. package/skills/lit-typographic-motion/engine/scaffold.mjs +219 -0
  232. package/skills/lit-typographic-motion/engine/score.mjs +594 -0
  233. package/skills/lit-typographic-motion/engine/sound.mjs +9 -57
  234. package/skills/lit-typographic-motion/engine/stage/capture.mjs +23 -9
  235. package/skills/lit-typographic-motion/engine/stage/clock.js +8 -2
  236. package/skills/lit-typographic-motion/engine/stage/cover-init.js +12 -1
  237. package/skills/lit-typographic-motion/engine/stage/frame-worker.mjs +15 -7
  238. package/skills/lit-typographic-motion/engine/stage/gate.mjs +48 -4
  239. package/skills/lit-typographic-motion/engine/stage/qa-page.js +16 -5
  240. package/skills/lit-typographic-motion/engine/stage/qa.mjs +148 -7
  241. package/skills/lit-typographic-motion/engine/stage/render.mjs +69 -7
  242. package/skills/lit-typographic-motion/engine/stage/scan.mjs +20 -1
  243. package/skills/lit-typographic-motion/engine/stage/stage-kit.js +85 -4
  244. package/skills/lit-typographic-motion/engine/stills.mjs +1 -1
  245. package/skills/lit-typographic-motion/engine/styles.mjs +221 -0
  246. package/skills/lit-typographic-motion/engine/text.mjs +1 -1
  247. package/skills/lit-typographic-motion/engine/treatment.mjs +190 -6
  248. package/skills/lit-typographic-motion/engine/vendor/three/LICENSE +21 -0
  249. package/skills/lit-typographic-motion/engine/vendor/three/addons/environments/RoomEnvironment.js +144 -0
  250. package/skills/lit-typographic-motion/engine/vendor/three/addons/geometries/RoundedBoxGeometry.js +155 -0
  251. package/skills/lit-typographic-motion/engine/vendor/three/three.module.js +54571 -0
  252. package/skills/lit-typographic-motion/gate.mjs +18 -2
  253. package/skills/lit-typographic-motion/probe.mjs +13 -1
  254. package/skills/lit-typographic-motion/recipes/hero-3d/expected.json +64 -0
  255. package/skills/lit-typographic-motion/recipes/hero-3d/index.html +132 -0
  256. package/skills/lit-typographic-motion/recipes/orbit-loop/expected.json +2 -0
  257. package/skills/lit-typographic-motion/recipes/painted-light/expected.json +2 -0
  258. package/skills/lit-typographic-motion/recipes/weight-tide/expected.json +2 -0
  259. package/skills/lit-typographic-motion/references/cover.md +56 -13
  260. package/skills/lit-typographic-motion/references/craft-loop.md +20 -7
  261. package/skills/lit-typographic-motion/references/depth.md +112 -0
  262. package/skills/lit-typographic-motion/references/runtime.md +21 -6
  263. package/skills/lit-typographic-motion/references/sound.md +76 -0
  264. package/skills/lit-typographic-motion/references/stage.md +50 -7
  265. package/skills/lit-typographic-motion/references/studio.md +162 -0
  266. package/skills/lit-typographic-motion/references/style-bibles.md +18 -0
  267. package/skills/lit-typographic-motion/references/treatment.md +108 -9
  268. package/skills/lit-typographic-motion/render.mjs +60 -3
  269. package/skills/lit-typographic-motion/styles/paper.json +185 -0
  270. package/skills/lit-typographic-motion/styles/swiss-signal.json +188 -0
  271. package/skills/lit-typographic-motion/styles/terminalcore.json +182 -0
  272. package/skills/lit-typographic-motion/styles/tidal.json +187 -0
  273. package/skills/litresearch/SKILL.md +2 -0
  274. package/skills/managed-skill-manifest.json +517 -88
  275. package/skills/readme-studio/SKILL.md +1 -1
  276. package/skills/review-work/SKILL.md +2 -0
  277. package/skills/start-work/SKILL.md +8 -0
  278. package/skills/workflow-loop/SKILL.md +4 -0
  279. package/tools/check-pack-payload.mjs +5 -2
  280. package/tools/payload-substance-parity.json +4 -3
  281. package/tools/qa-cover-recipes.mjs +4 -3
  282. package/tools/version-manifests.json +2 -2
@@ -7,53 +7,16 @@
7
7
  import { spawnSync } from "node:child_process";
8
8
  import { existsSync, readFileSync, writeFileSync } from "node:fs";
9
9
  import path from "node:path";
10
+ import { PEAK_CEILING_DBFS, RATE, TARGET_LUFS, dbfs, integratedLoudness, samplePeakDbfs, wavBytes } from "./loudness.mjs";
11
+ import { accentsFor } from "./accents.mjs";
12
+ import { writeScore } from "./score.mjs";
10
13
  import { mulberry32, fnv1a32 } from "./text.mjs";
11
14
 
15
+ // The meter, the sample peak and the WAV writer live in loudness.mjs; the gate and the tests still read them here.
16
+ export { RATE, accentsFor, integratedLoudness, samplePeakDbfs, wavBytes };
12
17
  export const SOUND_EXIT = 20;
13
- export const RATE = 48000;
14
- const TARGET_LUFS = -16;
15
- const PEAK_CEILING_DBFS = -2;
16
18
  const fadeSec = 0.05;
17
19
 
18
- // ---- ITU-R BS.1770-4: K-weighting at 48 kHz, 400 ms blocks with 75 % overlap, -70 LUFS absolute and
19
- // -10 LU relative gates ----
20
- const shelf = { b: [1.53512485958697, -2.69169618940638, 1.19839281085285], a: [1, -1.69065929318241, 0.73248077421585] };
21
- const highPass = { b: [1, -2, 1], a: [1, -1.99004745483398, 0.99007225036621] };
22
- function biquad(input, { b, a }) {
23
- const out = new Float64Array(input.length);
24
- let x1 = 0, x2 = 0, y1 = 0, y2 = 0;
25
- for (let i = 0; i < input.length; i++) {
26
- const x = input[i];
27
- const y = b[0] * x + b[1] * x1 + b[2] * x2 - a[1] * y1 - a[2] * y2;
28
- x2 = x1; x1 = x; y2 = y1; y1 = y;
29
- out[i] = y;
30
- }
31
- return out;
32
- }
33
- export function integratedLoudness(channels, rate = RATE) {
34
- if (rate !== RATE) throw new Error("the loudness meter's K-weighting coefficients are for 48 kHz");
35
- const weighted = channels.map((channel) => biquad(biquad(channel, shelf), highPass));
36
- const block = Math.round(0.4 * rate), hop = Math.round(0.1 * rate);
37
- const blocks = [];
38
- for (let start = 0; start + block <= weighted[0].length; start += hop) {
39
- let sum = 0;
40
- for (const channel of weighted) { let s = 0; for (let i = start; i < start + block; i++) s += channel[i] * channel[i]; sum += s / block; }
41
- blocks.push(sum);
42
- }
43
- const loudness = (z) => -0.691 + 10 * Math.log10(z);
44
- const absolute = blocks.filter((z) => loudness(z) > -70);
45
- if (absolute.length === 0) return -Infinity;
46
- const relativeGate = loudness(absolute.reduce((s, z) => s + z, 0) / absolute.length) - 10;
47
- const gated = absolute.filter((z) => loudness(z) > relativeGate);
48
- return loudness(gated.reduce((s, z) => s + z, 0) / gated.length);
49
- }
50
-
51
- const dbfs = (value) => (value > 0 ? 20 * Math.log10(value) : -Infinity);
52
- export function samplePeakDbfs(channels) {
53
- let peak = 0;
54
- for (const channel of channels) for (const v of channel) peak = Math.max(peak, Math.abs(v));
55
- return dbfs(peak);
56
- }
57
20
  // The longest run of 50 ms windows under `floorDb` RMS inside [0, untilSec).
58
21
  export function quietestStretchSec(channels, rate, untilSec = 3, floorDb = -50) {
59
22
  const win = Math.round(0.05 * rate), end = Math.min(channels[0].length, Math.round(untilSec * rate));
@@ -89,12 +52,6 @@ const palettes = {
89
52
  air: { pad: (f, t) => 0.5 * Math.sin(2 * Math.PI * f * t) + 0.12 * Math.sin(2 * Math.PI * 1.5 * f * t), pulse: 0.25, noise: 0.08 }
90
53
  };
91
54
 
92
- // Accent words a beat's `sound` field may carry.
93
- export function accentsFor(beat) {
94
- const text = String(beat.sound ?? "").toLowerCase();
95
- return { hit: /\bhits?\b|\bimpact\b|\bboom\b|\bstab\b/u.test(text), rise: /\brise\b|\briser\b|\bswell\b|\bbuild\b/u.test(text), cadence: /\bcadence\b|\bresolve\b|\bfinal chord\b/u.test(text), thin: /\bthin(?:s|ned)?\b|\bsparse\b|\bpared back\b/u.test(text) };
96
- }
97
-
98
55
  // cuts[i] is the film time where beat i starts on screen (the render's own cut times).
99
56
  export function buildBed(treatment, { frameCount, fps, cuts }) {
100
57
  const sound = treatment.sound;
@@ -207,15 +164,6 @@ export function buildBed(treatment, { frameCount, fps, cuts }) {
207
164
  return { pcm, cues, stats: { samples: length, durationSec: duration, lufs: Number(integratedLoudness(decoded).toFixed(2)), peakDbfs: Number(samplePeakDbfs(decoded).toFixed(2)), palette: sound.palette, key: sound.key, tempo: sound.tempo } };
208
165
  }
209
166
 
210
- export function wavBytes(pcm, rate = RATE, channels = 2) {
211
- const data = Buffer.from(pcm.buffer, pcm.byteOffset, pcm.byteLength);
212
- const header = Buffer.alloc(44);
213
- header.write("RIFF", 0); header.writeUInt32LE(36 + data.length, 4); header.write("WAVEfmt ", 8);
214
- header.writeUInt32LE(16, 16); header.writeUInt16LE(1, 20); header.writeUInt16LE(channels, 22); header.writeUInt32LE(rate, 24);
215
- header.writeUInt32LE(rate * channels * 2, 28); header.writeUInt16LE(channels * 2, 32); header.writeUInt16LE(16, 34); header.write("data", 36); header.writeUInt32LE(data.length, 40);
216
- return Buffer.concat([header, data]);
217
- }
218
-
219
167
  // Writes <out>/sound.wav and <out>/sound-cues.json for a generated bed; returns the stats.
220
168
  export function writeBed(out, treatment, timing) {
221
169
  const bed = buildBed(treatment, timing);
@@ -230,6 +178,10 @@ export function resolveTrack(out, treatment, timing, briefAudio) {
230
178
  const mode = treatment.sound.mode;
231
179
  if (mode === "none") return { mode };
232
180
  if (mode === "generated") return { mode, ...writeBed(out, treatment, timing), label: "generated" };
181
+ if (mode === "score") {
182
+ const summary = writeScore({ treatment, frameCount: timing.frameCount, fps: timing.fps, cuts: timing.cuts, dir: out });
183
+ return { mode, file: summary.file, label: "score", lufs: summary.lufs, peakDbfs: summary.peakDbfs, key: summary.key, tempo: summary.tempo, palette: summary.genre };
184
+ }
233
185
  const named = treatment.sound.file ?? briefAudio;
234
186
  const file = named ? path.resolve(out, named) : null;
235
187
  if (!file || !existsSync(file)) throw Object.assign(new Error(`SOUND_INVALID (20): the ${mode} track ${named ?? "(none named)"} is missing`), { exitCode: SOUND_EXIT });
@@ -12,12 +12,18 @@ export const stageHost = "lit.stage";
12
12
  export const stageOrigin = `http://${stageHost}`;
13
13
  const here = path.dirname(fileURLToPath(import.meta.url));
14
14
  const kitSource = () => readFileSync(path.join(here, "stage-kit.js"), "utf8");
15
+ const threeRoot = path.join(here, "..", "vendor", "three");
15
16
  const clockSource = () => readFileSync(path.join(here, "clock.js"), "utf8");
16
17
 
17
- // The Wave 1 software rung (SwiftShader, CPU raster; keychain flags included) plus the stage flags.
18
- export function stageChromeFlags(width, height) {
19
- const software = chromeFlagRungs().find((rung) => rung.name === "swiftshader").flags;
20
- return [...software,
18
+ // The stage's Chrome flags on one rung. "swiftshader" (the default) is the software rung: CPU raster,
19
+ // the same pixels on every machine. "gpu" asks the platform's native ANGLE backend for WebGL, so a
20
+ // three.js film draws in real time on the graphics card; page raster stays on the CPU, which keeps the
21
+ // text and the capture path identical to the software rung.
22
+ export function stageChromeFlags(width, height, rung = "swiftshader", platform = process.platform) {
23
+ const found = chromeFlagRungs(platform).find((candidate) => candidate.name === rung);
24
+ if (!found) throw new Error(`unknown Chrome rung "${rung}" (gpu or swiftshader)`);
25
+ const base = rung === "gpu" ? [...found.flags.filter((flag) => flag !== "--enable-gpu-rasterization"), "--disable-gpu-rasterization"] : found.flags;
26
+ return [...base,
21
27
  "--run-all-compositor-stages-before-draw", "--disable-checker-imaging", "--disable-new-content-rendering-timeout",
22
28
  "--disable-threaded-animation", "--disable-threaded-scrolling", "--disable-image-animation-resync", "--disable-lcd-text",
23
29
  "--force-color-profile=srgb", "--hide-scrollbars", "--mute-audio", "--force-device-scale-factor=1", `--window-size=${width},${height}`,
@@ -47,7 +53,7 @@ export function stageFonts(skillRoot, state, pins) {
47
53
  }
48
54
 
49
55
  // Serve the stage origin from disk through request interception: the product's scripts and faces
50
- // under /lit/, every other path from inside `stageDir` only. Anything else is recorded, never fetched:
56
+ // under /lit/ (fonts, and the pinned three.js under /lit/three/), every other path from inside `stageDir` only. Anything else is recorded, never fetched:
51
57
  // another origin in `blocked`, a path that leaves the directory or has no stage type in `refused`, a
52
58
  // file that is not there in `missing`. The stage and the cover browsers both use it.
53
59
  export async function serveStage(context, { stageDir, fonts, scripts }) {
@@ -59,12 +65,19 @@ export async function serveStage(context, { stageDir, fonts, scripts }) {
59
65
  const url = new URL(route.request().url());
60
66
  if (url.origin !== stageOrigin) { blocked.push(url.href); return route.abort("blockedbyclient"); }
61
67
  const pathname = decodeURIComponent(url.pathname);
62
- if (Object.hasOwn(scripts, pathname)) return route.fulfill({ status: 200, contentType: contentTypes[".js"], body: scripts[pathname] });
68
+ if (Object.hasOwn(scripts, pathname)) return route.fulfill({ status: 200, contentType: contentTypes[path.extname(pathname)] ?? contentTypes[".js"], body: scripts[pathname] });
63
69
  if (pathname === "/lit/fonts.css") return route.fulfill({ status: 200, contentType: contentTypes[".css"], body: fonts.css });
64
70
  if (pathname.startsWith("/lit/fonts/")) {
65
71
  const file = fonts.files.get(pathname.slice("/lit/fonts/".length));
66
72
  return file && existsSync(file) ? route.fulfill({ status: 200, contentType: contentTypes[path.extname(file)], body: readFileSync(file) }) : route.fulfill({ status: 404, body: "" });
67
73
  }
74
+ if (pathname.startsWith("/lit/three/")) {
75
+ // The pinned three.js build and its two addons, JavaScript only; the licence and anything else is not served.
76
+ const sub = pathname.slice("/lit/three/".length);
77
+ const file = path.join(threeRoot, sub);
78
+ const ok = !sub.split("/").includes("..") && sub.endsWith(".js") && insideDir(threeRoot, file) && existsSync(file);
79
+ return ok ? route.fulfill({ status: 200, contentType: contentTypes[".js"], body: readFileSync(file) }) : route.fulfill({ status: 404, body: "" });
80
+ }
68
81
  const rel = pathname.replace(/^\/+/u, "") || "index.html";
69
82
  if (rel.split("/").includes("..")) { refused.push(rel); return route.fulfill({ status: 404, body: "" }); }
70
83
  const full = path.join(root, rel);
@@ -79,10 +92,11 @@ export async function serveStage(context, { stageDir, fonts, scripts }) {
79
92
  }
80
93
 
81
94
  // Launch one stage browser. `profile` is a short directory under the run's .run/, removed on close.
82
- export async function openStage(chromium, { chromePath, profile, stageDir, width, height, fps, seed, fonts, extraInit }) {
95
+ // `styleFiles` maps /lit/style.css and /lit/style.js to the film's house style, written per treatment.
96
+ export async function openStage(chromium, { chromePath, profile, stageDir, width, height, fps, seed, fonts, extraInit, styleFiles = {}, rung = "swiftshader" }) {
83
97
  rmSync(profile, { recursive: true, force: true });
84
98
  mkdirSync(profile, { recursive: true });
85
- const flags = stageChromeFlags(width, height);
99
+ const flags = stageChromeFlags(width, height, rung);
86
100
  let context;
87
101
  try {
88
102
  context = await chromium.launchPersistentContext(profile, {
@@ -94,7 +108,7 @@ export async function openStage(chromium, { chromePath, profile, stageDir, width
94
108
  const line = String(error?.message ?? error).split("\n").find((part) => part.trim()) ?? String(error);
95
109
  throw Object.assign(new Error(`BLOCKED_NO_CHROME (10): Chrome failed to launch headless: ${line}; fix: check that Chrome can start headless in this sandbox`), { exitCode: 10 });
96
110
  }
97
- const { blocked, refused, missing } = await serveStage(context, { stageDir, fonts, scripts: { "/lit/stage-kit.js": kitSource() } });
111
+ const { blocked, refused, missing } = await serveStage(context, { stageDir, fonts, scripts: { "/lit/stage-kit.js": kitSource(), ...styleFiles } });
98
112
  await context.addInitScript({ content: `window.__litStageConfig = ${JSON.stringify({ seed: seed >>> 0, fps })};\n${clockSource()}` });
99
113
  if (extraInit) await context.addInitScript({ content: extraInit });
100
114
  const page = context.pages()[0] ?? await context.newPage();
@@ -65,12 +65,18 @@
65
65
  new MutationObserver((list) => { for (const m of list) for (const node of m.addedNodes) inspect(node); }).observe(document, { childList: true, subtree: true });
66
66
 
67
67
  // ---- canvas: WebGL keeps its drawing buffer for capture; 2D text calls are listed for the QA ----
68
- const webgl = { requested: false, failed: false };
68
+ const webgl = { requested: false, failed: false, renderer: null };
69
69
  const nativeGetContext = HTMLCanvasElement.prototype.getContext;
70
70
  HTMLCanvasElement.prototype.getContext = function (type, attributes, ...rest) {
71
71
  const isGl = /webgl/iu.test(String(type));
72
72
  const ctx = nativeGetContext.call(this, type, isGl ? { ...(attributes ?? {}), preserveDrawingBuffer: true } : attributes, ...rest);
73
- if (isGl) { webgl.requested = true; if (!ctx) webgl.failed = true; }
73
+ if (isGl) {
74
+ webgl.requested = true;
75
+ if (!ctx) webgl.failed = true;
76
+ else if (!webgl.renderer) {
77
+ try { const info = ctx.getExtension("WEBGL_debug_renderer_info"); webgl.renderer = info ? String(ctx.getParameter(info.UNMASKED_RENDERER_WEBGL)) : String(ctx.getParameter(ctx.RENDERER)); } catch { webgl.renderer = null; }
78
+ }
79
+ }
74
80
  return ctx;
75
81
  };
76
82
  const canvasTexts = new Set();
@@ -23,7 +23,18 @@
23
23
  CoverDate.UTC = RealDate.UTC;
24
24
  Object.defineProperty(window, "Date", { value: CoverDate, writable: true, configurable: true });
25
25
  Object.defineProperty(Performance.prototype, "now", { value: refuse("performance.now", fromT), writable: true, configurable: true });
26
- Math.random = refuse("Math.random", "use LitLoop.rng(seed) for layout and t for motion");
26
+ // Building a scene before window.ready may draw ids from Math.random (three.js names every object it
27
+ // makes): that phase gets a fixed-seed sequence, the same in every browser. Once the page is ready,
28
+ // frames are drawn in any order, so a call then is misuse.
29
+ const refusedRandom = refuse("Math.random", "use LitLoop.rng(seed) for layout and t for motion");
30
+ let seed = 0x9e3779b9;
31
+ Math.random = function random() {
32
+ if (window.ready === true) return refusedRandom();
33
+ seed = (seed + 0x6d2b79f5) | 0;
34
+ let x = Math.imul(seed ^ (seed >>> 15), 1 | seed);
35
+ x = (x + Math.imul(x ^ (x >>> 7), 61 | x)) ^ x;
36
+ return ((x ^ (x >>> 14)) >>> 0) / 4294967296;
37
+ };
27
38
  window.requestAnimationFrame = refuse("requestAnimationFrame", "the renderer calls renderFrame(t); a cover page runs no loop of its own");
28
39
  window.setInterval = refuse("setInterval", "the renderer calls renderFrame(t); a cover page runs no loop of its own");
29
40
  if (window.crypto) Object.defineProperty(window.crypto, "getRandomValues", { value: refuse("crypto.getRandomValues", "use LitLoop.rng(seed)"), configurable: true });
@@ -4,6 +4,7 @@
4
4
  import { createHash } from "node:crypto";
5
5
  import { parentPort } from "node:worker_threads";
6
6
  import { auditCells, flashGeometryFor, linearLut } from "../flash.mjs";
7
+ import { busyShare } from "../fill.mjs";
7
8
  import { areaResize, decodePng, encodePng } from "../png.mjs";
8
9
 
9
10
  function toRgba(image) {
@@ -16,14 +17,20 @@ function toRgba(image) {
16
17
  return out;
17
18
  }
18
19
 
19
- // p99.5 of linear luminance, the near-black statistic MO-D-03 reads.
20
- function p995(rgba) {
20
+ // The 0.5 % and 99.5 % points of linear luminance. MO-D-03 reads the upper one (near-black), and
21
+ // FLAT-FRAME reads the spread between them: a frame of one flat colour, light or dark, has almost none.
22
+ function spread(rgba) {
21
23
  const bins = new Uint32Array(1024);
22
24
  for (let i = 0; i < rgba.length; i += 4) bins[Math.min(1023, Math.floor((0.2126 * linearLut[rgba[i]] + 0.7152 * linearLut[rgba[i + 1]] + 0.0722 * linearLut[rgba[i + 2]]) * 1024))]++;
23
- const target = (rgba.length / 4) * 0.995;
24
- let acc = 0;
25
- for (let b = 0; b < 1024; b++) { acc += bins[b]; if (acc >= target) return (b + 1) / 1024; }
26
- return 1;
25
+ const total = rgba.length / 4;
26
+ const high = total * 0.995, low = total * 0.005;
27
+ let acc = 0, p005 = null, p995 = 1;
28
+ for (let b = 0; b < 1024; b++) {
29
+ acc += bins[b];
30
+ if (p005 === null && acc >= low) p005 = b / 1024;
31
+ if (acc >= high) { p995 = (b + 1) / 1024; break; }
32
+ }
33
+ return { p005: p005 ?? 0, p995 };
27
34
  }
28
35
 
29
36
  parentPort.on("message", ({ id, png, width, height, preview }) => {
@@ -36,7 +43,8 @@ parentPort.on("message", ({ id, png, width, height, preview }) => {
36
43
  const rgba = toRgba(image);
37
44
  const cells = auditCells(rgba, width, height, flashGeometryFor(width, height));
38
45
  const hash = createHash("sha256").update(rgba).digest("hex");
39
- const out = { id, hash, p995L: Number(p995(rgba).toFixed(5)), cells };
46
+ const { p005, p995 } = spread(rgba);
47
+ const out = { id, hash, p995L: Number(p995.toFixed(5)), p005L: Number(p005.toFixed(5)), busy: Number(busyShare(rgba, width, height).toFixed(4)), cells };
40
48
  if (preview) out.previewPng = encodePng(preview.width, preview.height, areaResize(rgba, width, height, preview.width, preview.height, 4), 4, 1);
41
49
  const copy = rgba.byteOffset === 0 && rgba.byteLength === rgba.buffer.byteLength ? new Uint8Array(rgba.buffer) : Uint8Array.from(rgba);
42
50
  out.rgba = copy;
@@ -11,6 +11,47 @@ import { soundChecks } from "../sound.mjs";
11
11
 
12
12
  const pass = (detail = "") => ({ pass: true, detail });
13
13
  const fail = (detail) => ({ pass: false, detail });
14
+ const notMeasured = (detail) => ({ pass: true, na: true, detail });
15
+
16
+ // The empty-frame rules. A frame of one flat colour (0.5-99.5 % luminance spread under 0.02) may hold
17
+ // for a second (a second more at the open or the close); a frame where fewer than 2.5 % of its 48 px
18
+ // cells carry anything may hold for 0.5 s, with 0.4 s more to build at the open and 0.6 s more to
19
+ // fade at the close. Measured on earlier films, a known dead frame sat at 1.1 % and drawn shots at 5 %
20
+ // and up.
21
+ export const stageGate = Object.freeze({ flatSpread: 0.02, flatHoldSec: 1, flatEdgeSec: 1, deadBusy: 0.025, deadHoldSec: 0.5, deadOpenSec: 0.4, deadCloseSec: 0.6 });
22
+
23
+ // The longest run of frames where `hit` holds that exceeds its allowance, or null.
24
+ function longestRun(count, hit, limit, allowance) {
25
+ let worst = null;
26
+ let start = -1;
27
+ for (let f = 0; f <= count; f++) {
28
+ if (f < count && hit(f)) {
29
+ if (start < 0) start = f;
30
+ continue;
31
+ }
32
+ if (start >= 0) {
33
+ const length = f - start;
34
+ if (length > limit + allowance(start === 0, f === count) && (!worst || length > worst.length)) worst = { length, from: start };
35
+ start = -1;
36
+ }
37
+ }
38
+ return worst;
39
+ }
40
+
41
+ function flatFrames(frameLines, fps) {
42
+ if (!frameLines.length || frameLines.some((line) => typeof line.p005L !== "number")) return notMeasured("per-frame luminance spread not recorded");
43
+ const flat = frameLines.map((line) => line.p995L - line.p005L < stageGate.flatSpread);
44
+ const edge = stageGate.flatEdgeSec * fps;
45
+ const worst = longestRun(flat.length, (f) => flat[f], stageGate.flatHoldSec * fps, (open, close) => (open || close ? edge : 0));
46
+ return worst ? fail(`${(worst.length / fps).toFixed(2)} s of empty frames from ${(worst.from / fps).toFixed(2)} s (nothing but one flat colour; limit ${stageGate.flatHoldSec} s)`) : pass(`no run of empty frames longer than ${stageGate.flatHoldSec} s`);
47
+ }
48
+
49
+ function deadAir(frameLines, fps) {
50
+ if (!frameLines.length || frameLines.some((line) => typeof line.busy !== "number")) return notMeasured("per-frame busy share not recorded");
51
+ const dead = frameLines.map((line) => line.busy < stageGate.deadBusy);
52
+ const worst = longestRun(dead.length, (f) => dead[f], stageGate.deadHoldSec * fps, (open, close) => (open ? stageGate.deadOpenSec * fps : 0) + (close ? stageGate.deadCloseSec * fps : 0));
53
+ return worst ? fail(`${(worst.length / fps).toFixed(2)} s of near-empty frames from ${(worst.from / fps).toFixed(2)} s (under ${stageGate.deadBusy * 100} % of the frame carries anything; limit ${stageGate.deadHoldSec} s)`) : pass(`no near-empty run longer than ${stageGate.deadHoldSec} s`);
54
+ }
14
55
 
15
56
  function probe(file) {
16
57
  const result = spawnSync("ffprobe", ["-v", "error", "-show_entries", "stream=codec_type,codec_name,width,height,r_frame_rate,pix_fmt,color_range,color_space,color_transfer,color_primaries,duration,sample_rate,channels:format=duration", "-of", "json", file], { encoding: "utf8" });
@@ -41,7 +82,7 @@ export function checkStageRun(root) {
41
82
  : !previewLines.length ? fail(`preview frames were not audited; ${flashDetail}`)
42
83
  : general <= limits.flashLimitGeneral && red <= limits.flashLimitRed ? pass(flashDetail) : fail(flashDetail);
43
84
 
44
- if (manifest.webgl?.requested) checks["MO-C-02"] = manifest.webgl.failed ? fail("the page asked for WebGL and got none") : pass("WebGL on the stage's software rung (SwiftShader), labelled software");
85
+ if (manifest.webgl?.requested) checks["MO-C-02"] = manifest.webgl.failed ? fail("the page asked for WebGL and got none") : pass(manifest.rung === "gpu" ? `WebGL on the GPU rung (${manifest.renderer ?? "native ANGLE"})` : `WebGL on the stage's software rung (SwiftShader), labelled software${manifest.warnings?.length ? "; the 3D route fell back here, see the notice" : ""}`);
45
86
 
46
87
  const det = readJson(path.join(root, ".run", "determinism.json"));
47
88
  const mismatch = det?.frames?.find((entry) => entry.master !== entry.replay);
@@ -82,6 +123,8 @@ export function checkStageRun(root) {
82
123
  if (run > allowance + edge) { emptyError = `${run} near-black frames ending at frame ${line.frame}`; break; }
83
124
  }
84
125
  checks["MO-D-03"] = emptyError ? fail(emptyError) : pass(`no near-black run past ${allowance} frames`);
126
+ checks["FLAT-FRAME"] = flatFrames(frameLines, fps);
127
+ checks["DEAD-AIR"] = deadAir(frameLines, fps);
85
128
 
86
129
  if (manifest.sound?.mode) Object.assign(checks, soundChecks(film, manifest.durationSec, manifest.sound.mode));
87
130
  const qa = readJson(path.join(root, ".run", "qa.json"));
@@ -94,7 +137,7 @@ export function checkStageRun(root) {
94
137
  return { manifest, checks, warnings, failed, passed: failed.length === 0, sizes, flash: { master, preview } };
95
138
  }
96
139
 
97
- const order = ["MO-C-03", "MO-C-02", "MO-C-09", "MO-C-10", "MO-C-11", "MO-C-12", "MO-C-13", "MO-C-14", "MO-D-03"];
140
+ const order = ["MO-C-03", "MO-C-02", "MO-C-09", "MO-C-10", "MO-C-11", "MO-C-12", "MO-C-13", "MO-C-14", "MO-D-03", "FLAT-FRAME", "DEAD-AIR"];
98
141
  export function formatStageReport(result, context) {
99
142
  const { manifest, checks } = result;
100
143
  const out = (name) => path.join(context.exportDir, name);
@@ -102,7 +145,8 @@ export function formatStageReport(result, context) {
102
145
  "lit-typographic-motion — render report (stage)",
103
146
  `outputs: ${out("film.mp4")} · ${out(manifest.previewEncoder === "gif" ? "preview.gif" : "preview.webp")} (encoder: ${manifest.previewEncoder}) · ${out("poster.png")} · ${out("reduced-motion.png")}`,
104
147
  `format / fps / length: ${manifest.format} ${manifest.resolution.join("x")} @ ${manifest.fps} fps, ${manifest.durationSec.toFixed(2)} s (treatment ${manifest.targetDurationSec} s)`,
105
- `renderer: stage software rung (flags: ${(manifest.chromeFlags ?? []).join(" ")})`,
148
+ `renderer: stage ${manifest.rung === "gpu" ? `GPU rung (${manifest.renderer ?? "native ANGLE"})` : "software rung"} (flags: ${(manifest.chromeFlags ?? []).join(" ")})`,
149
+ ...(manifest.warnings ?? []).map((note) => `notice: ${note}`),
106
150
  `timing: master ${manifest.timing?.masterSec ?? "?"} s, per frame p50 ${manifest.timing?.frameP50 ?? "?"} ms / p95 ${manifest.timing?.frameP95 ?? "?"} ms, total ${manifest.timing?.totalSec ?? "?"} s`,
107
151
  "",
108
152
  `QA gate: ${result.passed ? "PASS" : "FAIL"}`
@@ -110,7 +154,7 @@ export function formatStageReport(result, context) {
110
154
  const ids = [...order.filter((id) => checks[id]), ...Object.keys(checks).filter((id) => !order.includes(id))];
111
155
  for (const id of ids) {
112
156
  const check = checks[id];
113
- lines.push(` ${id} ${check.pass ? "PASS" : check.warn ? "WARN" : "FAIL"}${check.detail ? ` (${check.detail})` : ""}`);
157
+ lines.push(` ${id} ${check.na ? "N/A" : check.pass ? "PASS" : check.warn ? "WARN" : "FAIL"}${check.detail ? ` (${check.detail})` : ""}`);
114
158
  }
115
159
  lines.push("", `craft rounds run: ${manifest.craftRound} / 3 max`, `frames actually viewed this run: ${context.viewed} (confirmed looked, not just rendered)`);
116
160
  if (result.warnings.length) lines.push(`warnings: ${result.warnings.join("; ")}`);
@@ -37,6 +37,8 @@
37
37
  }
38
38
  return out.map(([x0, y0, x1, y1]) => [Math.max(0, x0), Math.max(0, y0), Math.min(innerWidth, x1), Math.min(innerHeight, y1)]).filter(([x0, y0, x1, y1]) => x1 > x0 && y1 > y0);
39
39
  }
40
+ const firstFamily = (style) => String(style.fontFamily).split(",")[0].trim().replace(/^["']|["']$/gu, "");
41
+
40
42
  function runRoot(textNode) {
41
43
  const parent = textNode.parentElement;
42
44
  const marked = parent.closest("[data-lit-text]");
@@ -60,7 +62,7 @@
60
62
  for (const [root, nodes] of groups) {
61
63
  const range = document.createRange();
62
64
  const rects = [], elements = new Set();
63
- let opacity = 1, fontSize = 0, weight = 400, scale = 1, gradient = false;
65
+ let opacity = 1, fontSize = 0, weight = 400, scale = 1, gradient = false, color = "", family = "";
64
66
  for (const node of nodes) {
65
67
  range.selectNodeContents(node);
66
68
  rects.push(...clipRects(node.parentElement, [...range.getClientRects()]));
@@ -69,11 +71,11 @@
69
71
  opacity = Math.min(opacity, opacityChain(el));
70
72
  const style = getComputedStyle(el);
71
73
  const size = parseFloat(style.fontSize), s = scaleOf(el);
72
- if (size * s > fontSize * scale) { fontSize = size; scale = s; weight = Number(style.fontWeight) || 400; }
74
+ if (size * s > fontSize * scale) { fontSize = size; scale = s; weight = Number(style.fontWeight) || 400; color = style.color; family = firstFamily(style); }
73
75
  if (/text/u.test(style.webkitBackgroundClip || style.backgroundClip) || /url\(|gradient/u.test(style.fill)) gradient = true;
74
76
  }
75
77
  const text = nodes.map((node) => node.nodeValue).join("").replace(/\s+/gu, " ").trim();
76
- runs.push({ text, norm: normalize(text), rects, opacity, fontSize, scale, weight, gradient, decor: root.closest("[data-lit-decor]") !== null, kind: root instanceof SVGElement ? "svg" : "dom", elements: [...elements] });
78
+ runs.push({ text, norm: normalize(text), rects, opacity, fontSize, scale, weight, gradient, color, family, decor: root.closest("[data-lit-decor]") !== null, kind: root instanceof SVGElement ? "svg" : "dom", elements: [...elements] });
77
79
  }
78
80
  for (const el of document.querySelectorAll("body *")) {
79
81
  for (const pseudo of ["::before", "::after"]) {
@@ -82,7 +84,7 @@
82
84
  const text = content.slice(1, -1).trim();
83
85
  if (!text) continue;
84
86
  const style = getComputedStyle(el, pseudo);
85
- runs.push({ text, norm: normalize(text), rects: clipRects(el, [el.getBoundingClientRect()]), opacity: opacityChain(el), fontSize: parseFloat(style.fontSize), scale: scaleOf(el), weight: Number(style.fontWeight) || 400, gradient: false, decor: el.closest("[data-lit-decor]") !== null, kind: "pseudo", elements: [el] });
87
+ runs.push({ text, norm: normalize(text), rects: clipRects(el, [el.getBoundingClientRect()]), opacity: opacityChain(el), fontSize: parseFloat(style.fontSize), scale: scaleOf(el), weight: Number(style.fontWeight) || 400, gradient: false, color: style.color, family: firstFamily(style), decor: el.closest("[data-lit-decor]") !== null, kind: "pseudo", elements: [el] });
86
88
  }
87
89
  }
88
90
  for (const entry of window.__litStage.registered()) {
@@ -147,7 +149,16 @@
147
149
  probeFaces(families) {
148
150
  const box = document.createElement("div");
149
151
  box.style.cssText = "position:absolute;left:0;top:0;opacity:0;pointer-events:none";
150
- families.forEach((family, i) => { const el = document.createElement("span"); el.dataset.litQaProbe = String(i); el.style.fontFamily = `"${family}"`; el.textContent = "Aa가"; box.appendChild(el); });
152
+ // Each family at every weight and width the page can ask for: a static instance reports the file's own
153
+ // family name (Archivo at 900 is "Archivo Black"), so one default-weight probe would call it foreign.
154
+ let i = 0;
155
+ for (const family of families) for (const weight of [400, 700, 900]) for (const stretch of [75, 100, 125]) {
156
+ const el = document.createElement("span");
157
+ el.dataset.litQaProbe = String(i++);
158
+ el.style.cssText = `font-family:"${family}";font-weight:${weight};font-stretch:${stretch}%`;
159
+ el.textContent = "Aa가";
160
+ box.appendChild(el);
161
+ }
151
162
  document.body.appendChild(box);
152
163
  return () => box.remove();
153
164
  },