@m0saic/knowledge 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +71 -0
  3. package/dist/index.d.ts +8 -0
  4. package/dist/index.js +7 -0
  5. package/docs/README.md +60 -0
  6. package/docs/file-formats/m0-iteration-protocol.md +120 -0
  7. package/docs/file-formats/m0p-and-custom-field.md +195 -0
  8. package/docs/handbook/README.md +27 -0
  9. package/docs/handbook/composition-arithmetic.md +278 -0
  10. package/docs/handbook/dsl-complexity.md +75 -0
  11. package/docs/handbook/dsl-rules.md +367 -0
  12. package/docs/handbook/feasibility-precision-quantization.md +591 -0
  13. package/docs/handbook/m0-construction-methods.md +201 -0
  14. package/docs/handbook/precision-tiers.md +84 -0
  15. package/docs/m0saic-thesis.md +95 -0
  16. package/docs/runtime/README.md +17 -0
  17. package/docs/runtime/cli-usage.md +372 -0
  18. package/docs/runtime/ffmpeg-expression-limits.md +117 -0
  19. package/docs/runtime/reduce-to-one.md +96 -0
  20. package/docs/skills/README.md +40 -0
  21. package/docs/skills/axis-and-geometry.md +103 -0
  22. package/docs/skills/dsl-stdlib-method-catalog.md +7 -0
  23. package/docs/skills/identity.md +123 -0
  24. package/docs/skills/labels-and-masks.md +170 -0
  25. package/docs/skills/m0saic-string-generation.md +251 -0
  26. package/docs/skills/more-atoms-not-bigger-atoms.md +77 -0
  27. package/docs/skills/overlay-semantics.md +194 -0
  28. package/docs/skills/parse-apis.md +79 -0
  29. package/docs/skills/passthrough-semantics.md +136 -0
  30. package/docs/skills/structural-construction.md +86 -0
  31. package/docs/skills/text-in-templates.md +126 -0
  32. package/docs/skills/zero-overlay-analysis.md +87 -0
  33. package/docs/templates/README.md +65 -0
  34. package/docs/templates/capability-templates.md +72 -0
  35. package/docs/templates/construction-strategy.md +329 -0
  36. package/docs/templates/data-pipeline.md +324 -0
  37. package/docs/templates/emission-patterns.md +130 -0
  38. package/docs/templates/geometry-recipes.md +248 -0
  39. package/docs/templates/layout-contract.md +168 -0
  40. package/docs/templates/output-resolution-tree.md +202 -0
  41. package/docs/templates/patterns/case-study-lessons.md +69 -0
  42. package/docs/templates/patterns/perf-authoring-rules.md +100 -0
  43. package/docs/templates/patterns/primitive-extraction-pattern.md +103 -0
  44. package/docs/templates/philosophy-and-contract.md +310 -0
  45. package/docs/templates/recursion-nested-rendering.md +138 -0
  46. package/docs/templates/reference/grid.md +104 -0
  47. package/docs/templates/reference/json-prop-type.md +169 -0
  48. package/docs/templates/reference/mosaic-color.md +81 -0
  49. package/docs/templates/reference/mosaic-placement-props.md +103 -0
  50. package/docs/templates/reference/prop-bindings.md +203 -0
  51. package/docs/templates/reference/template-flags.md +205 -0
  52. package/docs/templates/render-lifecycle.md +117 -0
  53. package/docs/templates/rendering-model-contract.md +392 -0
  54. package/docs/templates/standalone-pack-authoring.md +233 -0
  55. package/docs/templates/theming.md +81 -0
  56. package/docs/templates/ui-controls.md +150 -0
  57. package/package.json +37 -0
