@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.
- package/LICENSE +21 -0
- package/README.md +71 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +7 -0
- package/docs/README.md +60 -0
- package/docs/file-formats/m0-iteration-protocol.md +120 -0
- package/docs/file-formats/m0p-and-custom-field.md +195 -0
- package/docs/handbook/README.md +27 -0
- package/docs/handbook/composition-arithmetic.md +278 -0
- package/docs/handbook/dsl-complexity.md +75 -0
- package/docs/handbook/dsl-rules.md +367 -0
- package/docs/handbook/feasibility-precision-quantization.md +591 -0
- package/docs/handbook/m0-construction-methods.md +201 -0
- package/docs/handbook/precision-tiers.md +84 -0
- package/docs/m0saic-thesis.md +95 -0
- package/docs/runtime/README.md +17 -0
- package/docs/runtime/cli-usage.md +372 -0
- package/docs/runtime/ffmpeg-expression-limits.md +117 -0
- package/docs/runtime/reduce-to-one.md +96 -0
- package/docs/skills/README.md +40 -0
- package/docs/skills/axis-and-geometry.md +103 -0
- package/docs/skills/dsl-stdlib-method-catalog.md +7 -0
- package/docs/skills/identity.md +123 -0
- package/docs/skills/labels-and-masks.md +170 -0
- package/docs/skills/m0saic-string-generation.md +251 -0
- package/docs/skills/more-atoms-not-bigger-atoms.md +77 -0
- package/docs/skills/overlay-semantics.md +194 -0
- package/docs/skills/parse-apis.md +79 -0
- package/docs/skills/passthrough-semantics.md +136 -0
- package/docs/skills/structural-construction.md +86 -0
- package/docs/skills/text-in-templates.md +126 -0
- package/docs/skills/zero-overlay-analysis.md +87 -0
- package/docs/templates/README.md +65 -0
- package/docs/templates/capability-templates.md +72 -0
- package/docs/templates/construction-strategy.md +329 -0
- package/docs/templates/data-pipeline.md +324 -0
- package/docs/templates/emission-patterns.md +130 -0
- package/docs/templates/geometry-recipes.md +248 -0
- package/docs/templates/layout-contract.md +168 -0
- package/docs/templates/output-resolution-tree.md +202 -0
- package/docs/templates/patterns/case-study-lessons.md +69 -0
- package/docs/templates/patterns/perf-authoring-rules.md +100 -0
- package/docs/templates/patterns/primitive-extraction-pattern.md +103 -0
- package/docs/templates/philosophy-and-contract.md +310 -0
- package/docs/templates/recursion-nested-rendering.md +138 -0
- package/docs/templates/reference/grid.md +104 -0
- package/docs/templates/reference/json-prop-type.md +169 -0
- package/docs/templates/reference/mosaic-color.md +81 -0
- package/docs/templates/reference/mosaic-placement-props.md +103 -0
- package/docs/templates/reference/prop-bindings.md +203 -0
- package/docs/templates/reference/template-flags.md +205 -0
- package/docs/templates/render-lifecycle.md +117 -0
- package/docs/templates/rendering-model-contract.md +392 -0
- package/docs/templates/standalone-pack-authoring.md +233 -0
- package/docs/templates/theming.md +81 -0
- package/docs/templates/ui-controls.md +150 -0
- 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.
|