@litfamily/lithermes 1.0.26 → 1.0.28

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 (96) hide show
  1. package/README.md +10 -9
  2. package/README_Ko-KR.md +10 -9
  3. package/assets/lithermes-plugin/README.md +28 -9
  4. package/assets/lithermes-plugin/__init__.py +8 -3
  5. package/assets/lithermes-plugin/auto_update.py +17 -3
  6. package/assets/lithermes-plugin/core.py +41 -2
  7. package/assets/lithermes-plugin/core_contexts.py +27 -9
  8. package/assets/lithermes-plugin/core_contract.py +7 -2
  9. package/assets/lithermes-plugin/core_review.py +144 -3
  10. package/assets/lithermes-plugin/core_routing.py +10 -0
  11. package/assets/lithermes-plugin/core_workflow_commands.py +3 -2
  12. package/assets/lithermes-plugin/deliverable_hedge_guard.py +171 -48
  13. package/assets/lithermes-plugin/diagnostics.py +5 -3
  14. package/assets/lithermes-plugin/humanizer_detector.py +117 -5
  15. package/assets/lithermes-plugin/lit_reentry.py +233 -0
  16. package/assets/lithermes-plugin/payload-version.json +184 -52
  17. package/assets/lithermes-plugin/plugin.yaml +1 -1
  18. package/assets/lithermes-plugin/rules/__init__.py +2 -0
  19. package/assets/lithermes-plugin/rules/bundled-rules/lit-humanizer.md +2 -0
  20. package/assets/lithermes-plugin/rules/engine.py +26 -11
  21. package/assets/lithermes-plugin/skills/deep-interview/SKILL.md +2 -1
  22. package/assets/lithermes-plugin/skills/frontend-ui-ux/references/complete-contract.md +3 -3
  23. package/assets/lithermes-plugin/skills/frontend-ui-ux/scripts/hostexec.mjs +118 -0
  24. package/assets/lithermes-plugin/skills/frontend-ui-ux/scripts/probe.mjs +6 -5
  25. package/assets/lithermes-plugin/skills/lit-brainstorm/SKILL.md +408 -0
  26. package/assets/lithermes-plugin/skills/lit-diagram-drawer/scripts/doctor.mjs +3 -3
  27. package/assets/lithermes-plugin/skills/lit-diagram-drawer/scripts/export.mjs +5 -5
  28. package/assets/lithermes-plugin/skills/lit-diagram-drawer/scripts/hostexec.mjs +118 -0
  29. package/assets/lithermes-plugin/skills/lit-docx/scripts/convert_md_to_pdf.py +38 -4
  30. package/assets/lithermes-plugin/skills/lit-docx/templates/latex/acs/template.tex +3 -0
  31. package/assets/lithermes-plugin/skills/lit-docx/templates/latex/elsevier/template.tex +3 -0
  32. package/assets/lithermes-plugin/skills/lit-docx/templates/latex/ieee/template.tex +3 -0
  33. package/assets/lithermes-plugin/skills/lit-docx/templates/latex/korean-generic/template.tex +3 -0
  34. package/assets/lithermes-plugin/skills/lit-docx/templates/latex/nature/template.tex +3 -0
  35. package/assets/lithermes-plugin/skills/lit-humanizer/SKILL.md +2 -0
  36. package/assets/lithermes-plugin/skills/lit-pptx/bin/hostexec.mjs +118 -0
  37. package/assets/lithermes-plugin/skills/lit-pptx/bin/office.mjs +16 -9
  38. package/assets/lithermes-plugin/skills/lit-typographic-motion/SKILL.md +13 -12
  39. package/assets/lithermes-plugin/skills/lit-typographic-motion/bin/motion.mjs +19 -6
  40. package/assets/lithermes-plugin/skills/lit-typographic-motion/bin/runtime.mjs +25 -19
  41. package/assets/lithermes-plugin/skills/lit-typographic-motion/engine/THIRD_PARTY_NOTICES +2 -0
  42. package/assets/lithermes-plugin/skills/lit-typographic-motion/engine/browser.mjs +11 -4
  43. package/assets/lithermes-plugin/skills/lit-typographic-motion/engine/constants.mjs +29 -0
  44. package/assets/lithermes-plugin/skills/lit-typographic-motion/engine/cover-assets.mjs +87 -0
  45. package/assets/lithermes-plugin/skills/lit-typographic-motion/engine/cover-capture.mjs +189 -0
  46. package/assets/lithermes-plugin/skills/lit-typographic-motion/engine/cover-gate.mjs +263 -0
  47. package/assets/lithermes-plugin/skills/lit-typographic-motion/engine/cover-image.mjs +103 -0
  48. package/assets/lithermes-plugin/skills/lit-typographic-motion/engine/cover-init.js +36 -0
  49. package/assets/lithermes-plugin/skills/lit-typographic-motion/engine/cover-png.mjs +77 -0
  50. package/assets/lithermes-plugin/skills/lit-typographic-motion/engine/cover-sheet.mjs +70 -0
  51. package/assets/lithermes-plugin/skills/lit-typographic-motion/engine/cover-treatment.mjs +104 -0
  52. package/assets/lithermes-plugin/skills/lit-typographic-motion/engine/cover-webp.mjs +112 -0
  53. package/assets/lithermes-plugin/skills/lit-typographic-motion/engine/cover-worker.mjs +19 -0
  54. package/assets/lithermes-plugin/skills/lit-typographic-motion/engine/cover.mjs +338 -0
  55. package/assets/lithermes-plugin/skills/lit-typographic-motion/engine/encode.mjs +1 -1
  56. package/assets/lithermes-plugin/skills/lit-typographic-motion/engine/hostexec.mjs +118 -0
  57. package/assets/lithermes-plugin/skills/lit-typographic-motion/engine/look.mjs +103 -7
  58. package/assets/lithermes-plugin/skills/lit-typographic-motion/engine/loop-kit.js +80 -0
  59. package/assets/lithermes-plugin/skills/lit-typographic-motion/engine/sound.mjs +1 -1
  60. package/assets/lithermes-plugin/skills/lit-typographic-motion/engine/stage.mjs +8 -2
  61. package/assets/lithermes-plugin/skills/lit-typographic-motion/engine/treatment.mjs +1 -0
  62. package/assets/lithermes-plugin/skills/lit-typographic-motion/recipes/negatives/card-scenes/expected.json +49 -0
  63. package/assets/lithermes-plugin/skills/lit-typographic-motion/recipes/negatives/card-scenes/index.html +37 -0
  64. package/assets/lithermes-plugin/skills/lit-typographic-motion/recipes/negatives/reset-sweep/expected.json +49 -0
  65. package/assets/lithermes-plugin/skills/lit-typographic-motion/recipes/negatives/reset-sweep/index.html +27 -0
  66. package/assets/lithermes-plugin/skills/lit-typographic-motion/recipes/negatives/still-glow/expected.json +51 -0
  67. package/assets/lithermes-plugin/skills/lit-typographic-motion/recipes/negatives/still-glow/index.html +29 -0
  68. package/assets/lithermes-plugin/skills/lit-typographic-motion/recipes/negatives/title-slam/expected.json +51 -0
  69. package/assets/lithermes-plugin/skills/lit-typographic-motion/recipes/negatives/title-slam/index.html +37 -0
  70. package/assets/lithermes-plugin/skills/lit-typographic-motion/recipes/orbit-loop/expected.json +68 -0
  71. package/assets/lithermes-plugin/skills/lit-typographic-motion/recipes/orbit-loop/index.html +112 -0
  72. package/assets/lithermes-plugin/skills/lit-typographic-motion/recipes/painted-light/expected.json +96 -0
  73. package/assets/lithermes-plugin/skills/lit-typographic-motion/recipes/painted-light/index.html +208 -0
  74. package/assets/lithermes-plugin/skills/lit-typographic-motion/recipes/weight-tide/expected.json +68 -0
  75. package/assets/lithermes-plugin/skills/lit-typographic-motion/recipes/weight-tide/index.html +81 -0
  76. package/assets/lithermes-plugin/skills/lit-typographic-motion/references/cover.md +458 -0
  77. package/assets/lithermes-plugin/skills/lit-typographic-motion/references/craft-loop.md +4 -1
  78. package/assets/lithermes-plugin/skills/lit-typographic-motion/references/runtime.md +13 -5
  79. package/assets/lithermes-plugin/skills/lit-typographic-motion/references/stage.md +4 -2
  80. package/assets/lithermes-plugin/skills/lit-typographic-motion/references/treatment.md +6 -0
  81. package/assets/lithermes-plugin/skills/lit-typographic-motion/runtime/fonts.json +11 -0
  82. package/assets/lithermes-plugin/skills/lit-typographic-motion/runtime/requirements.lock +72 -0
  83. package/assets/lithermes-plugin/skills/litgoal/SKILL.md +19 -1
  84. package/assets/lithermes-plugin/skills/lsp-setup/scripts/verify-lsp.ts +16 -3
  85. package/assets/lithermes-plugin/skills/review-work/SKILL.md +38 -16
  86. package/assets/lithermes-plugin/skills/visual-qa/references/complete-contract.md +3 -3
  87. package/package.json +2 -1
  88. package/readme-assets/badge-version.svg +1 -1
  89. package/readme-assets/cover-motion-still.webp +0 -0
  90. package/readme-assets/cover-motion.webp +0 -0
  91. package/src/cli.js +2 -2
  92. package/src/lib/check.js +9 -6
  93. package/src/lib/hermesDiscovery.js +2 -2
  94. package/src/lib/hostExec.js +81 -0
  95. package/src/lib/skillPayload.js +1 -1
  96. package/src/lib/updateNotifier.js +14 -4
