@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,130 @@
1
+ # Template emission patterns — data + telemetry
2
+
3
+ Templates produce up to three kinds of output: **pixels** (the rendered video/image —
4
+ the primary purpose, documented in `rendering-model-contract.md`), **structured data**
5
+ (typed values published to downstream consumers), and **telemetry** (observability
6
+ events). This doc covers the latter two.
7
+
8
+ ---
9
+
10
+ ## The three channels
11
+
12
+ | Channel | Audience | Persistence | Read via |
13
+ |---|---|---|---|
14
+ | `MosaicDataSource` in `doc.sources` | Downstream pipeline steps | In-memory, render-scoped | `ctx.upstreamData[alias]` |
15
+ | `doc.sidecars.<key>` | End user / external callers | Disk, as `{output-basename}.<key>.json` | the file |
16
+ | `emitTemplateLog(ctx, …)` | Analytics, debugging, dev console | Sink-defined (NDJSON, IPC, …) | `MosaicEngineContext.telemetry` |
17
+
18
+ Sidecar writing is owned by the engine — the render engine source (not published).
19
+ Templates *declare*; core *writes*. Do not write sidecar files from inside `render()`.
20
+
21
+ > **Adoption reality check (2026-07-26).** These are real, wired mechanisms, but
22
+ > adoption is thin: **`@m0saic/media/subtitle-burn/v1` is still the only template that
23
+ > emits telemetry at all**, and only it and `@m0saic/forensic/watermark/video/v1`
24
+ > declare `sidecarsSchema`. Treat the conventions below as *the intended shape*, not as
25
+ > a well-trodden path — you may be the second or third adopter, so prefer following
26
+ > subtitle-burn exactly over inventing a variant.
27
+
28
+ ---
29
+
30
+ ## Telemetry: always via `emitTemplateLog`
31
+
32
+ Emit through `emitTemplateLog` from `@m0saic/types` — **never construct envelopes
33
+ inline**. The helper supplies correlation-field defaults and falls back to a no-op sink
34
+ when `ctx.telemetry` is absent, so it is safe to call unconditionally.
35
+
36
+ **`traceId` / `spanId` still default to the `"unwired"` sentinel**
37
+ (https://github.com/m0saic-project/m0saic-packages/blob/main/packages/types/src/telemetry/emit.ts#L13-14`). This survived the telemetry sprint —
38
+ it is current behavior, not a stale note. Don't be surprised by `"unwired"` in output,
39
+ and don't hand-fake a trace id to work around it.
40
+
41
+ ### Event discriminator convention
42
+
43
+ The closed `kind` union stays `template_log` for **all** template-emitted events.
44
+ Distinguish event types with `data.event: "<name>"` inside the free-form payload. This
45
+ gives consumers a stable filter key without growing the union — adding a `kind` member
46
+ is a types-package change with a much wider blast radius.
47
+
48
+ ### Taxonomy for selection-style templates
49
+
50
+ Templates that pick one thing from many (track, preset, route, fallback):
51
+
52
+ | Event | Carries |
53
+ |---|---|
54
+ | `<thing>_discovered` | what the template saw — count + identifying metadata |
55
+ | `<thing>_selected` | what it picked, including `rule: "<algorithm-path>"` |
56
+ | `<thing>_filtered` | counts before/after window/predicate filtering |
57
+ | `<thing>_applied` | that an optional transform actually fired |
58
+ | `passthrough` | no selection was possible — **always include `reason`** |
59
+
60
+ Subtitle-burn emits all five: `tracks_discovered`, `track_selected`, `cues_filtered`,
61
+ `clip_applied`, and `passthrough` at three distinct sites (`no-video-source`,
62
+ `invalid-clip-range`, and the selection `rule`). Note `passthrough` recurs with
63
+ different `reason` values rather than becoming several event names — follow that.
64
+
65
+ ---
66
+
67
+ ## Data emission: dual-channel symmetry
68
+
69
+ When publishing structured data alongside pixels, emit through **both** channels with
70
+ the **same payload shape**:
71
+
72
+ 1. `MosaicDataSource` with `alias: "<name>"` in `doc.sources` — pipeline-readable
73
+ 2. `doc.sidecars.<name>` — disk-readable
74
+
75
+ They serve different audiences (pipeline composition vs. external consumption);
76
+ symmetric semantics let a consumer switch channels without recoding.
77
+
78
+ **Sidecar-only is legitimate** when there is no plausible downstream *pipeline* reader.
79
+ `forensic/watermark/video/v1` is the reference: its `watermark` sidecar is the
80
+ per-render embedding record (payload, ECC params, key, grid, slots, embed α, host
81
+ reference, render dims) that the **decoder** consumes later, out of band. Nothing in
82
+ the render graph wants it, so it declares no `MosaicDataSource`. Don't add one for
83
+ symmetry's sake.
84
+
85
+ ### Opt-in via `emit<Thing>?: boolean`, default `false`
86
+
87
+ Auxiliary data emission is opt-in and free when disabled — no extra sources, no disk
88
+ write. The pixels are the primary output; aux data shouldn't bloat every render.
89
+ Subtitle-burn's `emitCues` (default `false`) is the pattern.
90
+
91
+ Exception: when the sidecar *is* the point (forensic watermark — the render is useless
92
+ without its decode record), make it unconditional and `required: true` in the schema.
93
+
94
+ ### Declare the schemas even when free-form
95
+
96
+ Declare `outputsSchema` (for `MosaicDataSource` shapes) and `sidecarsSchema` (for
97
+ `doc.sidecars` keys) even when entries are `type: "object"` with a prose description.
98
+ They are living documentation that ships with the code, and engine-side validation may
99
+ follow. Write the description as if it's the only thing a consumer will read — for
100
+ sidecars, say **where the file lands** (`{output-basename}.<key>.json`) and **what
101
+ gates it**.
102
+
103
+ ---
104
+
105
+ ## Two habits worth copying
106
+
107
+ **Selection-rule introspection.** A template making an algorithmic choice should return
108
+ a typed `{ result, rule }` from its selector and stamp `rule` into *both* the
109
+ `<thing>_selected` telemetry event and the published data payload. Consumers then
110
+ attribute behavior to an algorithm path without reverse-engineering props.
111
+ Subtitle-burn's rule union: `"explicit-index" | "language-match" | "fallback-default" | …`.
112
+
113
+ **Pass-through graceful degradation.** A template handling optional input (a video that
114
+ may or may not have subtitles) must render the base content cleanly when the optional
115
+ path can't fire — and emit `passthrough` with a `reason` so consumers can see *why*.
116
+ Silence here is the failure mode: an empty overlay and a successful render are
117
+ indistinguishable without the event.
118
+
119
+ ---
120
+
121
+ ## Worked example
122
+
123
+ https://github.com/m0saic-project/m0saic-packages/blob/main/packages/templates/src/m0saic/media/subtitle-burn/v1/subtitle-burn.ts is the canonical
124
+ reference — read it end-to-end before authoring an emission-heavy template. It
125
+ demonstrates every convention above: five telemetry events, dual-channel emission gated
126
+ by `emitCues`, selection-rule stamping, and both schemas declared.
127
+
128
+ **Related:** `data-pipeline.md` — the data-fetcher role's
129
+ `MosaicDataSource` publishing is one specific application of the dual-channel idea here;
130
+ that doc covers the *role* contract, this one covers the *mechanism*.
@@ -0,0 +1,248 @@
1
+ # Geometry recipes — laundering pixel math into composable m0
2
+
3
+ Scope: the placement recipes for templates that follow the Real-Geometry Hard Rule.
4
+ You computed each piece's pixel rect in JS; these recipes turn those rects into m0
5
+ geometry whose precision does NOT track the canvas. Read
6
+ [`construction-strategy.md`](construction-strategy.md) first (the Rect Thesis + Hard
7
+ Rule — why the rects must be real cells at all); the underlying theory (feasibility
8
+ vs precision vs quantization, GCD collapse, §3a/§3c fixes) is
9
+ [`../handbook/feasibility-precision-quantization.md`](../handbook/feasibility-precision-quantization.md).
10
+
11
+ ## Routing — which launder for which shape
12
+
13
+ - Uniform grid of equal cells → **Recipe 3** (gutterless ratio grid + `latticeCellInset`).
14
+ - Bar row/column → Recipe 3 with a non-zero `marginPx` on the layout axis.
15
+ - Independent chrome/text (stat-card bands, KPI blocks, badges) → **Recipe 2**
16
+ (`placeInsetPieces`; `placeOptimizedPieces` when inset-recovery doesn't apply).
17
+ - Tight-mask pieces (animated line/area/points) → **Recipe 1** (`SNAP_PX` snap-outward).
18
+ - Squarified tiling (treemap) → nested ratio splits with gap-as-inset per leaf
19
+ (adjacent tiles share edges — drift opens gaps).
20
+ - Connected/curve/ring geometry → mask-in-a-cell (handbook §3c); legit-absolute ONLY
21
+ when the animation varies the mask PATH per frame — almost nothing does.
22
+ - One must-be-exact element at any resolution → the `placeRect` escape hatch (end of
23
+ this doc).
24
+
25
+ ## Recipe 1 — snap tight-mask cells to a grid (`SNAP_PX`)
26
+
27
+ After computing a masked piece's tight bbox (see the animation-exemption steps in
28
+ `construction-strategy.md`), **snap it OUTWARD to a small px grid (`SNAP_PX`, e.g. 4)
29
+ before you author the mask** — then build the mask sized to the *snapped* cell. Two
30
+ wins from one move:
31
+
32
+ - **Seams disappear.** Independently-quantized tight cells miss each other by a
33
+ sub-pixel (hairline seams between abutting pieces). Snapping every edge to a
34
+ shared grid lands neighbours on the same grid lines, so they meet.
35
+ - **DSL collapses.** Once all band edges are multiples of `SNAP_PX`, `placeRects`
36
+ GCD-reduces by that factor (~`SNAP_PX`× shorter). donut/v3: 27,016 → 6,142 chars
37
+ at `SNAP_PX=4`, ring unchanged.
38
+
39
+ **Mask-safe because the order matters:** snap the cell, THEN author the mask for
40
+ the snapped cell (`bounds` = snapped cell px). The shape path is still generated
41
+ from the true center/radius (exact geometry); only the cell *bounds* snap, so the
42
+ mask never re-scales. This is the build-time, distortion-free analog of the Layout
43
+ editor's "GCD reduce + drift %" compaction — do NOT run that compaction on a
44
+ mask-bearing doc after the fact (it resizes cells the masks were authored against
45
+ → distortion).
46
+
47
+ Pick `SNAP_PX` to taste: ↑ = smaller DSL + coarser edges (more outward drift),
48
+ ↓ = larger DSL + finer. ~4px is a good default. (The line-chart does the
49
+ equivalent via a fixed basis grid: `snapAndPlace` quantizes to `BASIS_COL`×
50
+ `BASIS_ROW` so its data layer shares the chrome's basis.) Pair it with a hair of
51
+ overlap between abutting same-content pieces (and a smaller amount across a color
52
+ boundary) so anti-aliased edges can't reveal the background — snapping aligns
53
+ edges, overlap covers the residual.
54
+
55
+ > Worked examples: `charts/donut/v3/donut.ts` (`SNAP_PX` + `placePiece`);
56
+ > `charts/line-chart/v1/line-series.ts` (`snapAndPlace`). This is how a tight-mask
57
+ > template *makes* its positions GCD-collapsible on purpose — see handbook §3a.
58
+
59
+ **Head-only as written — a FIXED `SNAP_PX` still tracks the canvas.** A fixed
60
+ pitch bounds the CHAR count but not precision: the basis is `regionSide/SNAP_PX`,
61
+ which grows with the canvas (donut/v3 probed ABSOLUTE, slope 1.04, and pinned its
62
+ parent). A **nestable primitive** uses the self-framed version instead:
63
+ canvas-proportional pitch `P = round(min(W,H)/K)`, square frame side
64
+ `S = ceil(regionSide/P)·P` (a `P`-multiple by construction), `placeRects` inside
65
+ the frame, then ratio-letterbox the frame into the canvas (cost: ≤1px uniform
66
+ translation, zero relative drift). Precision bounds at ~`K` on every canvas —
67
+ even coprime dims. `charts/donut/v4` is the exemplar (`ABSOLUTE 1.04 → RATIO ~0`,
68
+ library to 0 audit warnings); full treatment handbook §3c. Also from v4: on a
69
+ light or deeply-nested surface, anti-aliased sliver notches need an **inflated
70
+ seamless base per segment, timed to each segment's completion** — the
71
+ overlap-hair above can't close an AA gap.
72
+
73
+ ## Recipe 2 — independent chrome/text: launder the basis (`placeInsetPieces` / `placeOptimizedPieces`)
74
+
75
+ The `SNAP_PX` recipe above is the by-hand move for masked pieces. For a primitive
76
+ that positions **independent** rects (stat-card chrome, KPI blocks, badges) with
77
+ `placeRects`, use the tool-ified version — and the default tool is the
78
+ **zero-drift** one: hand your `{ rect, source }` pieces to `placeInsetPieces`
79
+ (`@m0saic/template-utils`, https://github.com/m0saic-project/m0saic-packages/blob/main/packages/template-utils/src/layout/placeInsetPieces.ts#L119).
80
+ It quantizes each piece's CELL outward to a divisor
81
+ lattice (precision bounds at ~120, the modern canvas-family gcd) and returns the
82
+ frame-aligned sources with a recovery `placement.inset` wired onto each — every
83
+ element paints on its EXACT computed rect. Why it matters: ONE coprime edge pins
84
+ the whole leaf to ~its render canvas AND back-solves to pin the parent
85
+ (`needs = self × canvas ÷ slot`) — the stat-card rail is what held the pulse
86
+ hero's Safe-Min at 1895×1080. Exemplar: `alpine/stat-card/v1` (migrated
87
+ 2026-07-10 from the drift budget: same `100% → ~17%` collapse, now with zero
88
+ visual drift; hostile prime-dim cells automatically degrade to exact on that
89
+ axis and render fine). Contract: sources must paint nothing outside their inset
90
+ box (transparent margins), and a source may not already carry `placement.inset`
91
+ — bake design insets into the rect instead (it paints exactly, so shrinking it
92
+ is free).
93
+
94
+ Reach for `placeOptimizedPieces` (drift;
95
+ https://github.com/m0saic-project/m0saic-packages/blob/main/packages/template-utils/src/layout/placeOptimizedPieces.ts#L95) instead when
96
+ inset-recovery doesn't
97
+ apply: per-piece LOCKS (`drift: "exact"` on must-stay-exact geometry mixed with
98
+ collapsible chrome), container rects, tiles that paint their whole cell, or
99
+ wanted-snap (pixel-aligned strokes). Scope guard for BOTH: independent elements
100
+ ONLY — a uniform grid wants the ratio-grid recipe (Recipe 3; equal cells snap
101
+ unequal under drift), a tiling shares edges (drift opens gaps), and
102
+ connected/curve geometry wants mask-in-a-cell instead (handbook §3c). Full
103
+ treatment: handbook §3c (fixes three and four).
104
+
105
+ ## Recipe 3 — uniform grids: gutterless ratio grid + gap-as-inset (`grid` + `latticeCellInset`)
106
+
107
+ > **⚠️ Helper changed 2026-07-21.** This recipe's *shape* is unchanged and still
108
+ > correct — gutterless `grid()`, gap in the fiber, zero DSL tokens. But the helper
109
+ > that computes the inset is now **`latticeCellInset`**; **`gridCellInset` is
110
+ > deprecated** (approximate, ±1px gap wobble — see handbook §3c and
111
+ > `HELPER_DEPRECATIONS` in https://github.com/m0saic-project/m0saic-packages/blob/main/packages/template-utils/src/deprecation.ts). Read
112
+ > "Migrating off `gridCellInset`" at the end of this recipe before writing code:
113
+ > the replacement is **not a drop-in rename**, it takes different inputs.
114
+
115
+ **A nestable primitive whose picture is a uniform grid of equal
116
+ cells emits a gutterless `grid()` with the gap as a `latticeCellInset` on each cell
117
+ source — never DSL gutters, never `placeRects` per cell.** The absolute build
118
+ (`placeRects` per cell on a per-pixel snap grid) pins precision to the canvas —
119
+ coprime cell pitches ⇒ `gcd 1` ⇒ ~100% precision (heatmap v1 was a **30k-node
120
+ m0**) — and drift is the WRONG launder here (equal cells snap unequal; a
121
+ quantization-spread guard catches it). The ratio build:
122
+
123
+ 1. **Gutterless `grid({ rows, cols })`** for the cells. No DSL gutters — a gutter
124
+ is a pixel-width band baked into the split, which re-introduces the
125
+ coprime-basis blowup. The gutterless grid's precision is the **cell count**
126
+ (`max(rows, cols)`), constant across every canvas.
127
+ 2. **Gap as a per-cell `placement.inset`** via `latticeCellInset({ rows, cols,
128
+ canvasW, canvasH, gutterXPx, gutterYPx, marginPx, cells })`
129
+ (`@m0saic/template-utils`,
130
+ https://github.com/m0saic-project/m0saic-packages/blob/main/packages/template-utils/src/layout/latticeCellInset.ts#L79) — a render-time
131
+ fractional inset, **zero DSL
132
+ tokens**, so the layout string stays compact. It authors the TARGET painted
133
+ grid first as integer lattice lines (`X_k = round(m + k·(unitW + g))`, each
134
+ boundary rounded independently so drift can't accumulate), then emits each
135
+ inset as `(px + 0.5) / rawSize` so the engine's per-edge `Math.floor`
136
+ recovers the integer target exactly. Result: gutters are **exactly** `g` px
137
+ between every adjacent pair and margins exactly `m`, on every canvas.
138
+ 3. **Chrome as plain ratio splits** (`weightedSplit`; the alpine kit's local
139
+ `rowSplit`/`colSplit` in `alpine/_shared` are sugar over it — not published) —
140
+ row/col labels, legend, letterbox. Center square cells with a null-node
141
+ letterbox (`{ weight: LB, node: EMPTY }`), never absolute offsets.
142
+
143
+ Measured (heatmap v1 → v2, defaultProps): audit `ABSOLUTE slope 1.04 → RATIO
144
+ 0.00`; Safe-Min @1080² `~777×583 → ~120×121`; `~30,316 → ~2,211` m0 nodes (~13×
145
+ leaner); ~5× faster default render; precision sweep clean everywhere. The
146
+ Safe-Min collapse is the composability payoff — an absolute grid back-solves
147
+ (`needs = self × canvas ÷ slot`) to pin its PARENT near its own render canvas;
148
+ the ratio grid asks its parent for almost nothing.
149
+
150
+ Trade-offs to know:
151
+
152
+ - **Full-bleed vs equal cells.** With `latticeCellInset` this is just
153
+ `marginPx`: `marginPx: 0` is full-bleed (gaps only *between* cells),
154
+ `marginPx: g` puts an equal margin around the grid. Unlike the deprecated
155
+ `gridCellInset` — whose full-bleed default left outer cells up to `gapPx/2`
156
+ larger than interior ones (invisible for image/color tiles, **wrong for bars**)
157
+ — the lattice authors every boundary from the same integer line set, so cells
158
+ differ only by ±1px independent-rounding jitter at any `marginPx`.
159
+ - **Check `maxClampPx` / `clampedEdges` on the result.** When the ideal slack
160
+ (≈ `g/cols` px at the extreme interior boundaries) drops below the raw split's
161
+ quantization jitter — tiny canvases with tiny gutters — a target edge escapes
162
+ its raw cell and is **clamped to the cell boundary rather than thrown**,
163
+ narrowing that one gutter. `0` on healthy lattices. A template that cares
164
+ should assert it in its geometry-contract test; degrading to a ≤2px-off gutter
165
+ at hostile canvases beats a dead render, but you want to know.
166
+ - **`skipFirstRowTop`** (a `gridCellInset` knob, for a grid hugging a header
167
+ directly above it) has **no direct `latticeCellInset` equivalent** — express it
168
+ in the lattice instead, by authoring the target rows you actually want.
169
+ - **The gap is not a DSL edge.** Because it's an inset, geometry-mode previews
170
+ and the Render Hero layout viz must read `placement.inset` to show it (they
171
+ do). A consumer reading only the m0 doesn't see the gap — that's the point
172
+ (compactness), but preview code has to opt in.
173
+ - **Values / overlays** ride a *second* gutterless grid of the same shape,
174
+ `overlay`-stacked on the cells — same precision, index-aligned.
175
+
176
+ ### Migrating off `gridCellInset` (not a rename)
177
+
178
+ The two helpers take different inputs, because that's *why* the new one is exact:
179
+
180
+ | | `gridCellInset` (deprecated) | `latticeCellInset` |
181
+ |---|---|---|
182
+ | Sizing basis | the **ideal** cell (`gridW / cols`) | the **raw parsed** cell the engine really produced |
183
+ | Needs | `rows, cols, gridW, gridH, gapPx` | `rows, cols, canvasW, canvasH, gutterXPx, gutterYPx, marginPx, cells[]` |
184
+ | Fractions | plain `n / cell` | half-pixel-centered `(n + 0.5) / rawSize` |
185
+ | Outer margin | `outerMargin: true \| {x,y}` | `marginPx` (integer px) |
186
+ | Returns | inset per cell | `insetAt(i)`, `targets[]`, `columnEdges/rowEdges`, `maxClampPx`, `clampedEdges` |
187
+
188
+ The load-bearing step is supplying `cells[]`: **parse the COMPOSED m0 at the
189
+ render canvas** and hand over each span's `{ unit, raw }`. The raw cell is the
190
+ truth only *after* basis-capped ratio splits, letterbox bands, and engine
191
+ rounding have all happened — a `split()` whose weights sum past 120 RESCALES to
192
+ basis 120, so no up-front pixel math survives. Computing `cells[]` from your own
193
+ intended geometry re-introduces exactly the ideal-vs-quantized bug you're
194
+ migrating away from.
195
+
196
+ Then set `placement.inset = insetAt(i)` per source (skip when it returns
197
+ `undefined` — the raw cell already *is* the target), and assert against
198
+ `targets[]` in the geometry-contract test.
199
+
200
+ > Exemplars: `@m0saic/media/screencap_grid/v2` (gap-carved integer rects → one
201
+ > `placeInsetPieces` call — the pick when the helper should own the whole
202
+ > layout); `@m0saic/alpine/heatmap/v2` (the full data-viz build — cells + value
203
+ > overlay grid + labels + legend — migrated to `latticeCellInset`, with a
204
+ > regression test that replays the engine floor and asserts ONE gap value per
205
+ > axis at both the design canvas and an awkward 1000×700).
206
+ > `@m0saic/media/screencap_grid/v1` is kept registered + `deprecated` as the
207
+ > canonical reference for the ideal-cell-gap pitfall; `@m0saic/alpine/heatmap/v1`
208
+ > likewise for the "absolute grid" anti-example.
209
+
210
+ ## The two floors — and the third question
211
+
212
+ Every m0 string exposes **two floors** — know which one you're hitting:
213
+
214
+ - **Feasibility** (`computeFeasibility` → `minWidthPx/minHeightPx`): the "won't
215
+ error" floor. Below it the layout **errors** (0-size *frame*) — won't render.
216
+ - **Precision** (`M0Precision.maxSplit*`): the "looks right" floor — the smallest
217
+ canvas where every split cell (incl. passthrough/donation cells) is ≥1px.
218
+ **Independent of feasibility, and can be higher OR lower** (gutter grids:
219
+ precision > feasibility; deep nesting: feasibility > precision). In practice
220
+ render at ≥ the per-axis max of both (`evaluateM0`'s `recommendedMin`).
221
+ - Then a THIRD question above both floors: **quantization** — even on a big-enough
222
+ canvas, if the split factor doesn't divide the axis the remainder spreads
223
+ (1996px ÷ 499 → uniform 4px cells; 1920px ÷ 499 → some 3px/some 4px → positions
224
+ shift). Deterministic, **not a bug** — see the handbook.
225
+
226
+ To get the appearance guarantee back: render at quantization-free dimensions
227
+ (`snapGridFit` / `snapGridEnumerate` find the largest clean inner rect), keep
228
+ cell counts modest, or quantize to a bounded basis and **derive dependent
229
+ geometry from the weights** so animated/overlaid layers track whatever spread the
230
+ canvas imposes.
231
+
232
+ Full treatment — feasibility vs precision, the quantization options, and GCD
233
+ collapse — in [`../handbook/feasibility-precision-quantization.md`](../handbook/feasibility-precision-quantization.md).
234
+
235
+ ## Escape hatch — `placeRect` for must-be-exact elements
236
+
237
+ When an icon / chip / fixed-font band must occupy an exact pixel rect *regardless
238
+ of resolution*, position it with `placeRect` (null-tile margins, byte-exact at any
239
+ canvas; https://github.com/m0saic-dsl/m0/blob/main/packages/dsl-stdlib/src/builders/placeRect.ts#L99) — NOT a margin-based
240
+ `insetNode` (an alpine-pack-local helper at
241
+ https://github.com/m0saic-project/m0saic-packages/blob/main/packages/templates/src/m0saic/alpine/_shared/alpine-card.ts#L90, not a published
242
+ API), which turns the margins into split cells and spreads the quantization
243
+ remainder INTO the element (a flaky, resolution-dependent misalignment). See the
244
+ placeRect escape-hatch section in the handbook.
245
+
246
+ Layout-intent verification for all of these recipes — the label-keyed layout
247
+ contract + the three resolve-only audits — lives in
248
+ [`layout-contract.md`](layout-contract.md).
@@ -0,0 +1,168 @@
1
+ # The layout contract — verifying layout intent survived
2
+
3
+ Scope: the opt-in, label-keyed contract layer in `@m0saic/template-utils` that
4
+ checks a template's authored design intent against what the m0 actually resolves
5
+ to at a given canvas. Companion to
6
+ [`construction-strategy.md`](construction-strategy.md) (real-geometry authoring)
7
+ and [`geometry-recipes.md`](geometry-recipes.md) (the placement recipes whose
8
+ output this layer guards).
9
+
10
+ Verified code paths (as of 2026-07-27; re-verify:
11
+ `grep -rn "export function \(withLayoutContract\|checkLayout\|assertLayout\|withGeometryContract\)" https://github.com/m0saic-project/m0saic-packages/blob/main/packages/template-utils/src/geometry-contract):
12
+
13
+ - `withLayoutContract` / `assertLayout` — https://github.com/m0saic-project/m0saic-packages/blob/main/packages/template-utils/src/geometry-contract/withLayoutContract.ts#L65 / `:106`
14
+ - `checkLayout` + `LayoutConstraint` — https://github.com/m0saic-project/m0saic-packages/blob/main/packages/template-utils/src/geometry-contract/layoutConstraint.ts#L215 / `:39`
15
+ - `withGeometryContract` — https://github.com/m0saic-project/m0saic-packages/blob/main/packages/template-utils/src/geometry-contract/withGeometryContract.ts#L69; `GeometryExpectation` — https://github.com/m0saic-project/m0saic-packages/blob/main/packages/template-utils/src/geometry-contract/types.ts#L24
16
+ - The three audits — npm scripts in https://github.com/m0saic-project/m0saic-packages/blob/main/packages/templates/package.json#L13-15` (run from https://github.com/m0saic-project/m0saic-packages/blob/main/packages/templates)
17
+
18
+ ## Why it exists
19
+
20
+ Real-geometry-first gets the rects right in JS pixel math — but the m0 is exact
21
+ yet **disposable**: a template re-addresses its whole geometry tree on any change
22
+ of canvas or props, so stableKeys and tile order are per-string and can't carry
23
+ authored intent. And quantization squashes stay invisible to every
24
+ string-level tool because the STRING is healthy (the stat-card icon rendered
25
+ 33×23 for an intended 26px square; the value band crushed 59→38px; grid/v1
26
+ SILENTLY VANISHED when nested at small canvases). `@m0saic/template-utils`
27
+ closes the loop with a **label-keyed, ratio-based contract** — opt-in per
28
+ template.
29
+
30
+ ## Labels carry intent
31
+
32
+ **Intent lives on a LABEL.** Tag a source (`source.editor.label = "hero"`);
33
+ wherever the builder scatters it in the string, the **source array is the
34
+ invariant** — the engine binds `sources[frame.logicalIndex]`, so a tag ties to
35
+ its exact geometry node. Declare invariants against the label, as
36
+ canvas-INDEPENDENT ratios:
37
+
38
+ ```ts
39
+ withLayoutContract(doc, ctx, {
40
+ templateId, debug: props.debugLayout,
41
+ constraints: [
42
+ { label: "hero", aspect: 16/9, aspectTolerance: 0.04 }, // shape, at ANY canvas
43
+ { label: "value", minHeightFrac: 0.18, minWidthFrac: 0.5 },// ≥18% tall, ≥50% wide
44
+ { label: "header", within: { yFrac: [0, 0.22] } }, // lives in the top 22%
45
+ { label: "grid" }, // PRESENCE — must render at all
46
+ ],
47
+ relations: [
48
+ { label: "photo", equal: "size" }, // uniform thumbnails (one-to-MANY)
49
+ { label: "photo", gutter: { axis: "x", target: 0.02 } }, // even 2%-of-canvas gutters
50
+ ],
51
+ });
52
+ ```
53
+
54
+ - **`checkLayout(doc, { canvasW, canvasH, constraints, relations, flatten? })`**
55
+ is the PURE evaluator — a template (or an agent) loops on it during a **layout
56
+ search**: try a packing via the DSL builders → evaluate → keep the one whose
57
+ labeled invariants hold. `withLayoutContract` is the debug tripwire (falsy
58
+ `debug` → returns `doc` untouched at zero cost; a violation → a
59
+ `LAYOUT_CONTRACT` error mosaic at the canvas that broke + an
60
+ `editor.layoutContract` stamp). `assertLayout` is the throwing CI sibling.
61
+ - **A label is one-to-MANY** — `{ label: "cell", aspect: 1 }` checks EVERY cell,
62
+ so you constrain a KIND, not a node.
63
+ - **Presence, including THROUGH nesting.** A bare `{ label }` asserts the element
64
+ rendered; `checkLayout` auto-flattens nested docs (a parent's top-level m0 is
65
+ trivial), so a parent guards a nested child — and a flatten failure
66
+ (`SPLIT_EXCEEDS_AXIS`) IS the drop signal. This is exactly what would have
67
+ caught grid/v1's silent nested vanish at the canvas it broke (regression test:
68
+ https://github.com/m0saic-project/m0saic-packages/blob/main/packages/templates/src/m0saic/primitives/grid/v1/grid.regression.test.ts).
69
+ - **Running the contract backfills `doc.labels`** (`stableKey → label`) from the
70
+ source tags — labeling and checking are one pass; Make / `.m0c` / diagnostics
71
+ get the human names for free.
72
+ - **You author the label; the per-m0 resolution is OUTPUT.** The result's
73
+ `matched` / `resolved` map each label → the `stableKey` + `sourceIndex`
74
+ (= `logicalIndex`, the `sources[]` index) it landed on THIS render — for tooling
75
+ (Make's jump-to-piece), never for authored intent.
76
+
77
+ ## The three audits (all resolve-only — NO ffmpeg)
78
+
79
+ - `npm run audit:layout-envelope` → `LAYOUT-ENVELOPE.md` — per labeled invariant,
80
+ the **canvas range where it holds vs breaks**, with the boundary
81
+ (`breaks ≤ Npx, holds ≥ Mpx`). The layout-solver report: it hands you the
82
+ feasible envelope, so you fix the geometry math for the range you need or
83
+ declare the template's supported output range.
84
+ - `npm run audit:geometry-matrix` → `GEOMETRY-MATRIX.md` — curated tier sweep
85
+ (modern-core / adjacent / hostile) with §3c routing hints (geometry expectations).
86
+ - `npm run audit:geometry-proof -- --template <id> [--range … | --canvases …]`
87
+ → **sloth** tier (NOT in the merge gate; estimator refuses ≥10min without
88
+ `--yes`), a clustered PROVEN/FAILED report (geometry expectations).
89
+
90
+ ## Two contracts, distinct jobs
91
+
92
+ - **`LayoutConstraint`** (label + ratio) — the authored DESIGN contract; survives
93
+ every m0 the template regenerates. Add a `debugLayout?: boolean` prop (section
94
+ "Debug", deterministic default false).
95
+ - **`GeometryExpectation`** (px rect + inset + maskBounds, zip-ordered) — the
96
+ SECONDARY "did the builder's m0 round-trip" guard, emitted for free by
97
+ `placeInsetPieces` / `placeOptimizedPieces`; `withGeometryContract` +
98
+ `debugGeometry`. Near-tautological for a correct builder — use it to
99
+ regression-lock a zero-drift placement, not to express design intent.
100
+
101
+ ## Text: fit under the ruler the contract measures with
102
+
103
+ The engine's `drawtext` never shrinks a string — `placement.fit:"contain"` only
104
+ picks the anchor. A font derived from a bar's HEIGHT alone clips the moment the
105
+ canvas is narrower than the design aspect (portrait 1080×1920 turned "Animated
106
+ Wireframe" into "Animated"; 4-digit values into two digits — gate 33,
107
+ dsl-tutorial, 2026-09-05). Every text a template paints needs a WIDTH budget.
108
+
109
+ - **`textFits` is the contract side** — `{ label, textFits: { charWidthEm?, padPx? } }`
110
+ (https://github.com/m0saic-project/m0saic-packages/blob/main/packages/template-utils/src/geometry-contract/layoutConstraint.ts#L285; defaults
111
+ `0.72em` / `2px`, measured against the CLI rasterizer) estimates
112
+ `textEmUnits(text) × fontSize × em + pad` (`:71`) against the label's realized box.
113
+ Expr content is skipped by design (a live counter has no single width).
114
+ - **Fit with the SAME ruler.** Size the font from `textEmUnits × em` so the fit and
115
+ the check can't disagree. Per-site em that held at 12 canvases (1920×200 …
116
+ 3840×2160): prose/labels `0.62`, digit-heavy values `0.68`, ALL-CAPS captions
117
+ `0.76` (https://github.com/m0saic-project/m0saic-packages/blob/main/packages/templates/src/m0saic/dsl-tutorial/v1/_shared/text-fit.ts:
118
+ `PROSE_EM` / `DIGIT_EM` / `CAPS_EM`, `fitFontPx`, `fitLine`).
119
+ - **Budget = `cell × 0.94 − 2px`** (`FIT_SLACK`, `FIT_QUANT_PX`). Floor quantization
120
+ hands a leaf 1–2px LESS than its modelled share, per split level, not
121
+ proportionally — a proportional-only slack passed the model and still failed the
122
+ realized box (a 28px chip realized 24px at 800×450). With the budget carrying
123
+ the allowance, declare `textFits.padPx: 0`.
124
+ - **Degrade ladders beat ellipsis at the floor.** Title: shrink to 70% of the design
125
+ size, THEN ellipsize. Pills: full → compact wording (`Speed 1.0x` → `1.0x`).
126
+ Status strips: drop trailing metrics (whole ones). Captions: blank at the floor.
127
+ Tile marks: drop the dims line, keep the number.
128
+ - **Per-glyph-column text (a strip, a ruler) is bounded by its WIDEST glyph**
129
+ (`>` .86em, brackets .42em on the m0 alphabet — `widestM0GlyphEm`), not a flat
130
+ cap; a flat prose em over-measures a comma-heavy m0 string ~1.7×.
131
+ - **Live/expr text can't be contract-checked** — model its WIDEST state
132
+ (`Step N / N`) for the fit and lock it in a unit test.
133
+ - **Chrome that is a fraction of HEIGHT is a portrait bug waiting** — size bars off
134
+ `min(H, 0.75·W)` (landscape byte-identical; portrait gets app-bar chrome) and cap
135
+ row heights to the panel WIDTH in tall side panels.
136
+
137
+ ## `missing-label` is the drop signal, not noise
138
+
139
+ A hairline (a 1-unit rule inside a ~1px/unit band) quantizes to a 0-size frame →
140
+ `SPLIT_EXCEEDS_AXIS` → the CLI refuses the whole render ("expects 39 renderable
141
+ sources but doc.sources has 40" at 480×270), and `flattenMosaicDocument` collapses
142
+ the WHOLE nested layout wherever the child's slot and the flattened parent's cell
143
+ differ by ±1px (700–960px wide for dsl-tutorial). The label-keyed contract reports
144
+ this class as `missing-label` ("layout collapsed … flatten failed") — treat it as
145
+ the geometry bug it is. The sizing rule lives in the handbook
146
+ ([`feasibility-precision-quantization.md`](../handbook/feasibility-precision-quantization.md)
147
+ §3 "Hairlines").
148
+
149
+ ## Recommended: the contract BEFORE the first candidate (founder, 2026-09-05)
150
+
151
+ "The layout contract can be heavily recommended in a template authoring guide due
152
+ to this exact scenario." Default authoring step for any template that paints text:
153
+
154
+ 1. **Tag every fitted text source** (`tag(src, label)`) and declare `textFits` for it
155
+ under the template's `debugLayout` prop — before minting candidate-01.
156
+ 2. **Sweep `assertLayout` at the 7-canvas set in the gate test** — 1920×1080 ·
157
+ 1280×720 · 1080×1920 · 1080×1080 · 3840×2160 · 640×360 · 480×270 — THROUGH
158
+ nested children (`checkLayout` auto-flattens). The end user only ever sees
159
+ "layout contract satisfied"; the failures it takes to get there are the agent's,
160
+ and the sweep is where they surface. Exemplar:
161
+ https://github.com/m0saic-project/m0saic-packages/blob/main/packages/templates/src/m0saic/dsl-tutorial/v1/dsl-tutorial.gate33.test.ts.
162
+ 3. Debug-only, forever (2026-08-22 ruling): a production render never blocks on a
163
+ contract violation — `debugLayout` is the tripwire, the gate test is the lock.
164
+
165
+ Full design: (internal design history). Opt-in —
166
+ trivial-geometry templates gain nothing; don't add ceremony. The positioning
167
+ audit + precision sweep remain the zero-effort baseline every template gets for
168
+ free.