@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,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.
|