@litfamily/litopencode 1.0.29 → 1.0.30

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 (151) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/README-Ko-KR.md +7 -7
  3. package/README.md +7 -7
  4. package/dist/activation-primary-prompts.js +15 -6
  5. package/dist/activation-prompt-utils.d.ts +2 -2
  6. package/dist/activation-prompt-utils.js +9 -3
  7. package/dist/activation-reentry.d.ts +29 -0
  8. package/dist/activation-reentry.js +71 -0
  9. package/dist/activation-routing.d.ts +1 -1
  10. package/dist/activation-routing.js +40 -7
  11. package/dist/activation-workflow-prompts.d.ts +2 -0
  12. package/dist/activation-workflow-prompts.js +32 -7
  13. package/dist/activation.d.ts +6 -1
  14. package/dist/activation.js +34 -7
  15. package/dist/cli/args.js +14 -0
  16. package/dist/cli/auto-update.js +2 -4
  17. package/dist/cli/command-plan.d.ts +40 -0
  18. package/dist/cli/command-plan.js +62 -0
  19. package/dist/cli/doctor.d.ts +4 -1
  20. package/dist/cli/doctor.js +65 -4
  21. package/dist/cli/host-detect.d.ts +58 -0
  22. package/dist/cli/host-detect.js +199 -0
  23. package/dist/cli/install-report.js +3 -1
  24. package/dist/cli/install.d.ts +9 -1
  25. package/dist/cli/install.js +134 -31
  26. package/dist/cli/managed-skill-assets.d.ts +1 -1
  27. package/dist/cli/managed-skill-assets.js +5 -3
  28. package/dist/cli/office-dependencies.d.ts +2 -0
  29. package/dist/cli/office-dependencies.js +7 -3
  30. package/dist/cli/scientific-visualization-dependencies.js +1 -1
  31. package/dist/cli/tui-config.d.ts +27 -2
  32. package/dist/cli/tui-config.js +77 -2
  33. package/dist/cli/types.d.ts +17 -0
  34. package/dist/cli.js +9 -6
  35. package/dist/commands.d.ts +10 -1
  36. package/dist/commands.js +17 -3
  37. package/dist/features.d.ts +27 -1
  38. package/dist/features.js +40 -8
  39. package/dist/host/server.d.ts +7 -0
  40. package/dist/host/server.js +12 -0
  41. package/dist/host/tui.d.ts +7 -0
  42. package/dist/host/tui.js +12 -0
  43. package/dist/server.js +9 -0
  44. package/dist/skill-renames.d.ts +1 -1
  45. package/dist/skills.d.ts +9 -2
  46. package/dist/skills.js +22 -8
  47. package/dist/tui/mission-source-v2.d.ts +51 -0
  48. package/dist/tui/mission-source-v2.js +52 -0
  49. package/dist/v2/agents.d.ts +105 -0
  50. package/dist/v2/agents.js +185 -0
  51. package/dist/v2/commands.d.ts +44 -0
  52. package/dist/v2/commands.js +65 -0
  53. package/dist/v2/context.d.ts +67 -0
  54. package/dist/v2/context.js +17 -0
  55. package/dist/v2/events.d.ts +55 -0
  56. package/dist/v2/events.js +193 -0
  57. package/dist/v2/hooks.d.ts +39 -0
  58. package/dist/v2/hooks.js +61 -0
  59. package/dist/v2/server.d.ts +2 -0
  60. package/dist/v2/server.js +68 -0
  61. package/dist/v2/session-hooks.d.ts +64 -0
  62. package/dist/v2/session-hooks.js +230 -0
  63. package/dist/v2/tools.d.ts +26 -0
  64. package/dist/v2/tools.js +49 -0
  65. package/dist/v2/tui.d.ts +63 -0
  66. package/dist/v2/tui.js +259 -0
  67. package/docs/assets/cover-motion-still.webp +0 -0
  68. package/docs/assets/cover-motion.webp +0 -0
  69. package/docs/assets/cover.webp +0 -0
  70. package/docs/assets/readme/badge-version.svg +1 -1
  71. package/docs/assets/readme/litfamily-machines.png +0 -0
  72. package/docs/migration.md +4 -3
  73. package/docs/reference-Ko-KR.md +109 -7
  74. package/docs/reference.md +122 -9
  75. package/package.json +10 -5
  76. package/skills/deep-interview/SKILL.md +1 -1
  77. package/skills/lit-brainstorm/SKILL.md +299 -0
  78. package/skills/lit-brainstorm/references/handoff-briefs.md +72 -0
  79. package/skills/lit-brainstorm/references/lenses.md +106 -0
  80. package/skills/lit-brainstorm/references/scoring-and-convergence.md +93 -0
  81. package/skills/lit-brainstorm/references/session-notes-and-lanes.md +122 -0
  82. package/skills/lit-docx/SKILL.md +1 -1
  83. package/skills/lit-docx/run.py +42 -4
  84. package/skills/lit-docx/scripts/convert_md_to_pdf.py +38 -0
  85. package/skills/lit-docx/templates/latex/acs/template.tex +2 -0
  86. package/skills/lit-docx/templates/latex/elsevier/template.tex +2 -0
  87. package/skills/lit-docx/templates/latex/ieee/template.tex +2 -0
  88. package/skills/lit-docx/templates/latex/korean-generic/template.tex +2 -0
  89. package/skills/lit-docx/templates/latex/nature/template.tex +2 -0
  90. package/skills/lit-humanizer/scripts/detect.mjs +1 -1
  91. package/skills/lit-pptx/SKILL.md +1 -1
  92. package/skills/lit-pptx/command.mjs +94 -0
  93. package/skills/lit-pptx/run.mjs +16 -6
  94. package/skills/lit-pptx/scripts/compile-deck.js +2 -2
  95. package/skills/lit-typographic-motion/SKILL.md +5 -2
  96. package/skills/lit-typographic-motion/THIRD_PARTY_NOTICES +3 -0
  97. package/skills/lit-typographic-motion/engine/browser.mjs +3 -9
  98. package/skills/lit-typographic-motion/engine/command.mjs +94 -0
  99. package/skills/lit-typographic-motion/engine/constants.mjs +29 -1
  100. package/skills/lit-typographic-motion/engine/cover/assets.mjs +91 -0
  101. package/skills/lit-typographic-motion/engine/cover/capture.mjs +173 -0
  102. package/skills/lit-typographic-motion/engine/cover/decode.mjs +173 -0
  103. package/skills/lit-typographic-motion/engine/cover/frame-worker.mjs +19 -0
  104. package/skills/lit-typographic-motion/engine/cover/gate.mjs +261 -0
  105. package/skills/lit-typographic-motion/engine/cover/image.mjs +138 -0
  106. package/skills/lit-typographic-motion/engine/cover/render.mjs +323 -0
  107. package/skills/lit-typographic-motion/engine/cover/sheet.mjs +65 -0
  108. package/skills/lit-typographic-motion/engine/cover/treatment.mjs +110 -0
  109. package/skills/lit-typographic-motion/engine/encode.mjs +1 -1
  110. package/skills/lit-typographic-motion/engine/look.mjs +113 -10
  111. package/skills/lit-typographic-motion/engine/sound.mjs +1 -1
  112. package/skills/lit-typographic-motion/engine/stage/capture.mjs +26 -18
  113. package/skills/lit-typographic-motion/engine/stage/cover-init.js +38 -0
  114. package/skills/lit-typographic-motion/engine/stage/loop-kit.js +102 -0
  115. package/skills/lit-typographic-motion/engine/stage/render.mjs +4 -4
  116. package/skills/lit-typographic-motion/engine/treatment.mjs +1 -0
  117. package/skills/lit-typographic-motion/fonts/JetBrainsMono-OFL.txt +93 -0
  118. package/skills/lit-typographic-motion/fonts/JetBrainsMono-wght-latin.woff2 +0 -0
  119. package/skills/lit-typographic-motion/fonts/pins.json +3 -1
  120. package/skills/lit-typographic-motion/gate.mjs +34 -1
  121. package/skills/lit-typographic-motion/probe.mjs +8 -3
  122. package/skills/lit-typographic-motion/recipes/negatives/card-scenes/expected.json +16 -0
  123. package/skills/lit-typographic-motion/recipes/negatives/card-scenes/index.html +35 -0
  124. package/skills/lit-typographic-motion/recipes/negatives/reset-sweep/expected.json +16 -0
  125. package/skills/lit-typographic-motion/recipes/negatives/reset-sweep/index.html +29 -0
  126. package/skills/lit-typographic-motion/recipes/negatives/still-glow/expected.json +17 -0
  127. package/skills/lit-typographic-motion/recipes/negatives/still-glow/index.html +29 -0
  128. package/skills/lit-typographic-motion/recipes/negatives/title-slam/expected.json +17 -0
  129. package/skills/lit-typographic-motion/recipes/negatives/title-slam/index.html +35 -0
  130. package/skills/lit-typographic-motion/recipes/orbit-loop/expected.json +62 -0
  131. package/skills/lit-typographic-motion/recipes/orbit-loop/index.html +100 -0
  132. package/skills/lit-typographic-motion/recipes/painted-light/expected.json +95 -0
  133. package/skills/lit-typographic-motion/recipes/painted-light/index.html +232 -0
  134. package/skills/lit-typographic-motion/recipes/weight-tide/expected.json +62 -0
  135. package/skills/lit-typographic-motion/recipes/weight-tide/index.html +81 -0
  136. package/skills/lit-typographic-motion/references/cover.md +423 -0
  137. package/skills/lit-typographic-motion/references/craft-loop.md +2 -0
  138. package/skills/lit-typographic-motion/references/runtime.md +17 -2
  139. package/skills/lit-typographic-motion/references/stage.md +3 -0
  140. package/skills/lit-typographic-motion/references/treatment.md +4 -0
  141. package/skills/lit-typographic-motion/render.mjs +30 -6
  142. package/skills/lit-typographic-motion/runtime.mjs +12 -13
  143. package/skills/{durable-litgoal → litgoal}/SKILL.md +31 -12
  144. package/skills/managed-skill-manifest.json +173 -29
  145. package/skills/review-work/SKILL.md +27 -18
  146. package/skills/skill-rename-aliases.json +2 -1
  147. package/tools/check-pack-payload.mjs +4 -1
  148. package/tools/payload-substance-allowlist.json +1 -1
  149. package/tools/payload-substance-parity.json +6 -4
  150. package/tools/qa-cover-recipes.mjs +167 -0
  151. package/tools/version-manifests.json +6 -0