@@ -0,0 +1,202 @@
1
+ # The output-resolution tree
2
+
3
+ How a render's top-level **duration / size / fps / format / audio** resolve,
4
+ across CLI, Make, templates, and the engine. Mapped at gate 28 (2026-08-31)
5
+ after three gates of the same bug class; merged from
6
+ `candidates/2026-08-31-output-resolution-tree.md`.
7
+
8
+ When writing a template or a host feature that touches any of these values,
9
+ place it in the tree FIRST — the recurring failure is a value from one layer
10
+ masquerading as another.
11
+
12
+ ## The law (as it now stands, gates 20–28)
13
+
14
+ Precedence, top to bottom — higher wins:
15
+
16
+ ```
17
+ L0 USER EXPLICIT the ask the user actually typed/clicked
18
+ L1 TEMPLATE INTENT what the doc AUTHORS (the render's own declarations)
19
+ L2 HOST DEFAULTS hints the host seeds when the user said nothing
20
+ L3 ENGINE DEFAULTS DEFAULT_DURATION_MS / DEFAULT_FPS / mp4 / silence
21
+ ```
22
+
23
+ The recurring bug class of this sprint (gates 26, 27, 28) was always the same
24
+ shape: **an L2 value masquerading as L0** (hint-seeded target read as a pin;
25
+ host canvas clobbering authored size) or **an L1 value lost in the wrapper**
26
+ (stampDocOutput clobbering authored durationMs/size).
27
+
28
+ ## Per-property trees (current, verified)
29
+
30
+ ### durationMs
31
+ - **L0**: CLI `--durationMs` → `ctx.userIntent.durationMs` (assembleUserIntent)
32
+ + plan `options.durationMs` (DURATION_MS_OVERRIDDEN_BY_CLI diagnostic).
33
+ Make's Duration field → `userIntent.durationMs` when SET (Q1, 08-31).
34
+ CLI `.mosaicx` path (`make <file.mosaicx>` / `resolve`): `--durationMs ??
35
+ wrapper.durationMs` → `userIntent.durationMs` via `mosaicxUserIntent`
36
+ (the CLI source (not published), 09-05) — the wrapper's `durationMs`
37
+ was already the plan length, so it is an explicit ask; before 09-05 it only
38
+ set the plan length and TRIMMED self-timing templates instead of pinning them.
39
+ - **L1**: doc authors `durationMs` → `stampDocOutput` preserves it (gate-26
40
+ law). Pipelines: per-step `durationMs` is always authored (rule 4);
41
+ `resolveStampedPipelineDurationMs`.
42
+ - **L2**: CLI seeds the render target from `outputHints.durationMs`
43
+ (animated variants: `resolveUnpinnedDurationMs`). The hint-echo trap is
44
+ CLOSED (Q1, 08-31): `resolvePinnedDurationMs` is `userIntent`-only, so a
45
+ hint-seeded `output.durationMs` can never read as a user ask. Template side:
46
+ never compare `ctx.output.durationMs` to your own default to detect a pin —
47
+ that reads L2 as L0 (dsl-tutorial crammed every walk into its 12s hint, gate
48
+ 33, 09-05); call `resolvePinnedDurationMs(ctx)`, else author the natural
49
+ length on `doc.durationMs` (L1).
50
+ - **L3**: `DEFAULT_DURATION_MS`.
51
+
52
+ ### size (width × height)
53
+ - **L0**: explicit `-w/-h` / Make Device — ALWAYS wins. `make`'s dims are
54
+ now OPTIONAL: absence IS the explicitness signal (no provenance
55
+ introspection needed).
56
+ - **L1**: the doc's own `size` — **required on every ROOT document** (the
57
+ boundary law: platform serializer + loader assert it; children stay
58
+ optional — the stretch-vs-letterbox lever). Hosts default the plan to
59
+ it: saved files reproduce ("open a .mosaic, render, get the same
60
+ thing"), templates that author their canvas (brand marks at official
61
+ dictionary dims) get it as the default. `stampDocOutput` preserves
62
+ authored size; docs authoring nothing get the resolved target so every
63
+ stamped root carries a canvas. Steps: `step.file.size` (rule 4) — same
64
+ law, always had it.
65
+ - **L2**: template path seeds ctx from the template's hints when no explicit
66
+ dims — the static `outputHints.width/height`, with the template's
67
+ `resolveOutputHints(props)` merged over them (2026-09-15, the canvas-as-a-
68
+ knob contract: a creator template's `platform` picks 1920×1080 vs
69
+ 1080×1920). ONE helper for every host: `resolveTemplateOutputHints(tmpl,
70
+ props)` in `@m0saic/template-utils` (CLI `make` + the wireframe/tutorial
71
+ paths, Electron preview/cover/renderToFile, the web design preview, and
72
+ Make's Device anchor, which re-resolves on every prop change so a locked
73
+ canvas follows the knob BEFORE the next preview). Tolerant: a throwing or
74
+ junk resolver falls back to the static hints; width/height round to even.
75
+ Fallback 1280×720.
76
+ - **L3**: none.
77
+ - **Guard law (2026-09-15):** the template-path feasibility guard measures the
78
+ rendered doc at the size the PLAN uses — the doc's authored size when no
79
+ `-w/-h` — never the hint-seeded target. It used to read the target and
80
+ rejected a correctly authored 1080×1920 doc against a 1920×1080 hint
81
+ ("below the minimum feasible") — the hint-read-as-ask bug class again.
82
+ - **Removed**: `sizePolicy` (existed for ~a day) — "size always present +
83
+ default-not-command" dissolves the intent-vs-history ambiguity without
84
+ a field.
85
+
86
+ ### fps
87
+ - **L0**: CLI `--fps` (explicit only — threads to planOptions;
88
+ FPS_OVERRIDDEN_BY_CLI). Make Output fps passes explicitly → wins (open
89
+ founder call from gate 20).
90
+ - **L1**: pipeline-authored fps (gate-20: `resolveStampedPipelineFps`,
91
+ fps-follow for recordings). Plain docs: stamped from resolved target.
92
+ - **L2**: `outputHints.fps` as ctx default. **L3**: DEFAULT_FPS.
93
+
94
+ ### format (container/kind)
95
+ - **L0**: `-o` extension / `--format`.
96
+ - **L1**: doc `format` (gate-26 convention: every rendered doc declares it)
97
+ + CLI rule-5 POST-RENDER enrichment (gate-22: authored mode-dependent
98
+ detail reaches the resolver; explicit `--alpha` wins). `doc.target`
99
+ presets resolve to format/audio/color defaults; per-field overrides win.
100
+ - **L2**: the template tier, two statements, the more specific first:
101
+ 1. a reserved prop named **`outputFormat`** holding a container name
102
+ (`png` / `jpeg` / `webp` → image; `mp4` / `mov` / `webm` / `mkv` → video) —
103
+ trickplay, screencap-grid, page-skeleton, qr/code, highlights. A prop is
104
+ the template speaking *per render*, so it WINS over the static hint
105
+ (the CLI source (not published) `handleTemplateMake`, flipped 2026-09-13 —
106
+ before that the hint would have silenced the knob; Make's
107
+ `outputFormat`-prop sync already behaved this way).
108
+ 2. `outputHints.format` — the static deliverable. **Every public template
109
+ declares one** (the `outputFormat` convention, record posture — see
110
+ [`philosophy-and-contract.md`](philosophy-and-contract.md) §Output
111
+ contract); a knob template declares the knob's DEFAULT so the two agree
112
+ at defaults.
113
+ - **L3**: mp4 — reached only by internal / deprecated templates now.
114
+
115
+ Why L2 matters beyond the extension: the Make share link carries an `f=` ask
116
+ only when the form's output kind differs from what the hint would set on the
117
+ receiver, and the modal maps an empty kind to video — so a hint-less template
118
+ put `f=video` on every link at defaults (the 2026-09-13 sweep: 33 public
119
+ templates gained a hint, 26 video / 7 image).
120
+
121
+ ### audio
122
+ - **L0**: CLI `--no-audio` → `audio.mode:"off"` patch.
123
+ - **L1**: doc/encode `audio.mode` — the TRI-STATE law (Q2, founder-ruled
124
+ 2026-08-31; `enabled: boolean` is GONE from `MosaicAudioConfig`, hard
125
+ swap, no deprecation):
126
+ - `"off"` → never a track, even with real inputs (the mute knob);
127
+ - `"on"` → always a track (real mix, or an anullsrc silence bed);
128
+ - `"auto"`/absent → real mix when audio-bearing inputs exist, **NO track
129
+ when none** — silent visuals ship video-only by default.
130
+ Source-level `MosaicAudioProps.enabled` (per-tile mute) is a DIFFERENT
131
+ type and keeps its boolean.
132
+ - **L3**: silence-by-default is RETIRED. The engine chain rule:
133
+ - "Audio-bearing" = INTENT, not bindability: probed `hasAudio` streams
134
+ count, and an unprobed `mediaType:"audio"` source still counts (its
135
+ intent is unambiguous; the mix degrades to a bed, never to no-track).
136
+ - Internal carriers still always mux `[outa]` (gate-21 binding), but a
137
+ binding-only bed is stamped `audioIntent:false` on the command, and the
138
+ node-output stamp skips `hasAudio` for it — a child's bed never becomes
139
+ the parent's "audio input" (pre-fix every child-bearing composite
140
+ shipped a phantom silent track).
141
+ - Pipelines: `pipelineWantsAudio` resolves the same tri-state AFTER the
142
+ step loop ("auto" = any step actually muxed); the xfade stitch now
143
+ synths silence legs for audio-less steps exactly like the cut-concat
144
+ path (the old KNOWN GAP, hit the moment silent steps stopped muxing).
145
+ - Argv honesty on no-`-map` commands: `stepMuxesAudio`'s fallback reads
146
+ "no -map, no -an" as "keeps the input's audio", so any command without
147
+ maps that can end a nested plan MUST carry `-an` when its output is
148
+ video-only — the loop-fit pass (`pipeline_duration_fit`) does now
149
+ (a silent nested pipeline's fit output otherwise made the outer stitch
150
+ bind a `[i:a:0]` that matched no streams).
151
+ - Mixed compositions ("one source has audio, one doesn't" — the historic
152
+ reason for authoring null-audio beds everywhere) are guarded at three
153
+ levels: flat mixes probe-gate `[i:a:0]` refs to stamped streams; stitch
154
+ legs synth silence per audio-less step; child carriers always mux beds
155
+ so parent binding can't fail. Authored silence beds are no longer
156
+ needed for composition safety.
157
+ Locked by: `pipelineAudio.spec` ("Q2 tri-state law" describe),
158
+ `ffmpegCommands`/`buildMosaicPlanFromFile` unit specs, the CLI contract
159
+ matrix (57 silent-fixture rows now `acodec:null`, `audio-mode-on`
160
+ fixture = the bed opt-in leg, `codec-matrix-audio` out-silent = the mute
161
+ leg, `audio-montage-child` = real child audio survives).
162
+
163
+ ## Open simplifications (proposed, awaiting founder ruling)
164
+
165
+ 1. **Q1 — DONE (2026-08-31, founder-ruled).** Duration pinning is
166
+ `userIntent`-only: `resolvePinnedDurationMs` reads ONLY
167
+ `ctx.userIntent.durationMs`; the CLI populates it from an explicit
168
+ `--durationMs`, Make (electron main) from its Duration field when SET —
169
+ in both the real-render and design-preview ctx so they agree. The
170
+ hint-echo trap is dead globally; search-typing's gate-27 local
171
+ discriminator deleted. Fallback users verified: mermaid already fit to
172
+ `?? ctx.output.durationMs` (unchanged); camera-debug + snippet-morph
173
+ treat unpinned as natural cadence (the correct behavior their hint-pins
174
+ were masking).
175
+ 2. **Q2 — DONE (2026-08-31, founder-ruled).** Audio default flipped + the
176
+ config went tri-state (`mode: "auto"|"on"|"off"`, `enabled` deleted with
177
+ no deprecation — "we are pre launch", callers migrated, old docs may
178
+ break). Full law + engine chain rule in the audio section above. The
179
+ recurring gate-20 silent-track class is retired at the root: doc-level
180
+ `audio:{enabled:false}` boilerplate is no longer needed on silent
181
+ templates (existing `mode:"off"` sites are now pure intent, not
182
+ bug-avoidance). Golden sweep done (command goldens re-minted: silence-
183
+ bed mux siblings dropped, `-an` on silent deliverables).
184
+ 3. **Q3 — one duration-follow helper.** subtitle-burn, search-typing,
185
+ highlights each hand-roll "explicit ask wins, else follow natural/
186
+ input" with different spellings. A template-utils
187
+ `resolveOutputDurationMs(ctx, { naturalMs })` encoding L0>L1 would make
188
+ the law one function. (Blocked on Q1 for the clean version.)
189
+ 4. **DONE this gate:** the size tree is fully coherent (required-at-root
190
+ boundary law + optional `-w/-h` + host defaults to doc size);
191
+ `makeErrorMosaic` audio + size fixed fleet-wide; the stale
192
+ pre-gate-26 duration expectation in defineMosaicTemplate.test aligned.
193
+
194
+ ## Non-goals
195
+
196
+ `outputHints` stays static (L2 seeding only) and is the browse-time truth
197
+ (manifest, cards). Props-dependent dims need BOTH halves: author the doc for
198
+ the PLAN (L1), and declare `resolveOutputHints(props)` so hosts SEED the
199
+ right target too (L2) — the resolver is what removes the letterbox and the
200
+ wrong-canvas guard. The seam's `outputHintsResolve` convention (throw) keeps
201
+ it honest: an object, deterministic, and at `defaultProps` equal to the
202
+ static hints for every field it returns.
@@ -0,0 +1,69 @@
1
+ # Template case-study lessons
2
+
3
+ Distilled 2026-07-26 from seven Feb-era case-study essays (deleted — git history has
4
+ the long forms). Format follows [`perf-authoring-rules.md`](perf-authoring-rules.md):
5
+ the rule, the incident that paid for it, the code that proves it. Companion:
6
+ [`primitive-extraction-pattern.md`](primitive-extraction-pattern.md).
7
+
8
+ **L1 — Derive overlay depth from source count; never hardcode `F{F{F}}`.**
9
+ `buildOverlayStack(sources.length)` (https://github.com/m0saic-project/m0saic-packages/blob/main/packages/template-utils/src/m0saic/build-overlay-stack.ts#L11)
10
+ keeps the m0 and the source array in lockstep when layers are added/removed.
11
+ **But** apply the Rect-Thesis smell test first: if your m0 is `buildOverlayStack(N)`
12
+ with `N == element count` and every source is full-canvas, you've baked pixel-math
13
+ into drawtext instead of geometry — Preview/Render Hero will be empty. Rebuild with
14
+ real cells ([`../construction-strategy.md`](../construction-strategy.md)).
15
+
16
+ **L2 — Solid-colour cells are `makeColorTile`, not text-sources-with-background.**
17
+ The old pill-bar technique (empty `MosaicTextSource` + `visual.backgroundColor`) is
18
+ explicitly legacy: lavfi `color=` is free per cell, and `makeColorTile` tiles are
19
+ valid `MosaicRefSource` targets without escape hatches
20
+ (https://github.com/m0saic-project/m0saic-packages/blob/main/packages/template-utils/src/sources/makeColorTile.ts#L28). For lavfi sizing,
21
+ `fitMode:"tile"` fills the slot rect; `"content"` sizes to the content box +
22
+ placement.
23
+
24
+ **L3 — Transparent lavfi + `drawbox` needs `:replace=1`.** Plain `drawbox` on
25
+ `color=black@0` blends RGB but does NOT write alpha. This is
26
+ [`perf-authoring-rules.md`](perf-authoring-rules.md) **R7** — canonical there;
27
+ verify transparent tracks in rgba.
28
+
29
+ **L4 — Staggered reveals: lead + trail padding, identity by `logicalIndex`.**
30
+ The wireframe reveal (`wireframe/animated/v1/animated-wireframe.ts`) is the reference:
31
+ `startAtSec = leadPaddingSec + i * staggerSec`; trail padding =
32
+ `max(leadPaddingMs, OVERLAY_SLIDE_MS = 300)` so the LAST slide finishes before the
33
+ cut (`:157-161`); per-tile identity via `frame.logicalIndex`
34
+ (`childId = \`cell-${logicalIndex}\``, `:236`). Gate with the typed
35
+ `overlay.window` (preferred) or a recognized `enable` shape (R4).
36
+
37
+ **L5 — Rank-driven per-tile animation (brand/logo/v3 machinery).** A deterministic
38
+ rank function maps every filled tile to its time slot; four modes
39
+ (`logo_loop | progress_fill | loading_shimmer | loading_ui_v2`) + `rankSet`
40
+ (`diag | cascade | radial`) reuse one rank→enable/alpha pipeline
41
+ (`brand/logo/v3/logo.ts:39`). Guard frame 0 with `gte(t, 1/fps)` so nothing pops
42
+ before its slot. Two-doc blends ride the `F{F}` runner
43
+ (`logo/v3/logo_runner.ts:233`). Dictionary geometry ids: `m-33`, `m0`,
44
+ `m0saic-pattern`, `m-33_bitmap` (https://github.com/m0saic-project/m0saic-packages/blob/main/packages/dictionary/src/entries/brand).
45
+
46
+ **L6 — Primitive positioning must be RATIO-based, and verified NESTED.** grid v1
47
+ computed absolute pixel positions and re-encoded them as a canvas-scale `placeRects`
48
+ basis: renders perfectly standalone, **silently drops when nested** (the engine
49
+ re-divides the per-pixel basis below 1px at e.g. 1024²). grid v2's design law:
50
+ position = a proportion of the cell expressed as an overlay offset (`H*frac`);
51
+ thickness = decoupled fixed-px strips (`primitives/grid/v2/grid.ts:23-40`).
52
+ Composability wins over node count. Corollary: "runnable standalone" is NOT a
53
+ sufficient primitive gate — test the primitive nested inside a parent split.
54
+
55
+ **L7 — Primitives never interpret domain config.** A gridline primitive takes counts
56
+ and fractions, not "chart config". [`primitive-extraction-pattern.md`](primitive-extraction-pattern.md)
57
+ Rule 1 owns this.
58
+
59
+ **L8 — Internal-pane text sizes derive from `ctx.target.height`, NOT the pane rect.**
60
+ The screencap InfoPane fix (`media/screencap_grid/internal/info_pane.ts:56-58,93-94`):
61
+ `titleFontSize = clamp(16..48, round(targetH * 0.022))` — pane-proportional fonts
62
+ wobble as the pane resizes, and fractional y-offsets resolve against the RASTER
63
+ height, landing text in the wrong band. Pixel-anchor metadata blocks
64
+ (`metaY = topPad + titleBlockH + gap`); auto-size the pane from content in the
65
+ parent.
66
+
67
+ **L9 — A template can be a TOOL.** The contact-sheet class (screencap grid) earns
68
+ its keep by producing a decision artifact a human can understand, share, and act on
69
+ immediately — design for that artifact, not for DSL flex.
@@ -0,0 +1,100 @@
1
+ # Template perf authoring rules
2
+
3
+ > Templates layer. The author-facing distillation of the engine's cost model and
4
+ > ffmpeg walls — the rules below stand alone; the deep dives are
5
+ > **engine-internal** (`.ai/moat/runtime/render-cost-model.md`,
6
+ > `.ai/moat/runtime/ffmpeg-limitations.md` — absent in the shipped copy).
7
+ > Every rule below was paid for by a real dsl-tutorial or chart-template incident.
8
+
9
+ ## R1 — Complexity belongs in the m0 string, not the source list
10
+ The string is near-free (millions of chars parse fine); **source count is the cost**
11
+ — every source is built at setup AND composited on every frame for the whole clip
12
+ (no active window unless it declares one — see R4). The smell: emitting N sources
13
+ to draw ONE visual thing. Measured: a donut whose sweep needed 31 lavfi arc
14
+ slivers took 376s to render 3s. Window gating does NOT fix this — the setup
15
+ intercept and the per-frame composite count remain per source.
16
+
17
+ ## R2 — Smooth shapes are ONE masked tile; coarse shapes are a few unmasked tiles
18
+ A mask is one source regardless of edge length; a tile grid stitches at ~N².
19
+ Never draw shapes with drawtext glyphs (tofu) or text-masked rects (distortion) —
20
+ shapes are `makeColorTile` + inline-mask SVG with `bounds` = cell aspect.
21
+
22
+ ## R3 — Never geq; never expression-mode filters
23
+ geq evaluates its expression per pixel per frame — 2–3 orders of magnitude over
24
+ compiled filters. Rounding and shape masks are SVG sidecars now (engine default).
25
+ The trap re-enters through the back door: a time-varying `overlay.alpha` expression
26
+ compiles INTO a per-pixel geq fold. Which is why —
27
+
28
+ ## R4 — Give every short-lived source a WINDOW; animated alpha is affordable when canonical
29
+ `overlay:enable=` is a scalar per-frame gate (free) — still the right tool for
30
+ hard on/off. And since the enable-gating engine sprint (2026-07-09/10), windowed
31
+ FADES are cheap too: canonical alpha products (`fadeInExpr` shapes, `exit()`
32
+ complements, the in/hold/out envelope, constants) lower to compiled
33
+ `fade`/`colorchannelmixer`, and any surviving fold is dual-gated + trimmed to
34
+ the source's window. Declare the window as an `enable` in a recognized shape
35
+ (`gte(t,A)`, `lt(t,B)`, `between(t,A,B)`, `gte*lt`) or the typed
36
+ `overlay.window {startSec,endSec}` — the published motion-kit helpers
37
+ (`entrance`/`exit`/`composeMotion` + the track builders in
38
+ `@m0saic/template-utils`) emit it automatically, as do the alpine-pack-local
39
+ reveals (`revealGate`/`revealSlideNode`/`revealFade` in
40
+ `alpine/_shared/alpine-anim.ts` — pack-local, not published). What still costs: NON-canonical alpha with NO window (spatial
41
+ sweeps, custom eases, unbounded lifetimes). Consequence for two-mode templates:
42
+ the alpine premium/light split is now a LOOK decision, not a perf one — the
43
+ runner renders premium ≈ 3× faster than pre-sprint light.
44
+
45
+ ## R5 — A value that changes N times = one layer per DISTINCT VALUE, not one N-boundary expression
46
+ The narration-collapse idiom (dsl-tutorial inspector). A single `%{eif:…}` with ~100
47
+ change-boundaries crashes drawtext's ~80-boundary budget; 8 fields × animated alpha
48
+ overflowed a filtergraph. One text source per distinct value, enable-gated over the
49
+ union of windows where that value holds.
50
+
51
+ ## R6 — Line geometry over time = ONE lavfi drawbox track, not N masked overlays
52
+ Cursors, split lines, borders, curtain wipes: collapse N time-disjoint elements into
53
+ one lavfi source whose graph is a flat chain of enable-gated `drawbox` ops. Depth
54
+ O(N) → O(1); measured 37.9s → 9.0s on the 4×4 canvas. Keeps the whole panel at a
55
+ fixed handful of overlay layers at any tile count — safely under the ~25 overlay-depth
56
+ mask-drop ceiling (see limitations doc W3). Constraints: solid rect strokes only
57
+ (dashes = runs of boxes); positions snap per-window (no smooth per-frame motion).
58
+ For a masked line draw-on, use a curtain wipe over the static art, not a per-sliver
59
+ cascade.
60
+
61
+ ## R7 — `replace=1` on every drawbox over a transparent base
62
+ Plain `drawbox` on `color=black@0` blends RGB but does NOT write alpha — edges are
63
+ `(r,g,b,0)`: visible in an rgb24 probe, gone after compositing. Verify transparent
64
+ lavfi tracks in **rgba** and check edge-pixel alpha.
65
+
66
+ ## R8 — Bake what never changes
67
+ A subtree that is constant across renders and expensive to composite → render it once
68
+ and reference the asset; a precise subtree that bloats the parent → nest it
69
+ ("reduce to 1"). Triggers, mechanics, and measured payoffs:
70
+ [`../../runtime/reduce-to-one.md`](../../runtime/reduce-to-one.md). The cost-model
71
+ bar: an intermediate round-trip costs ~5–15 ms/frame — anything costlier in-graph
72
+ should be materialized.
73
+
74
+ ## R9 — Text: svg rasterizer for static, drawtext only for per-frame-dynamic
75
+ `rasterizer:"svg"` turns static text into a mask + color tile (no drawtext, no
76
+ per-frame shaping). It does NOT support expressions — that fallback to drawtext is
77
+ correct, keep dynamic text drawtext and keep the instance count low. Mind
78
+ density budgets: hundreds of glyph masks in one panel approach the argv wall
79
+ (dsl-canvas caps: 260 mask subpaths, 200 labels, 500 curtain boxes per source).
80
+ Also: the app fits text wider than the CLI — leave generous fixed-font margins.
81
+
82
+ ## R10 — Camera: constant zoom is cheap, animated zoom is a different animal
83
+ Constant zoom = static `eval=init` scale + free crop. An animated zoom forces
84
+ per-frame scale (`eval=frame`) — the stream becomes variable-size and materially
85
+ more expensive. Don't animate zoom for effect you can get from enable-gated cuts.
86
+
87
+ ## R11 — Measure before optimizing
88
+ `m0saic make … --perf` writes a `.perf.json`/`.perf.md` sidecar: per-command timing
89
+ rolled up by node, with `overlayDepth` per rect and a hotPath. Render at two
90
+ durations to split setup cost (intercept) from per-frame cost (slope). Optimize the
91
+ named bottleneck, not the vibe.
92
+
93
+ ## Quick pre-flight (before handing off a candidate)
94
+ - [ ] Source count ≈ number of visually distinct things (not slivers/glyph shards)?
95
+ - [ ] Any geq / animated-alpha / N-boundary expression? (R3–R5)
96
+ - [ ] Line geometry collapsed to tracks; `replace=1` present? (R6–R7)
97
+ - [ ] Overlay depth per panel comfortably under ~25?
98
+ - [ ] Static text on svg rasterizer; dynamic text instances counted? (R9)
99
+ - [ ] Constant-across-renders expensive subtree baked or nested? (R8)
100
+ - [ ] `--perf` sidecar checked once at target size?
@@ -0,0 +1,103 @@
1
+ # Primitive Extraction Pattern (Templates → Primitives)
2
+
3
+ The repeatable pattern for extracting reusable, globally-useful logic from a
4
+ domain template into a **primitive** under `@m0saic/primitives/*`. Goal:
5
+ primitives reusable across templates for years without inheriting domain
6
+ assumptions.
7
+
8
+ > **Source of truth:** primitives in [https://github.com/m0saic-project/m0saic-packages/blob/main/packages/templates/src/m0saic/primitives](https://github.com/m0saic-project/m0saic-packages/blob/main/packages/templates/src/m0saic/primitives); extraction helpers: `buildOverlayStack` in https://github.com/m0saic-project/m0saic-packages/blob/main/packages/template-utils/src/m0saic/build-overlay-stack.ts; `transparentSlot`, `lavfiStrip`, `buildGridlineSources` in https://github.com/m0saic-project/m0saic-packages/blob/main/packages/template-utils/src/primitives (`transparentSlot.ts`, `lavfiStrip.ts`, `girdlines.ts` (sic — misspelled in code)).
9
+
10
+ ## Definitions
11
+
12
+ - **Domain template** — expresses a specific user-facing feature or design
13
+ (e.g. `@m0saic/collage/image-collage/v1`). Owns semantics, tunings, theme
14
+ tokens, animation rules.
15
+ - **Internal template** — used only inside one domain template's tree (e.g.
16
+ `charts/bar-graph/v1/internal/plot-area`). Often expects injected children /
17
+ parent refs; often not runnable standalone; carries domain assumptions.
18
+ - **Primitive template** — globally reusable building block (e.g.
19
+ `@m0saic/primitives/grid/v2`). One focused job, no domain coupling, runnable
20
+ standalone, designed for reuse across domains.
21
+
22
+ ## When to extract
23
+
24
+ Extract when most of these hold: the behavior is **general** (gridlines,
25
+ borders, matte fills, masks, simple overlays); it's **parameterizable** with a
26
+ small stable prop surface; it's **already being reimplemented** (copy/paste
27
+ creep); it has **no domain meaning**; it's **useful outside** the current
28
+ template.
29
+
30
+ Do NOT extract when: logic depends on domain semantics ("baseline belongs to
31
+ PlotArea"); props need parent/child wiring (refs like `barsStackRef`); it's an
32
+ experimental one-off or unstable API.
33
+
34
+ ## Hard rules
35
+
36
+ 1. **No domain semantics.** A primitive must not know "charts", "baseline",
37
+ "bars", "labels", or domain axis config. Accept generic parameters
38
+ (`direction`, `origin`, `excludeEdges`); the domain template decides meaning.
39
+ 2. **Small, explicit prop surface.** Prefer `direction`, `count`,
40
+ `excludeEdges?`, `origin?`, `color?`, `opacity?`, `thicknessFrac?`. No
41
+ kitchen-sink domain config objects.
42
+ 3. **Runnable standalone.** `m0saic make @m0saic/primitives/<slug>/vN` must
43
+ produce valid output — no required child refs, safe defaults, no crash in
44
+ isolation.
45
+ 4. **Flagged correctly.** Set `primitive: true`
46
+ (https://github.com/m0saic-project/m0saic-packages/blob/main/packages/types/src/template/template.ts#L1416 — "is this a base others
47
+ build on?") and default to public (`internal: false` — "can this render on
48
+ its own?"); the two flags are orthogonal and compose freely. Example:
49
+ `primitives/grid/v1/grid.ts:166`. A truly-internal helper is not a
50
+ primitive; it's an internal utility template.
51
+ 5. **Reusable composition helpers.** Never hardcode `F{F{F}}` overlay chains —
52
+ compute from count via `buildOverlayStack(n)`.
53
+ 6. **Valid canonical m0 only.** Helpers that generate m0saic must produce
54
+ canonical strings, validate (or finalize) before returning, and return
55
+ branded `M0String` where possible.
56
+ 7. **Verify the primitive NESTED, not just standalone.** Standalone renders
57
+ prove nothing about composition: grid v1 satisfied every rule above and was
58
+ still deprecated. Its single-axis path re-encoded absolute pixel positions
59
+ as a `placeRects` split with a per-pixel basis — per the v2 header
60
+ (https://github.com/m0saic-project/m0saic-packages/blob/main/packages/templates/src/m0saic/primitives/grid/v2/grid.ts#L16-22`): "it
61
+ renders exactly STANDALONE (853 slices of 853px = 1px each) but the
62
+ per-pixel basis does NOT nest — the engine folds the child split into the
63
+ parent layout, re-divides the basis below 1px, cells round to 0, and the
64
+ grid is SILENTLY DROPPED at certain canvases". A primitive's acceptance test
65
+ is composed-inside-a-cell at several canvas sizes, not one full-frame render.
66
+
67
+ ## The extraction process
68
+
69
+ 1. **Identify the core behavior** in non-domain words ("create N-1
70
+ evenly-spaced line overlays in one direction, optionally excluding edges").
71
+ 2. **Define the primitive API** — a tiny prop surface (Rule 2).
72
+ 3. **Move the implementation** to `@m0saic/primitives/...`: new template id,
73
+ defaults that render something clearly, runnable standalone (Rule 3),
74
+ flags per Rule 4.
75
+ 4. **Extract shared utilities into `@m0saic/template-utils`** when multiple
76
+ templates need them: `buildOverlayStack(count)`, `transparentSlot()`,
77
+ `buildGridlineSources(opts)`, `lavfiStrip(...)` (paths in the source-of-truth
78
+ note above).
79
+ 5. **Replace the old internal logic** with the primitive, keeping domain
80
+ decisions local (PlotArea decides baseline ownership; the grid primitive
81
+ just draws lines) — then **delete the legacy local versions**
82
+ (`horizontalLineAtFracY`-style shadow implementations must not survive).
83
+
84
+ For a breaking change later: create `.../v2`; never mutate `v1`.
85
+
86
+ ## Worked example: grid v1 → v2 (the ratio-vs-absolute extraction)
87
+
88
+ **v1** (`@m0saic/primitives/grid/v1`) was a textbook extraction by Rules 1-6:
89
+ generic props, standalone-runnable, public + `primitive: true`, validated m0.
90
+ It still failed as a primitive: it computed each gridline's ABSOLUTE pixel
91
+ position from `ctx.target` and re-encoded them as a split whose basis ≈ the
92
+ axis length in px (`853[0,1,0,…]`). Composed into a parent, the engine
93
+ re-divides that basis below 1px and the gridlines silently vanish at some
94
+ canvases (1024², 1000²) while surviving at others (1080²). Deprecated
95
+ 2026-07-07; kept registered as a "what not to do" reference
96
+ (`primitives/grid/v1/grid.ts:170-175`).
97
+
98
+ **v2** (`@m0saic/primitives/grid/v2`) repositions each line as a PROPORTION —
99
+ a thin lavfi strip at an overlay offset (`H*frac`) evaluated against the actual
100
+ cell at render time, thickness decoupled as a fixed thin strip. Costs N+1
101
+ nested overlays instead of one `placeRects`, but composes at every canvas —
102
+ for a primitive, composability wins. Full rationale in the v2 header
103
+ (`grid/v2/grid.ts:1-55`). This failure mode is why Rule 7 exists.