@@ -0,0 +1,68 @@
1
+ {
2
+ "schema": "lithermes.cover-recipe/v1",
3
+ "note": "The decoded gate row this recipe reached when it shipped, through this renderer (qa/cover-recipes.js re-renders it and fails when median d or the moving share leave +-15 % of the row, seamLocal leaves TARGET or the file passes 2 MiB).",
4
+ "row": {
5
+ "medianD": 0.009697857224064235,
6
+ "movingFrac": 1,
7
+ "longestStillSec": 0,
8
+ "spike": 1.1079454306412737,
9
+ "seamLocal": 1.0898095284450715,
10
+ "bytes": 1621942
11
+ },
12
+ "settings": {
13
+ "size": [
14
+ 1000,
15
+ 500
16
+ ],
17
+ "fps": 16,
18
+ "frames": 128,
19
+ "encode": {
20
+ "q": 58,
21
+ "edgeQ": 80,
22
+ "edgeFrames": 15,
23
+ "method": 4
24
+ }
25
+ },
26
+ "treatment": {
27
+ "mode": "cover",
28
+ "request": "An animated README cover for LitHermes where the name itself moves.",
29
+ "idea": "A weight swell rolls diagonally through rows of drifting letters and through the still hero name.",
30
+ "recipe": "weight-tide",
31
+ "recipeReason": "The name is the material: its weight moves and its position never does.",
32
+ "brandWord": "LITHERMES",
33
+ "motto": {
34
+ "text": "KEEP THE WORK LIT.",
35
+ "source": "project",
36
+ "from": "the motto painted on the repository artwork"
37
+ },
38
+ "aspect": "2:1",
39
+ "loopSec": 8,
40
+ "fps": 16,
41
+ "layers": {
42
+ "slow": "Rows of the word drift one word length per loop, alternate rows the other way.",
43
+ "mid": "The weight tide sweeps the screen once per loop.",
44
+ "beat": "A cursor steps under each hero letter and runs home on the last stop."
45
+ },
46
+ "system": {
47
+ "rule": "Weight is a gained swell of the loop phase.",
48
+ "copies": "About six hundred background letters and the nine hero letters.",
49
+ "offset": "Each letter's screen position, 0.85 x plus 0.35 y."
50
+ },
51
+ "title": {
52
+ "box": [
53
+ 0.11,
54
+ 0.31,
55
+ 0.78,
56
+ 0.32
57
+ ],
58
+ "kind": "live"
59
+ },
60
+ "encode": {
61
+ "q": 58,
62
+ "edgeQ": 80,
63
+ "edgeFrames": 15
64
+ },
65
+ "ambition": "Every letter on screen moves and the hero never thins below a readable weight."
66
+ },
67
+ "verdict": "PASS+TARGET"
68
+ }
@@ -0,0 +1,81 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <!-- Cover recipe: weight tide (type as material). Copy to DIR/cover/index.html and replace WORD
6
+ and the motto. One rule: a letter's weight is a swell of the loop phase, offset by where the
7
+ letter sits on screen. Rows of the word drift exactly one word per loop (alternate rows the
8
+ other way), so the letters travel through the tide; the hero word never moves and the tide
9
+ passes through it. A cursor steps under the hero letters on the beat. -->
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: #080d14; font-family: "JetBrains Mono"; }
14
+ .row { position: absolute; left: 0; white-space: pre; font-size: 56px; line-height: 1; color: #1b2531; }
15
+ .row span, #hero span { display: inline-block; }
16
+ #band { position: absolute; left: 0; right: 0; top: 180px; height: 252px; background: #080d14; border-top: 2px solid #1f2a37; border-bottom: 2px solid #1f2a37; }
17
+ #hero { position: absolute; left: 0; right: 0; top: 196px; text-align: center; font-size: 168px; line-height: 1; color: #f2efdf; white-space: pre; }
18
+ #cursor { position: absolute; top: 364px; height: 9px; background: #ff6337; }
19
+ #motto { position: absolute; left: 0; right: 0; top: 396px; text-align: center; font-size: 19px; letter-spacing: 9px; font-weight: 500; color: #8b8f8f; }
20
+ #motto b { color: #ff6337; font-weight: 700; }
21
+ body.mask, body.mask * { color: #000 !important; background: #000 !important; border-color: #000 !important; }
22
+ body.mask #hero, body.mask #hero span { color: #fff !important; background: transparent !important; }
23
+ </style>
24
+ </head>
25
+ <body>
26
+ <div id="rows"></div>
27
+ <div id="band"></div>
28
+ <div id="hero"></div>
29
+ <div id="cursor"></div>
30
+ <div id="motto">KEEP THE WORK <b>LIT.</b></div>
31
+ <script>
32
+ const WORD = 'LITHERMES';
33
+ const { clamp, fract, mix, ease, swell, makeNoise, loopNoise, cubicPulse } = LitLoop;
34
+ const SIZE = 56, ADVANCE = 0.6 * SIZE, PITCH = 64, HERO = 168, HERO_ADVANCE = 0.6 * HERO;
35
+ const noise = makeNoise(7);
36
+ // The offset field: a diagonal sweep plus a per-row step, so the front is never a ruler line.
37
+ const offset = (x, y, row) => 0.85 * (x / 1200) + 0.35 * (y / 600) + 0.06 * row;
38
+ // gain() makes the weight dwell at light and at bold and turn quickly between them.
39
+ const tide = (t, off) => ease.gain(swell(fract(t - off)), 2.4);
40
+
41
+ const rows = Array.from({ length: 10 }, (_, r) => {
42
+ const el = document.createElement('div');
43
+ el.className = 'row';
44
+ el.style.top = `${r * PITCH - 18}px`;
45
+ const spans = [...`${WORD}·`.repeat(6)].map((ch) => { const s = document.createElement('span'); s.textContent = ch; el.appendChild(s); return s; });
46
+ document.getElementById('rows').appendChild(el);
47
+ return { el, spans, y: r * PITCH + 10, dir: r % 2 ? 1 : -1, r };
48
+ });
49
+ const hero = [...WORD].map((ch) => { const s = document.createElement('span'); s.textContent = ch; document.getElementById('hero').appendChild(s); return s; });
50
+ const heroLeft = (1200 - HERO_ADVANCE * WORD.length) / 2;
51
+ const cursor = document.getElementById('cursor');
52
+
53
+ window.renderFrame = (t, options = {}) => {
54
+ document.body.className = options.mask ? 'mask' : '';
55
+ const span = (WORD.length + 1) * ADVANCE;
56
+ for (const row of rows) {
57
+ // Exactly one word of travel per loop keeps the drift seamless; loop noise breathes each row.
58
+ const shift = row.dir * span * t - 1.5 * span + 6 * loopNoise(noise, t, 3.1 * row.r, 0.9);
59
+ row.el.style.transform = `translateX(${shift}px)`;
60
+ const wobble = 0.05 * loopNoise(noise, t, 40 + row.r, 1.1);
61
+ row.spans.forEach((s, i) => {
62
+ const k = tide(t, offset(shift + (i + 0.5) * ADVANCE, row.y, row.r) + wobble);
63
+ s.style.fontVariationSettings = `"wght" ${Math.round(mix(100, 800, k))}`;
64
+ s.style.color = `rgb(${Math.round(mix(22, 46, k))},${Math.round(mix(31, 58, k))},${Math.round(mix(42, 74, k))})`;
65
+ });
66
+ }
67
+ // The hero takes the same tide over a narrower range, so it never thins below a readable weight.
68
+ hero.forEach((s, i) => { s.style.fontVariationSettings = `"wght" ${Math.round(mix(330, 800, tide(t, offset(heroLeft + (i + 0.5) * HERO_ADVANCE, 300, 4.5))))}`; });
69
+ // Beat: the cursor dwells under each letter, steps on with an overshoot, and on its last stop
70
+ // runs home like a carriage return.
71
+ const stops = WORD.length + 1, x = t * stops, i = Math.floor(x), f = x - i;
72
+ const at = i < stops - 1 ? i + ease.outBack(clamp((f - 0.62) / 0.38), 1.4) : WORD.length * (1 - ease.inCubic(clamp(f / 0.45)));
73
+ cursor.style.left = `${heroLeft + Math.min(at, WORD.length) * HERO_ADVANCE + 6}px`;
74
+ cursor.style.width = `${HERO_ADVANCE - 12}px`;
75
+ cursor.style.opacity = String(1 - 0.75 * (i === stops - 1 ? 0 : cubicPulse(0.3, 0.12, f)));
76
+ };
77
+ Promise.all(['100 56px "JetBrains Mono"', '800 168px "JetBrains Mono"', '700 19px "JetBrains Mono"'].map((f) => document.fonts.load(f)))
78
+ .then(() => document.fonts.ready).then(() => { window.renderFrame(0); window.ready = true; });
79
+ </script>
80
+ </body>
81
+ </html>
@@ -0,0 +1,458 @@
1
+ # Cover mode: README cover loops
2
+
3
+ Load this file instead of `treatment.md` when the request is a README cover, a
4
+ hero or banner loop, an animated project cover, or any short image that plays
5
+ forever at the top of a page. A cover is not a film. It has no beats, no arc,
6
+ no ending, no sound and no copy lines: it is one picture that never stops
7
+ moving and never shows where it starts. Every other request stays on the film
8
+ path (`treatment.md`, then `stage.md` or the type references), unchanged.
9
+
10
+ The film loop rewards the wrong things for a cover. Beats of at least 1.2 s, a
11
+ new composition per beat, stills at beat midpoints, a settled end card and a
12
+ reading floor all push a loop toward a slide deck. Cover mode replaces them
13
+ with a loop contract, a review that watches motion instead of stills, and a
14
+ gate that measures the encoded file a reader's browser actually decodes.
15
+
16
+ ## What changes from a film
17
+
18
+ | Film rule | Cover mode |
19
+ | --- | --- |
20
+ | Beats of at least 1.2 s, each showing something new | No beats. Three speed layers and one system, each continuous over the loop |
21
+ | A settled end card, holds as a reward | No ending. A hold longer than 0.5 s is a defect (G3) |
22
+ | 10 s minimum, never shorten | 6-12 s; 8 s worked for every recipe |
23
+ | 16:9 or 9:16 | 2:1 (page 1200x600, cover 1000x500) or 3:2 (page 1200x800, cover 960x640) |
24
+ | A generated sound bed and its gates | Silent: `sound` is absent or `none` |
25
+ | An invented subject and invented copy | The brand word, plus at most one motto the user wrote or the project already uses |
26
+ | A title fade-in, a stage that fades up from black | Frame 0 is the poster and already the whole picture |
27
+ | A stills round, then the film | Every round is a full cover render; a defect is checked first with `--window` |
28
+ | Look questions about purpose and beats | Look questions about motion, the seam and damage (below) |
29
+
30
+ ## Commands
31
+
32
+ ```text
33
+ $M cover --out DIR --round N render, encode and gate one round (exit 13 until TARGET)
34
+ $M cover --out DIR --round N --window t0,t1 re-render only t0-0.5 s .. t1+0.5 s and sheet it
35
+ $M cover --out DIR --round N --detach the same render in a detached child; poll DIR/run-state.json
36
+ $M cover-gate FILE [--frames D --frames2 D --masks D --title x,y,w,h] [--strict] [--json]
37
+ $M look --out DIR --round N --answers FILE record a look round on the cover stills
38
+ $M complete --out DIR exit 0 only when the cover is done
39
+ ```
40
+
41
+ Give a render call a timeout of at least 600 s (a painted cover took about
42
+ two minutes on the software rung), or use `--detach` and poll
43
+ `DIR/run-state.json` until `finished` is true.
44
+
45
+ ## The page
46
+
47
+ Write `DIR/cover/index.html`, with every local asset beside it under
48
+ `DIR/cover/`. The renderer serves only that folder, the loop kit at
49
+ `/lit/loop-kit.js` and the faces at `/lit/fonts.css`, from the same offline
50
+ origin as the stage path. The stage path's static scan applies: no network URL,
51
+ no `<video>`, `<audio>`, `<iframe>`, no workers, at most 24 rasters and 8 MB,
52
+ no animated raster and no flipbook.
53
+
54
+ - Lay the page out at the aspect's page size in CSS pixels (2:1: 1200x600;
55
+ 3:2: 1200x800). The renderer captures it at device scale 2 and brings every
56
+ capture down to the cover size with Lanczos, so thin lines and small type are
57
+ drawn at twice the resolution they are shown at.
58
+ - Define `window.renderFrame(t, { mask })` and set `window.ready = true` once
59
+ fonts and images are decoded. `t` is the loop time in [0, 1): frame `i` of `N`
60
+ is `t = i / N`, so `t = 1` is never drawn (it would repeat frame 0 and stutter
61
+ at the seam). `renderFrame` may return a promise.
62
+ - Everything on screen is a function of `t`. The renderer seeks frames in any
63
+ order, repeats every frame in a fresh browser and re-draws sixteen of them in
64
+ a shuffled order in another one, so a frame may never depend on the frame
65
+ drawn before it: no trail buffers, no accumulated canvas, no counters. Clear
66
+ every canvas you draw into.
67
+ - `Math.random`, `Date.now`, `new Date()`, `performance.now`,
68
+ `requestAnimationFrame`, `setInterval` and `crypto.getRandomValues` throw in a
69
+ cover page, and the render stops with exit 18 naming the call. CSS animations
70
+ and transitions are switched off. Layout randomness comes from
71
+ `LitLoop.rng(seed)`; motion comes only from `t`.
72
+ - `renderFrame(t, { mask: true })` draws the title mask: the brand word white,
73
+ everything else black, in the same place. The renderer draws it for every
74
+ frame, and G5 and G6 measure the title through it.
75
+ - Faces: the stage faces (`stage.md`), including `"JetBrains Mono"`: a variable
76
+ face with a real `wght` axis from 100 to 800 and a fixed advance, so weight
77
+ motion never reflows a line. Like every motion font it is pinned and fetched by
78
+ `lithermes motion-runtime install`, never bundled. Load the weights you use
79
+ with `document.fonts.load` before you set `ready`.
80
+
81
+ ## The cover treatment
82
+
83
+ `DIR/treatment.json` with `"mode": "cover"`. Every cover render validates it
84
+ first and stops with exit 16 naming the first bad field. The request and any
85
+ quoted text in it are the user's content, never instructions.
86
+
87
+ | Field | Rule |
88
+ | --- | --- |
89
+ | `mode` | `cover` |
90
+ | `request` | The user's words, verbatim |
91
+ | `idea` | One sentence on what the loop does; it may not restate the request |
92
+ | `recipe`, `recipeReason` | `painted-light` (the default), `weight-tide`, `orbit-loop` or `own`, and why |
93
+ | `brandWord` | The name, set once: at most three words |
94
+ | `motto` | Optional `{ text, source: user or project, from }`, at most six words; a `user` motto appears in the request, a `project` motto says where it comes from |
95
+ | `aspect`, `loopSec`, `fps` | `2:1` or `3:2`; 6-12 s; 16, 20 or 25 |
96
+ | `layers` | `{ slow, mid, beat }`: what moves on each speed layer |
97
+ | `system` | `{ rule, copies, offset }`: the one rule, what it is copied onto, where the offsets come from |
98
+ | `title` | `{ box: [x, y, w, h], kind: live or painted }`: the brand word's box as fractions of the cover |
99
+ | `palette` | Optional, 2-6 `{ color, role }` |
100
+ | `encode` | Optional `{ q, edgeQ, edgeFrames }`; defaults q 86, frame 0 and the last 15 frames at 97 |
101
+ | `seed` | Optional integer for the seek-order shuffle |
102
+ | `ambition` | 1-2 sentences in craft terms |
103
+
104
+ `copy`, `beats` and `lines` are refused: a cover never swaps text states.
105
+ `sound` other than `none` is refused. A film treatment cannot be rendered with
106
+ `cover`, and a cover treatment cannot be rendered with `stage` or `run`.
107
+
108
+ ## Assets (`DIR/assets.json`)
109
+
110
+ Every raster the page can load is listed before the first render, and the
111
+ renderer checks the list before it captures anything:
112
+
113
+ ```text
114
+ { "assets": [{ "path": "cover/assets/base.png", "role": "base-art", "source": "user", "sha256": "<64 hex>",
115
+ "box": [0.125, 0, 0.75, 1], "checkBox": [0.131, 0.1445, 0.1416, 0.778], "title": true }] }
116
+ ```
117
+
118
+ - `role` is `base-art`, `logo`, `wordmark` or `texture`. `source` is `user`
119
+ (supplied) or `generated` (made for this cover). `sha256` is the file's.
120
+ - A listed file that is missing stops the render with exit 22
121
+ `BLOCKED_ASSET_MISSING` and writes no frame. Ask for the art or generate it;
122
+ never draw a stand-in in its place. `painted-light` needs a `base-art` entry.
123
+ A raster in `cover/` that the list does not name, or a listed file whose hash
124
+ no longer matches, is exit 17.
125
+ - `box` is where the base art sits on the cover. After the render, decoded
126
+ frame 0 must show the art: the SSIM of `checkBox` (default: the whole box)
127
+ against the same region of the source scaled into the box reaches 0.90 (the
128
+ ART line). A page that relights a painting dims most of it at frame 0 on
129
+ purpose, so point `checkBox` at a part it leaves lit; it must cover at least
130
+ 10 % of the box. On the repository painting the lit lead panel measured 0.96.
131
+ - `title: true` declares that the brand word is painted into the base art. Only
132
+ then is the painted title exempt from the cap-height floor (G6); it is still
133
+ measured and reported as INFO.
134
+ - The art is scaled into its box, never cropped. A generated asset is labelled
135
+ as generated in the reply (`complete` prints the label).
136
+
137
+ ## Craft rules, ranked
138
+
139
+ 1. **The encoder is part of the design.** Lossy animated WebP treats a pixel as
140
+ unchanged when it moves less than about `31(1 - sqrt(q/100)) + sqrt(q/100)`
141
+ levels from the previous frame: about 6 levels at q72 and 3 at q90. Slow
142
+ fades and slow breathing below that are dropped, go stale, and snap back at
143
+ the next keyframe, which by default is the loop point. This is invisible in
144
+ the PNG frames and visible in the decoded file.
145
+ 2. **Light is a moving edge, not an in-place fade.** A travelling window with a
146
+ 24-40 px edge survives the encoder; a 150 px soft edge did not at q72 (the
147
+ moving share fell from 0.92 to 0.83 and the seam jumped to 24 times its
148
+ neighbours on the same frames). At a high edge quality the soft edge
149
+ survives and reads as a wash instead: no motion gate sees that, so the look
150
+ review has to.
151
+ 3. **Something translates.** A weight tide alone lost most of its motion in the
152
+ encoder (median step 0.0007, seam 14x); one word of row drift per loop kept
153
+ all of it (0.0098, seam 1.1x).
154
+ 4. **One rule, many copies, offsets from position.** One swell over hundreds of
155
+ letters, one orbit rule on three rings, one light window over the panels.
156
+ Offsets come from x, y, angle or a loop-noise field; no timeline, no scenes.
157
+ 5. **Three speed layers.** Slow, perpetual and linear (drift, laps, rising
158
+ motes); a mid swell (the tide, the light window); micro events on a beat (a
159
+ cursor step, a gate flash, a blink, a flare). Every cover that reached TARGET
160
+ had all three.
161
+ 6. **The brand word is set once and never leaves.** It is the material (its
162
+ weight moves, its position does not), or it stands still beside the system,
163
+ or it is painted into the base art and lifted.
164
+ 7. **Frame 0 is the poster.** Reduced-motion readers, thumbnails and slow
165
+ networks show it alone, so it is the whole picture: no fade from black.
166
+ 8. **Bands are flat steps, never smooth gradients, and every step clears the
167
+ threshold.** A 3 px leading line with a two-step afterglow (0.10, 0.05)
168
+ passed; a third 0.03 step drifted and pushed the seam from 1.71 to 2.48. A
169
+ single flat slab encodes well but reads as a box.
170
+ 9. **Exponential tails are slow fades.** `expImpulse` decays below the
171
+ threshold for half the loop. Use the symmetric `cubicPulse` for micro
172
+ events, and put one across `t = 0` so the keyframe reset lands under motion.
173
+ 10. **Small movers over texture are expensive.** Each mote over a painting
174
+ makes the encoder re-send the textured blocks around it; keep motes few and
175
+ mostly on the plain wings.
176
+ 11. **Lift a painted title instead of laying live text over it.** A
177
+ luminance-keyed gain (only pixels above the plate's level) raises dark
178
+ painted letters to 4.5:1 and keeps the plate dark; thresholding the same
179
+ letters gives the mask. Live text set at a painting's scale came out 11 px
180
+ tall at README width.
181
+ 12. **Spend quality at the seam, not on keyframes.** With every edge sharp, what
182
+ remains at the loop point is requantisation: frame 0 and the frames before
183
+ it carry different error. Raise only those (`edgeQ`, `edgeFrames`) and keep
184
+ the default keyframes; periodic keyframes spread the snap into visible pops,
185
+ and a small key interval made a 2.5-5 Hz shimmer.
186
+ 13. **16 fps for slow painted covers, 25 for a system with fast dots.** At
187
+ 16 fps img2webp writes 63 ms frames, so an 8 s loop is 8.064 s; that is fine.
188
+
189
+ ## Recipes
190
+
191
+ Each recipe is a worked page in `recipes/<name>/index.html` with the treatment
192
+ it shipped with and its decoded gate row in `recipes/<name>/expected.json`.
193
+ Copy the page to `DIR/cover/index.html` and replace the brand word, the motto
194
+ and, for the painted recipe, the `PAINTING` block. What transfers is the rules
195
+ above, not the pixels.
196
+
197
+ - **Painted base + travelling light (`painted-light`, the default).** A finished
198
+ painting (supplied, or generated and labelled) is the whole picture; the page
199
+ relights it. Macro: a window of work light with a 24 px feather walks the dim
200
+ panels once per loop. Mid: a scan band per panel at two passes per loop,
201
+ offset by the panel's position; one fast skewed glint across the title in
202
+ **overlay** blend (screen lit a visible box, colour-dodge turned a coloured
203
+ letter into a red block); a comet lapping the lead panel's frame. Beat: eyes
204
+ breathe twice and blink once, the core breathes once, the accent word flares
205
+ at `t = 0` and `t = 0.5`. A denser, brighter painting needs a deeper dim pool
206
+ and a lower `q` (the repository painting reached TARGET at dim 0.68, q 83,
207
+ edge q 97 over 18 frames).
208
+
209
+ **Choosing the lead.** Any panel can be the lead, so a shared line-up can feature each
210
+ product's own machine. Three things move together:
211
+ 1. `PAINTING.lead` is the panel index (0 is the leftmost). The dim pool is clipped around that
212
+ panel, so a lead in the middle stays at source brightness while the light window still walks
213
+ every other panel as one continuous light. The comet laps the lead's frame and its scan band
214
+ stays at full strength.
215
+ 2. `PAINTING.lamps` maps a panel index to the lamp anchors measured on that machine
216
+ (`{ x, y, rx, ry, eye }` in painting pixels; `eye: true` lamps breathe twice and blink,
217
+ `eye: false` is a core that breathes once). Only the lead's entry glows. A lead with no entry
218
+ stops the page with `PAINTING.lamps has no entry for the lead panel N` (the render ends as
219
+ `STAGE_CONTRACT_ERROR` after the 60 s ready wait), because borrowing another machine's
220
+ coordinates draws its eyes on the wrong body. Measure the anchors, or write `[]` for that
221
+ panel to run with no beat lights and say so in the treatment's beat layer.
222
+ 3. The base art's `checkBox` in `assets.json` points at the lead panel. With the shipped layout
223
+ (painting scaled by `S = 600 / 1024` and pillarboxed 150 px from the left of a 1200 x 600
224
+ page) a panel `[x0, x1]` with `top` and `bottom` is
225
+ `[(150 + x0*S) / 1200, top*S / 600, (x1 - x0)*S / 1200, (bottom - top)*S / 600]`. A box left
226
+ on another panel measures a dimmed panel and the ART line fails.
227
+
228
+ The accent flare and the title glint belong to the painted motto and title, not to the lead, and
229
+ stay where they are. One more thing moves with the lead: the light window's right edge sits at
230
+ `x = POOL.from - 330 + (POOL.to - POOL.from + 660) * 0.18 + 254` at `t = 0`. When the lead panel
231
+ covers that `x`, the neighbours of the seam go quiet, the seam reads high against them and G4
232
+ misses TARGET (measured 2.48 against 2.0 with the lead on panel 1 of 7). Move the `0.18` in
233
+ `windowAt` until that edge crosses visible panels at the seam; 0.30 did it there.
234
+ - **Weight tide (`weight-tide`).** Rows of the brand word in JetBrains Mono at
235
+ about 1.3:1 to the ground drift one word length per loop, alternate rows the
236
+ other way. Weight is `gain(swell(fract(t - offset)), 2.4)` with
237
+ `offset = 0.85 x/W + 0.35 y/H`; the hero word on a knockout band takes the
238
+ same field over 330-800 so it never thins. One accent: a cursor that dwells
239
+ under each letter, steps on with `outBack`, and runs home on the last stop.
240
+ - **Orbit loop (`orbit-loop`).** Three rings at 3, 2 and 1 laps per loop,
241
+ alternating directions, dots with fading arc trails. A gate at twelve o'clock
242
+ flashes on each crossing; the crossing times, computed rather than detected,
243
+ light the cells of a ledger under the word, as many crossings as cells so the
244
+ ledger loops. The core breathes once and rings on four bars. The word is still.
245
+ - **Own.** Any system that keeps the rules: one rule many copies, three layers,
246
+ a still word, edges instead of fades. Say which in `recipeReason`.
247
+
248
+ `recipes/negatives/` holds four slide-like pages that must keep failing their
249
+ declared FLOOR gates: a title slam with lines that appear and hold and an end card
250
+ (G2, G3), card scenes that fade in, hold and fade out (G2), a still with a faint
251
+ breathing glow (G2, G3), and a progress sweep that resets at the seam (G4). They
252
+ are never recipes. Read them to see what cover mode exists to prevent, and never
253
+ change a threshold so one of them passes.
254
+
255
+ `npm run qa:cover-recipes` in a LitHermes checkout re-renders every recipe
256
+ through this renderer and fails when median d or the moving share leaves
257
+ +-15 % of its row, seamLocal leaves TARGET or a file passes 2 MiB.
258
+
259
+ Every negative and regression declares the gates it must fail (`mustFail`, as
260
+ `G2.floor` or `G1.target`). `--negatives` runs them all, and one counts as
261
+ caught only when its render completed and gated the decoded file, every
262
+ declared gate failed at its tier, and no undeclared FLOOR gate among G1-G4
263
+ failed. A crash (exit 14 included), a missing declaration, a stale page patch
264
+ or a failure on another gate fails the QA with the reason.
265
+
266
+ The painted recipe declares one regression, `feather-150` (`--feather 150` runs
267
+ it alone): the light edge widened from 24 to 150 px. The defect it stands for
268
+ is light drawn as a soft wash instead of a travelling edge, but at this
269
+ recipe's encode (q83, edge q97 for 18 frames) the encoder keeps the soft edge
270
+ and the motion numbers stay where the recipe's are. What it reliably trips is
271
+ the 2 MiB size TARGET (G1), so `G1.target` is all it declares; seamLocal sits
272
+ inside run-to-run noise around its TARGET, so G4 is not declared. A decoded-file
273
+ edge measure (the share of each step's change carried by pixels that move at
274
+ least 8 levels) was tried and dropped: it separated this painting at 24 and
275
+ 150 px but scored lowest on a cover whose window edge was sharp and whose scan
276
+ bands were soft. It measures every soft change in the frame, not the travelling
277
+ edge, and any threshold would be fitted to one painting. The soft wash is
278
+ caught by look review on the sheet (question 6 under Review).
279
+
280
+ ## Loop kit (`/lit/loop-kit.js`)
281
+
282
+ `LitLoop` holds `rng(seed)` (mulberry32, layout only); `makeNoise(seed)` and
283
+ `loopNoise(noise, t, offset, radius)` (value noise walked on a circle, so it
284
+ closes at `t = 1`); `ease.smooth`, `ease.outCubic`, `ease.inCubic`,
285
+ `ease.outExpo`, `ease.outBack(p, s)`, `ease.gain(p, k)`; `cubicPulse(c, w, x)`
286
+ and `expImpulse(x, k)`; `phase(t, offset, cycles)`; `cdist(p, c)` (distance
287
+ around the loop, for an event that straddles `t = 0`); `window(p, start, dur)`
288
+ (a one-shot event, -1 outside); `steps(p, stops, travel, ease)` (hold and move);
289
+ `swell(p)` (a cosine swell that dwells at both ends); `TAU`, `clamp`, `fract`,
290
+ `mix`. Every helper is pure or periodic in `t`.
291
+
292
+ ## Encode
293
+
294
+ img2webp, lossy, loop forever, `-m 4`, every frame `-d` = round(1000 / fps) ms,
295
+ frame 0 and the last `edgeFrames` frames at `edgeQ`, default keyframes. When
296
+ img2webp or dwebp (the gate decodes with it) is missing, gifski is the fallback
297
+ at quality 90. Neither present is exit 21 `BLOCKED_NO_COVER_ENCODER`, checked
298
+ before any capture. A cover is never an MP4: README renderers do not autoplay
299
+ video.
300
+
301
+ ## Gate (the decoded file)
302
+
303
+ The gate decodes the encoded cover (each frame through dwebp, composited as a
304
+ browser does) and measures on a fixed time grid of `max(50 ms, shortest frame)`,
305
+ because the encoder merges identical frames into long ones and counting frames
306
+ would flatter a slide film. `d` is the mean |dY| (luma 0..1, about 300 px wide)
307
+ per grid step, and the loop is a ring, so the seam is one more step.
308
+
309
+ | Gate | Metric | FLOOR (fail) | TARGET (cover mode) |
310
+ | --- | --- | --- | --- |
311
+ | G0 determinism | SHA-256 per frame, two captures in fresh browsers | 100 % identical | same |
312
+ | G1 budget | bytes, duration, loop | <= 3 MiB, 6-12 s, loop 0 | <= 2 MiB |
313
+ | G2 motion energy | median d; share of moving steps | >= 0.0004; >= 0.45 | >= 0.0015; >= 0.95 |
314
+ | G3 anti-slide | longest still run; max d / median d | <= 2.0 s | <= 0.5 s, spike <= 6 |
315
+ | G4 seam | seam / mean of the two steps on each side | <= 8 | 0.25-2.0 |
316
+ | G5 title contrast | median title pixel vs 90th-percentile background in the box, every frame, through the mask | >= 4.5:1 | same |
317
+ | G6 title size | cap height of the title glyphs in the mask at the README column width (830 px) | >= 24 px | same; a title painted into declared base art is INFO |
318
+ | SEEK | 16 frames in a seeded shuffled order in a fresh page, against the master | identical, or within one level per channel (else exit 18 naming the frame and its time) | same |
319
+ | ART | SSIM of the base art's check box in decoded frame 0 against the source | >= 0.90 | same |
320
+ | FLASH | WCAG 2.3.1 flashes per second over the loop | <= 3 general, <= 3 red | same; a FAIL withholds the cover in `withheld/` |
321
+
322
+ FLOOR was set so that eight earlier painted covers pass and four slide-like
323
+ negatives fail. TARGET is what cover mode delivers; those earlier covers miss it
324
+ on purpose (a 1.9 s hold, a flash at the seam). The cover render exits 13 until
325
+ every gate reaches TARGET, the 2 MiB size included. G2 and G3 are area-weighted:
326
+ a small comet on a still picture counts as still at thumbnail scale, which is
327
+ intended. Never raise a number, shorten the loop or drop a layer to pass; fix
328
+ the cause.
329
+
330
+ G6 splits the white mask pixels in the title box into connected glyphs, drops
331
+ dots and accents (under 1 % of the largest glyph's area or 30 % of the tallest),
332
+ takes the median glyph height per sampled frame, and scales the smallest one
333
+ from the cover width to 830 px. Contrast alone once passed an 11 px name plate.
334
+ The seek-order check catches a page that keeps state between frames: the
335
+ sequential captures agree with each other, a frame sought out of order does not.
336
+ An element whose opacity and transform change together can rasterize one level
337
+ apart depending on what was painted before it, so a sought frame within one
338
+ level per channel of the master passes and is counted as `oneLevel`.
339
+
340
+ ## Review: the stills and the questions
341
+
342
+ Every cover render writes, from the DECODED file, into `DIR/stills/` (listed in
343
+ `stills/stills.json` like a film's set):
344
+
345
+ - `sheet.png`: twelve frames at `t = k/12`, each labelled with its time in
346
+ seconds, above a plot of every grid step's `d`, the still threshold (red
347
+ dashes) and the median (green dots). A hold is a flat run on the dashes,
348
+ encoder pops are regular spikes, a reset is a spike at the right end.
349
+ - `poster.png` (decoded frame 0) and `seam.png` (the last frame beside frame 0).
350
+ - `frame-00.png` ... `frame-11.png`, the sheet's frames at full size.
351
+
352
+ The gate cannot see damage. The pilot's two worst defects were found only by
353
+ eye on the sheet: a colour-dodge glint that turned a letter into a red block,
354
+ and a wing glow that read as a dark disc. View `sheet.png`, `poster.png` and
355
+ `seam.png` with `vision_analyze`, one call per file (and any `frame-NN.png` you
356
+ need closer). The plugin's tool hook records each call, and `complete` requires
357
+ a receipt for each of the three. A second opinion can come from a fresh
358
+ `delegate_task` subagent given only the sheet; record what it says as an aid.
359
+
360
+ Answer all eight every round, each naming a frame of the stills set and what is
361
+ visible there:
362
+
363
+ 1. Is something moving in every half-second window (the plot never lies flat on
364
+ the dashes)?
365
+ 2. Does the seam disappear in the decoded file (`seam.png`, the right end of the
366
+ plot)?
367
+ 3. Does it still move after encoding, with no region that freezes and then pops?
368
+ 4. Is the brand word present, legible and in place in every frame, never faded
369
+ or swapped?
370
+ 5. Are all three speed layers visible: a slow perpetual layer, a mid swell and
371
+ beat events?
372
+ 6. Does anything read as a slide, a card swap, a title fade or an end card, or
373
+ does light wash in place instead of travelling as an edge?
374
+ 7. Is any glyph, glint or glow damaged: a colour block, a dark disc, a visible
375
+ box?
376
+ 8. Name one thing a skilled motion designer would add or remove (`named`), or
377
+ `none`.
378
+
379
+ A "no" on 1-5, a "yes" on 6 or 7, or a named 8 asks for another round. Record
380
+ the round with `$M look --out DIR --round N --answers FILE`; the answers file has
381
+ the film's shape (`viewed`, `answers` with `q`, `verdict`, `frame`, `observed`,
382
+ `aids`), with no `by` or `stranger` on question 1, and round 1 names the
383
+ `change` you make before round 2. A round with no image tool is recorded with
384
+ `"blocked": "no-vision-tool"` and ends `DONE_UNVIEWED`.
385
+
386
+ ## Defects and window re-renders
387
+
388
+ What the eye finds goes into the round as `defects`, at most three, worst first:
389
+
390
+ ```text
391
+ "defects": [{ "t": 1.2, "region": [340, 10, 330, 50], "frame": "stills/frame-02.png",
392
+ "observed": "<what is visible there>", "cause": "<why>", "fix": "<the change>" }]
393
+ ```
394
+
395
+ `t` is the loop time in seconds where the defect shows, `region` the
396
+ `[x, y, w, h]` it covers in cover pixels, `frame` a file of the current stills
397
+ set that shows it. `look` refuses a defect without a time, outside the loop, off
398
+ the cover, on a frame outside the current set, or without a concrete cause and
399
+ fix; a film round may not carry defects.
400
+
401
+ Before the next full render, check each fix locally:
402
+ `$M cover --out DIR --round N --window t0,t1` captures only the frames from
403
+ `t0 - 0.5 s` to `t1 + 0.5 s` (wrapping at the seam) in a fresh page and writes
404
+ `windows/round-N/<t0>-<t1>/sheet.png` (the span shaded on its plot) and
405
+ `window.json`. It shows frames before encoding, writes nothing to `stills/`, the
406
+ report or the manifest, and never counts as done: the full render and its gate
407
+ decide.
408
+
409
+ ## Rounds and done
410
+
411
+ Rounds share one counter with the renders, 1 to 3, and every round is a full
412
+ cover render followed by a look. `$M complete --out DIR` exits 0 only when:
413
+
414
+ - `treatment.json` is a valid cover treatment and has not changed since the
415
+ last render;
416
+ - the last render exited 0 and its report reads `COVER gate --strict:
417
+ PASS+TARGET`;
418
+ - at least two look rounds are recorded, round 1 naming its change;
419
+ - the last round was recorded on the final render and viewed `stills/sheet.png`,
420
+ `stills/poster.png` and `stills/seam.png` (with `vision_analyze` receipts when
421
+ the tool hook is live);
422
+ - the last round asks for no further round, or it is round 3 (then the open
423
+ items are stated plainly in the reply).
424
+
425
+ `complete` prints one `label:` line per generated asset; the reply carries each.
426
+ A window render never counts.
427
+
428
+ ## Reply
429
+
430
+ Below the activation line, in plain words: what the loop does in one line; that
431
+ it was checked (motion on the decoded file, the seam, flash safety, and that the
432
+ frames were looked at, or that nobody could look at them); the file and its size;
433
+ a label on every generated asset; where the page lives and the one command that
434
+ re-renders it; any open look item.
435
+
436
+ ## Exit codes
437
+
438
+ | Code | State | Fix |
439
+ | --- | --- | --- |
440
+ | 13 | GATE_FAIL_QA | A gate misses TARGET or the flash audit failed; the report names the gates |
441
+ | 14, 15 | not pre-warmed, font missing | `lithermes motion-runtime install` outside the session |
442
+ | 16 | BLOCKED_TREATMENT_INVALID | Fix the named field of `treatment.json` or `assets.json` |
443
+ | 17 | STAGE_CONTRACT_ERROR | The page, its static scan or the asset list (an unlisted raster, a changed hash) |
444
+ | 18 | STAGE_NONDETERMINISTIC | A clock or entropy call, a frame that differs in a fresh browser, or a frame that differs when sought out of order |
445
+ | 19 | STAGE_NETWORK_REQUEST | Remove the URL; everything is local |
446
+ | 21 | BLOCKED_NO_COVER_ENCODER | Install libwebp (img2webp, dwebp) or gifski |
447
+ | 22 | BLOCKED_ASSET_MISSING | Supply or generate the listed asset; never a stand-in |
448
+
449
+ ## Unconfirmed
450
+
451
+ - Whether the single keyframe reset at the loop point is visible to a person on
452
+ a real README; the numbers say smooth, nobody has watched it there yet.
453
+ - How a cover reads inside GitHub's README column at full width; review so far
454
+ used local frames.
455
+ - The encoder threshold formula is libwebp's animation encoder as remembered;
456
+ the measured behaviour matches it (q72 drops slow fades, q86-90 drops fewer).
457
+ - The thresholds come from 8 positives and 4 negatives in one family's style;
458
+ re-check them on covers made for other projects.
@@ -1,7 +1,10 @@
1
1
  # Craft loop, look rounds and done
2
2
 
3
3
  Both paths share one loop: treatment, author, a stills round you look at, the
4
- film, a last look, then the done-check. A gate PASS alone is never done.
4
+ film, a last look, then the done-check. A gate PASS alone is never done. A cover
5
+ loop has its own eight questions, its own stills (the decoded sheet with its
6
+ frame-difference plot, the poster and the seam pair), defects with a window
7
+ re-render, and its own done; see `cover.md`.
5
8
 
6
9
  ## One loop
7
10