@@ -1,7 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
  // The five MO-A-44 probes, reported on every doctor/status run: Chrome, ffmpeg (with the preview
3
3
  // encoder rung), the WebGL2 renderer string from a real headless probe, the software-GL warning, and
4
- // pre-warm state naming what is missing and the command that fixes it.
4
+ // pre-warm state naming what is missing and the command that fixes it. A sixth line names the cover
5
+ // encoder (cover mode: img2webp decoded with dwebp, else gifski decoded with ffmpeg).
5
6
  import { spawnSync } from "node:child_process";
6
7
  import { existsSync, mkdtempSync, realpathSync, rmSync } from "node:fs";
7
8
  import os from "node:os";
@@ -9,7 +10,9 @@ import path from "node:path";
9
10
  import { fileURLToPath } from "node:url";
10
11
  import { isSoftwareRenderer, unknownRenderer } from "./engine/constants.mjs";
11
12
  import { chromeVersion, findChrome, launchLadder, loadPlaywright, which } from "./engine/browser.mjs";
13
+ import { splitLines } from "./engine/command.mjs";
12
14
  import { inspectWarmState } from "./runtime.mjs";
15
+ import { coverEncoder } from "./engine/cover/render.mjs";
13
16
 
14
17
  export function previewEncoderRung(ffmpeg, env = process.env) {
15
18
  if (ffmpeg) {
@@ -26,11 +29,12 @@ export async function probe(env = process.env) {
26
29
  const ffmpeg = which("ffmpeg", env);
27
30
  const report = {
28
31
  chrome: chrome.path ? { ok: true, path: chrome.path, version: chromeVersion(chrome.path) } : { ok: false, detail: `${chrome.missing} (not found on PATH / not installed)` },
29
- ffmpeg: ffmpeg ? { ok: true, path: ffmpeg, version: (spawnSync(ffmpeg, ["-version"], { encoding: "utf8", timeout: 15000 }).stdout ?? "").split("\n")[0], previewEncoder: previewEncoderRung(ffmpeg, env) }
32
+ ffmpeg: ffmpeg ? { ok: true, path: ffmpeg, version: splitLines(spawnSync(ffmpeg, ["-version"], { encoding: "utf8", timeout: 15000 }).stdout ?? "")[0], previewEncoder: previewEncoderRung(ffmpeg, env) }
30
33
  : { ok: false, detail: "ffmpeg not found on PATH", previewEncoder: previewEncoderRung(undefined, env) },
31
34
  webgl2: { ok: false, renderer: null, flags: [], detail: "" },
32
35
  softwareWarning: null,
33
- prewarm
36
+ prewarm,
37
+ coverEncoder: coverEncoder(env)
34
38
  };
35
39
  if (!chrome.path) report.webgl2.detail = "not probed: Chrome is missing";
36
40
  else if (prewarm.deps.length) report.webgl2.detail = `not probed: the pinned driver is not pre-warmed; run ${prewarm.fix}`;
@@ -63,6 +67,7 @@ export function formatProbe(report) {
63
67
  `motion ffmpeg: ${report.ffmpeg.ok ? `${report.ffmpeg.path} — ${report.ffmpeg.version}` : report.ffmpeg.detail}; preview encoder rung: ${report.ffmpeg.previewEncoder}`,
64
68
  `motion webgl2: ${report.webgl2.ok ? `${report.webgl2.renderer} (rung ${report.webgl2.rung}: ${report.webgl2.flags.join(" ")})` : report.webgl2.detail}`,
65
69
  `motion software-gl: ${report.softwareWarning}`,
70
+ `motion cover encoder: ${report.coverEncoder.name === "none" ? `none; cover renders stop with exit 21; fix: ${report.coverEncoder.fix}` : `${report.coverEncoder.name} (the gate decodes with ${report.coverEncoder.decoder})`}`,
66
71
  `motion prewarm: ${p.ready ? `ready in ${p.cache} (Hangul body: ${p.hangul})` : `missing ${missing.join(", ")}; run ${p.fix}`}; audio: ${p.audio.ready ? "ready" : p.audio.reason}; word timing: ${p.wordTiming.reason}`
67
72
  ].join("\n");
68
73
  }
@@ -0,0 +1,16 @@
1
+ {
2
+ "schemaVersion": 1,
3
+ "note": "A slide-like page that must keep failing these gates at FLOOR on the decoded file, and no other FLOOR gate among G1-G4; tools/qa-cover-recipes.mjs --negatives passes it only when the render completed and was gated. Never tune a threshold so it passes.",
4
+ "mustFail": [
5
+ "G2.floor"
6
+ ],
7
+ "treatment": {
8
+ "loopSec": 9,
9
+ "fps": 20,
10
+ "encode": {
11
+ "q": 78,
12
+ "edgeQ": 78,
13
+ "edgeFrames": 0
14
+ }
15
+ }
16
+ }
@@ -0,0 +1,35 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <!-- NEGATIVE, never a recipe: three text cards, each fading in, holding and fading out. Each hold
6
+ stays under 2 s, so G3 passes its floor, but only the fades move: the decoded file fails G2
7
+ (moving share under 0.45). It must keep failing it. -->
8
+ <link rel="stylesheet" href="/lit/fonts.css">
9
+ <style>
10
+ html, body { margin: 0; width: 1200px; height: 600px; overflow: hidden; background: #0f1318; font-family: "Archivo", sans-serif; }
11
+ .card { position: absolute; left: 150px; top: 150px; width: 900px; height: 300px; background: #e9edf2; color: #10141a; font-size: 72px; font-weight: 900; line-height: 300px; text-align: center; opacity: 0; }
12
+ body.mask { background: #000; }
13
+ body.mask .card { background: #000; color: #fff; }
14
+ </style>
15
+ </head>
16
+ <body>
17
+ <div class="card">LITCARD</div>
18
+ <div class="card">ONE TOOL</div>
19
+ <div class="card">EVERY DAY</div>
20
+ <script>
21
+ const cards = [...document.querySelectorAll(".card")];
22
+ // Each card owns a third of the loop; on a 9 s loop that is 0.6 s in, a 1.8 s hold, 0.6 s out.
23
+ window.renderFrame = (t, { mask = false } = {}) => {
24
+ document.body.className = mask ? "mask" : "";
25
+ cards.forEach((card, i) => {
26
+ const p = t * 3 - i;
27
+ const fade = 0.2;
28
+ const level = p < 0 || p >= 1 ? 0 : p < fade ? p / fade : p > 1 - fade ? (1 - p) / fade : 1;
29
+ card.style.opacity = mask ? (level > 0.5 ? "1" : "0") : level.toFixed(4);
30
+ });
31
+ };
32
+ document.fonts.ready.then(() => { window.renderFrame(0); window.ready = true; });
33
+ </script>
34
+ </body>
35
+ </html>
@@ -0,0 +1,16 @@
1
+ {
2
+ "schemaVersion": 1,
3
+ "note": "A slide-like page that must keep failing these gates at FLOOR on the decoded file, and no other FLOOR gate among G1-G4; tools/qa-cover-recipes.mjs --negatives passes it only when the render completed and was gated. Never tune a threshold so it passes.",
4
+ "mustFail": [
5
+ "G4.floor"
6
+ ],
7
+ "treatment": {
8
+ "loopSec": 8,
9
+ "fps": 20,
10
+ "encode": {
11
+ "q": 78,
12
+ "edgeQ": 78,
13
+ "edgeFrames": 0
14
+ }
15
+ }
16
+ }
@@ -0,0 +1,29 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <!-- NEGATIVE, never a recipe: a progress sweep that fills the frame over the loop and resets at the
6
+ seam. Everything moves steadily, so G2 and G3 pass their floors, but the jump back to empty is a
7
+ step far larger than its neighbours: the decoded file fails G4 (the seam). It must keep failing. -->
8
+ <link rel="stylesheet" href="/lit/fonts.css">
9
+ <style>
10
+ html, body { margin: 0; width: 1200px; height: 600px; overflow: hidden; background: #0e1217; font-family: "Archivo", sans-serif; }
11
+ #fill { position: absolute; left: 0; top: 0; height: 600px; width: 0; background: #2f4a63; }
12
+ #word { position: absolute; left: 0; right: 0; top: 240px; text-align: center; font-size: 96px; font-weight: 900; color: #f3f5f8; }
13
+ body.mask { background: #000; }
14
+ body.mask #fill { visibility: hidden; }
15
+ </style>
16
+ </head>
17
+ <body>
18
+ <div id="fill"></div>
19
+ <div id="word">LITSWEEP</div>
20
+ <script>
21
+ const fill = document.getElementById("fill");
22
+ window.renderFrame = (t, { mask = false } = {}) => {
23
+ document.body.className = mask ? "mask" : "";
24
+ fill.style.width = `${(1200 * t).toFixed(3)}px`;
25
+ };
26
+ document.fonts.ready.then(() => { window.renderFrame(0); window.ready = true; });
27
+ </script>
28
+ </body>
29
+ </html>
@@ -0,0 +1,17 @@
1
+ {
2
+ "schemaVersion": 1,
3
+ "note": "A slide-like page that must keep failing these gates at FLOOR on the decoded file, and no other FLOOR gate among G1-G4; tools/qa-cover-recipes.mjs --negatives passes it only when the render completed and was gated. Never tune a threshold so it passes.",
4
+ "mustFail": [
5
+ "G2.floor",
6
+ "G3.floor"
7
+ ],
8
+ "treatment": {
9
+ "loopSec": 8,
10
+ "fps": 16,
11
+ "encode": {
12
+ "q": 72,
13
+ "edgeQ": 72,
14
+ "edgeFrames": 0
15
+ }
16
+ }
17
+ }
@@ -0,0 +1,29 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <!-- NEGATIVE, never a recipe: a still picture with a faint breathing glow. The glow changes each
6
+ pixel by less than the encoder's threshold, so the decoded file barely moves: it fails G2 and
7
+ G3 (the whole loop reads as one hold). It must keep failing them. -->
8
+ <link rel="stylesheet" href="/lit/fonts.css">
9
+ <style>
10
+ html, body { margin: 0; width: 1200px; height: 600px; overflow: hidden; background: #0d1116; font-family: "Archivo", sans-serif; }
11
+ #word { position: absolute; left: 0; right: 0; top: 240px; text-align: center; font-size: 96px; font-weight: 900; color: #f3f5f8; }
12
+ #glow { position: absolute; left: 350px; top: 150px; width: 500px; height: 300px; border-radius: 50%; background: radial-gradient(closest-side, rgba(255, 140, 80, 0.35), rgba(255, 140, 80, 0)); }
13
+ body.mask { background: #000; }
14
+ body.mask #glow { visibility: hidden; }
15
+ </style>
16
+ </head>
17
+ <body>
18
+ <div id="glow"></div>
19
+ <div id="word">LITGLOW</div>
20
+ <script>
21
+ const glow = document.getElementById("glow");
22
+ window.renderFrame = (t, { mask = false } = {}) => {
23
+ document.body.className = mask ? "mask" : "";
24
+ glow.style.opacity = (0.92 + 0.08 * Math.sin(2 * Math.PI * t)).toFixed(4);
25
+ };
26
+ document.fonts.ready.then(() => { window.renderFrame(0); window.ready = true; });
27
+ </script>
28
+ </body>
29
+ </html>
@@ -0,0 +1,17 @@
1
+ {
2
+ "schemaVersion": 1,
3
+ "note": "A slide-like page that must keep failing these gates at FLOOR on the decoded file, and no other FLOOR gate among G1-G4; tools/qa-cover-recipes.mjs --negatives passes it only when the render completed and was gated. Never tune a threshold so it passes.",
4
+ "mustFail": [
5
+ "G2.floor",
6
+ "G3.floor"
7
+ ],
8
+ "treatment": {
9
+ "loopSec": 10,
10
+ "fps": 20,
11
+ "encode": {
12
+ "q": 78,
13
+ "edgeQ": 78,
14
+ "edgeFrames": 0
15
+ }
16
+ }
17
+ }
@@ -0,0 +1,35 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <!-- NEGATIVE, never a recipe: the slide habit cover mode exists to stop. A title slams in, three
6
+ lines arrive one by one, and an end card holds. Most of the loop is a hold, so the decoded file
7
+ fails G2 (motion energy) and G3 (longest still run). It must keep failing them. -->
8
+ <link rel="stylesheet" href="/lit/fonts.css">
9
+ <style>
10
+ html, body { margin: 0; width: 1200px; height: 600px; overflow: hidden; background: #10141a; font-family: "Archivo", sans-serif; color: #f2f4f7; }
11
+ #title { position: absolute; left: 0; right: 0; top: 150px; text-align: center; font-size: 84px; font-weight: 900; }
12
+ .line { position: absolute; left: 0; right: 0; text-align: center; font-size: 30px; font-weight: 400; }
13
+ body.mask { background: #000; }
14
+ body.mask .line { visibility: hidden; }
15
+ body.mask #title { color: #fff; }
16
+ </style>
17
+ </head>
18
+ <body>
19
+ <div id="title">LITCARD</div>
20
+ <div class="line" style="top: 300px">first line</div>
21
+ <div class="line" style="top: 350px">second line</div>
22
+ <div class="line" style="top: 400px">third line</div>
23
+ <script>
24
+ const lines = [...document.querySelectorAll(".line")];
25
+ const title = document.getElementById("title");
26
+ window.renderFrame = (t, { mask = false } = {}) => {
27
+ document.body.className = mask ? "mask" : "";
28
+ const slam = Math.min(1, t / 0.04);
29
+ title.style.transform = `scale(${(1.6 - 0.6 * slam).toFixed(4)})`;
30
+ lines.forEach((line, i) => { line.style.opacity = t > 0.12 + 0.1 * i ? "1" : "0"; });
31
+ };
32
+ document.fonts.ready.then(() => { window.renderFrame(0); window.ready = true; });
33
+ </script>
34
+ </body>
35
+ </html>
@@ -0,0 +1,62 @@
1
+ {
2
+ "schemaVersion": 1,
3
+ "note": "The decoded gate row this recipe reached when it shipped; tools/qa-cover-recipes.mjs renders it again and fails when median d or the moving share move more than 15 %, seamLocal leaves the cover TARGET, the file passes 2 MiB or the render exits non-zero. Measured 2026-10-20 on headless Chrome's software rung with CPU raster and libwebp 1.6.0.",
4
+ "row": {
5
+ "medianD": 0.002325,
6
+ "movingFrac": 1.0,
7
+ "longestStillSec": 0.0,
8
+ "spike": 2.394,
9
+ "seamLocal": 1.492,
10
+ "bytes": 1576692
11
+ },
12
+ "settings": {
13
+ "size": [
14
+ 1000,
15
+ 500
16
+ ],
17
+ "fps": 25,
18
+ "frames": 200,
19
+ "encode": {
20
+ "q": 78,
21
+ "edgeQ": 90,
22
+ "edgeFrames": 20,
23
+ "method": 4
24
+ }
25
+ },
26
+ "treatment": {
27
+ "mode": "cover",
28
+ "request": "Make a looping README cover for LitOpenCode that shows a running system.",
29
+ "idea": "Three orbits cross one gate and every crossing lights a cell of the ledger under the name.",
30
+ "recipe": "orbit-loop",
31
+ "recipeReason": "A system with whole-number laps closes by itself and gives the beat events a cause.",
32
+ "brandWord": "LITOPENCODE",
33
+ "aspect": "2:1",
34
+ "loopSec": 8,
35
+ "fps": 25,
36
+ "layers": {
37
+ "slow": "Dashed guide rings turn one dash period per loop.",
38
+ "mid": "Three dots lap their rings three, two and one times.",
39
+ "beat": "The gate flashes and a ledger cell lights at each crossing; the core rings on four bars."
40
+ },
41
+ "system": {
42
+ "rule": "One orbit law, angle from laps and direction.",
43
+ "copies": "Three rings.",
44
+ "offset": "Each ring's start angle and direction."
45
+ },
46
+ "title": {
47
+ "box": [
48
+ 0.05,
49
+ 0.39,
50
+ 0.375,
51
+ 0.14
52
+ ],
53
+ "kind": "live"
54
+ },
55
+ "encode": {
56
+ "q": 78,
57
+ "edgeQ": 90,
58
+ "edgeFrames": 20
59
+ },
60
+ "ambition": "Every beat event has a visible cause, and the crossings are solved rather than detected."
61
+ }
62
+ }
@@ -0,0 +1,100 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <!-- Cover recipe "orbit-loop": a system beside a still word. Three rings turn a whole number of laps
6
+ per loop (3, 2, 1), in alternating directions, each carrying a dot with a trail. A gate at twelve
7
+ o'clock flashes whenever a dot crosses it, and the crossing times, computed rather than detected,
8
+ light the cells of a ledger under the word: six crossings, six cells, so the ledger loops too.
9
+ The core breathes once per loop and rings on each of four bars. Change WORD and the colours. -->
10
+ <link rel="stylesheet" href="/lit/fonts.css">
11
+ <script src="/lit/loop-kit.js"></script>
12
+ <style>
13
+ html, body { margin: 0; width: 1200px; height: 600px; overflow: hidden; background: #0c1015; }
14
+ canvas { position: absolute; inset: 0; width: 1200px; height: 600px; }
15
+ #word { position: absolute; left: 70px; top: 238px; font: 800 64px/80px "JetBrains Mono", monospace; letter-spacing: 0; color: #f1f4f7; white-space: pre; }
16
+ body.mask { background: #000; }
17
+ body.mask canvas { visibility: hidden; }
18
+ body.mask #word { color: #fff; }
19
+ </style>
20
+ </head>
21
+ <body>
22
+ <canvas id="orbit" width="2400" height="1200"></canvas>
23
+ <div id="word">LITOPENCODE</div>
24
+ <script>
25
+ const { TAU, fract, mix, ease, cubicPulse, cdist } = LitLoop;
26
+ const ctx = document.getElementById("orbit").getContext("2d");
27
+ ctx.scale(2, 2);
28
+ const CORE = [880, 300];
29
+ const GATE = -Math.PI / 2;
30
+ // One orbit rule, three copies: radius, laps per loop, direction, and where the dot starts.
31
+ const RINGS = [
32
+ { r: 110, laps: 3, dir: 1, start: 0.3, colour: [255, 138, 76] },
33
+ { r: 185, laps: 2, dir: -1, start: 1.9, colour: [120, 200, 255] },
34
+ { r: 260, laps: 1, dir: 1, start: 4.1, colour: [196, 232, 92] }
35
+ ];
36
+ const angle = (ring, t) => ring.start + ring.dir * TAU * ring.laps * t;
37
+ // Every crossing of the gate in [0, 1), solved from the angle law: start + dir 2pi laps t = GATE + 2pi k.
38
+ const crossings = RINGS.flatMap((ring, i) => Array.from({ length: ring.laps }, (_, k) => ({ ring: i, t: fract(((GATE - ring.start) / (ring.dir * TAU) + k) / ring.laps) }))).sort((a, b) => a.t - b.t);
39
+ const LEDGER = { x: 72, y: 340, cell: 54, gap: 10 };
40
+ const since = (t, at) => fract(t - at);
41
+ // A lit cell falls back in four flat steps over 0.4 of the loop: big steps the encoder keeps.
42
+ const cellLevel = (s) => (s < 0.4 ? Math.ceil(4 * (1 - s / 0.4)) / 4 : 0);
43
+
44
+ window.renderFrame = (t, { mask = false } = {}) => {
45
+ document.body.className = mask ? "mask" : "";
46
+ ctx.clearRect(0, 0, 1200, 600);
47
+ if (mask) return;
48
+ // SLOW: dashed guide rings turn perpetually, one dash period per loop in alternate directions.
49
+ ctx.lineWidth = 3;
50
+ RINGS.forEach((ring, i) => {
51
+ ctx.setLineDash([10, 14]);
52
+ ctx.lineDashOffset = (i % 2 ? -1 : 1) * 24 * fract(t) * 4;
53
+ ctx.strokeStyle = "rgba(120, 140, 160, 0.55)";
54
+ ctx.beginPath(); ctx.arc(CORE[0], CORE[1], ring.r, 0, TAU); ctx.stroke();
55
+ });
56
+ ctx.setLineDash([]);
57
+ // MID: the dots and their fading arc trails.
58
+ RINGS.forEach((ring) => {
59
+ const a = angle(ring, t), [r, g, b] = ring.colour;
60
+ for (let s = 0; s < 20; s++) {
61
+ const a0 = a - ring.dir * 0.05 * s, a1 = a - ring.dir * 0.05 * (s + 1);
62
+ ctx.strokeStyle = `rgba(${r}, ${g}, ${b}, ${0.8 * (1 - s / 20)})`;
63
+ ctx.lineWidth = 9 - s * 0.35;
64
+ ctx.beginPath(); ctx.arc(CORE[0], CORE[1], ring.r, Math.min(a0, a1), Math.max(a0, a1)); ctx.stroke();
65
+ }
66
+ });
67
+ // BEAT: the gate flashes on each crossing and the crossing dot swells.
68
+ const hit = Math.max(...crossings.map((c) => cubicPulse(0, 0.025, cdist(t, c.t))));
69
+ ctx.fillStyle = `rgba(255, 236, 200, ${0.35 + 0.65 * hit})`;
70
+ ctx.fillRect(CORE[0] - 4, CORE[1] - RINGS[2].r - 24, 8, RINGS[2].r - RINGS[0].r + 48);
71
+ RINGS.forEach((ring, i) => {
72
+ const a = angle(ring, t), [r, g, b] = ring.colour;
73
+ const pulse = Math.max(0, ...crossings.filter((c) => c.ring === i).map((c) => cubicPulse(0, 0.03, cdist(t, c.t))));
74
+ ctx.fillStyle = `rgb(${r}, ${g}, ${b})`;
75
+ ctx.beginPath(); ctx.arc(CORE[0] + ring.r * Math.cos(a), CORE[1] + ring.r * Math.sin(a), 11 + 7 * pulse, 0, TAU); ctx.fill();
76
+ });
77
+ // The core breathes once per loop and rings on each of four bars.
78
+ const breath = 0.5 - 0.5 * Math.cos(TAU * t);
79
+ ctx.fillStyle = `rgba(255, 170, 110, ${0.55 + 0.35 * breath})`;
80
+ ctx.beginPath(); ctx.arc(CORE[0], CORE[1], 34 + 8 * breath, 0, TAU); ctx.fill();
81
+ const bar = fract(4 * t), ring = ease.outExpo(bar);
82
+ ctx.strokeStyle = `rgba(255, 190, 140, ${0.7 * (1 - bar)})`;
83
+ ctx.lineWidth = 4;
84
+ ctx.beginPath(); ctx.arc(CORE[0], CORE[1], 40 + 60 * ring, 0, TAU); ctx.stroke();
85
+ // The ledger under the word: one cell per crossing, lit at its crossing time.
86
+ crossings.forEach((c, k) => {
87
+ const [r, g, b] = RINGS[c.ring].colour, level = cellLevel(since(t, c.t));
88
+ ctx.fillStyle = "#1a222c";
89
+ ctx.fillRect(LEDGER.x + k * (LEDGER.cell + LEDGER.gap), LEDGER.y, LEDGER.cell, 14);
90
+ if (level > 0) {
91
+ ctx.fillStyle = `rgba(${r}, ${g}, ${b}, ${level})`;
92
+ ctx.fillRect(LEDGER.x + k * (LEDGER.cell + LEDGER.gap), LEDGER.y, LEDGER.cell, 14);
93
+ }
94
+ });
95
+ };
96
+
97
+ document.fonts.load('800 64px "JetBrains Mono"').then(() => { window.renderFrame(0); window.ready = true; });
98
+ </script>
99
+ </body>
100
+ </html>
@@ -0,0 +1,95 @@
1
+ {
2
+ "schemaVersion": 1,
3
+ "note": "The decoded gate row this recipe reached when it shipped; tools/qa-cover-recipes.mjs renders it again and fails when median d or the moving share move more than 15 %, seamLocal leaves the cover TARGET, the file passes 2 MiB or the render exits non-zero. Measured 2026-10-20 on headless Chrome's software rung with CPU raster and libwebp 1.6.0.",
4
+ "row": {
5
+ "medianD": 0.002112,
6
+ "movingFrac": 1.0,
7
+ "longestStillSec": 0.0,
8
+ "spike": 1.662,
9
+ "seamLocal": 1.903,
10
+ "bytes": 2059158
11
+ },
12
+ "settings": {
13
+ "size": [
14
+ 1000,
15
+ 500
16
+ ],
17
+ "fps": 16,
18
+ "frames": 128,
19
+ "encode": {
20
+ "q": 83,
21
+ "edgeQ": 97,
22
+ "edgeFrames": 18,
23
+ "method": 4
24
+ }
25
+ },
26
+ "base": "test/fixtures/cover/painted-light-five-panel.png in this repository (1536x1024, the five-panel family painting), the painting the PAINTING block was measured on; another painting needs its own block",
27
+ "assets": [
28
+ {
29
+ "path": "cover/assets/base.png",
30
+ "role": "base-art",
31
+ "source": "user",
32
+ "box": [
33
+ 0.125,
34
+ 0,
35
+ 0.75,
36
+ 1
37
+ ],
38
+ "checkBox": [
39
+ 0.131,
40
+ 0.1445,
41
+ 0.1416,
42
+ 0.778
43
+ ],
44
+ "title": true
45
+ }
46
+ ],
47
+ "treatment": {
48
+ "mode": "cover",
49
+ "request": "Make the LitFamily README motion cover from the painted robot line-up.",
50
+ "idea": "A window of work light walks the sleeping bays while the lead robot breathes and a comet laps its frame.",
51
+ "recipe": "painted-light",
52
+ "recipeReason": "The painting already carries the brand, so the loop relights it instead of drawing over it.",
53
+ "brandWord": "LITFAMILY",
54
+ "aspect": "2:1",
55
+ "loopSec": 8,
56
+ "fps": 16,
57
+ "layers": {
58
+ "slow": "A comet laps the lead frame and motes rise one screen per loop.",
59
+ "mid": "A sharp-edged light window walks the bays once per loop, with scan bands at two passes.",
60
+ "beat": "Eyes breathe twice, one blink, and the LIT. flare pulses at t = 0 and 0.5."
61
+ },
62
+ "system": {
63
+ "rule": "One light window and one scan band rule.",
64
+ "copies": "Seven bays and fifty motes.",
65
+ "offset": "Each bay's phase comes from its x position."
66
+ },
67
+ "title": {
68
+ "box": [
69
+ 0.347,
70
+ 0.031,
71
+ 0.31,
72
+ 0.086
73
+ ],
74
+ "kind": "painted"
75
+ },
76
+ "encode": {
77
+ "q": 83,
78
+ "edgeQ": 97,
79
+ "edgeFrames": 18
80
+ },
81
+ "ambition": "Light reads as moving edges that survive the encoder, and the seam disappears under the flare."
82
+ },
83
+ "regressions": {
84
+ "feather-150": {
85
+ "patch": {
86
+ "find": "half: 230, edge: 24,",
87
+ "replace": "half: 230, edge: 150,"
88
+ },
89
+ "mustFail": [
90
+ "G1.target"
91
+ ],
92
+ "defect": "A 150 px soft light edge: the window reads as a soft wash instead of a moving edge. At this recipe's encode (q83, edge q97 for 18 frames) the encoder keeps the soft edge, so no motion gate sees it; it measured 2.59 MB against 2.06 MB with median d and the seam inside TARGET. What it reliably trips is the 2 MiB size TARGET. The wash itself is caught by look review on the sheet, not by a gate."
93
+ }
94
+ }
95
+ }