@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,329 @@
1
+ # Skill: Template Construction Strategy (Human-Friendly vs Engine-Native)
2
+
3
+ How to decide between human-readable layout and engine-native power, choose splits
4
+ vs overlays, and build templates that are stable, predictable, and extensible.
5
+ Not every valid DSL construct is ideal for every template.
6
+
7
+ > **Source of truth:** Existing templates in [https://github.com/m0saic-project/m0saic-packages/blob/main/packages/templates/src/m0saic](https://github.com/m0saic-project/m0saic-packages/blob/main/packages/templates/src/m0saic) — categories (2026-07-26): `alpine`, `benchmark`, `brand`, `charts`, `code`, `collage`, `demos`, `dsl-tutorial`, `forensic`, `github`, `hero`, `language`, `media`, `meta`, `primitives`, `science`, `social`, `theming`, `wireframe` (`ls` the folder — this list drifts). `charts/bar-graph/v2` is the canonical example of programmatic construction.
8
+
9
+ Companion docs: the placement recipes are
10
+ [`geometry-recipes.md`](geometry-recipes.md); layout-intent verification is
11
+ [`layout-contract.md`](layout-contract.md).
12
+
13
+ ## Two construction styles
14
+
15
+ **Human-friendly layout style** — uses `N(...)` / `N[...]` splits directly,
16
+ `1{...}` or group overlays, avoids complex `0`-run overlay staging, keeps the
17
+ string short and readable:
18
+
19
+ 2(
20
+ 3[1,1,1],
21
+ 3[1,1,1]
22
+ )
23
+
24
+ Ideal for hand-written templates, teaching examples, simple grids, standard UI
25
+ layouts.
26
+
27
+ **Engine-native composition style** — heavy overlay stacking, rect-capture
28
+ overlays, deep nesting, generated programmatically, possibly thousands of
29
+ characters, geometry derived algorithmically. Ideal for dictionary-generated
30
+ layouts, glyph rendering, debug visualizers, signal-driven demos, high-precision
31
+ rectangle emission.
32
+
33
+ Choose engine-native only when demonstrating DSL power, encoding rect sets
34
+ programmatically, building art/glyph systems, or constructing debug tools.
35
+ **"Data visualizations" is NOT a blanket license for overlay-stack style.** A
36
+ chart's chrome (title, axis labels, ticks, legend, plot frame) is *real
37
+ geometry* — see the Hard Rule below. Only the data mark that needs per-frame
38
+ animation (a line draw-on, a count-up) is exempt and lives as an overlay.
39
+
40
+ ## Choosing between splits and overlays
41
+
42
+ Use **splits** when defining structural geometry, siblings need shared partition
43
+ logic, the layout is conceptually hierarchical, or you want stable structural
44
+ identity. Use **overlays** when layering paint, regions are independent, you are
45
+ composing rect instructions, you want depth isolation, or you are building glyphs
46
+ or masks. Splits define space; overlays define drawing.
47
+
48
+ ## Hard rule: real geometry first
49
+
50
+ > **The Rect Thesis.** To render anything at known bounds: carve an m0 CELL sized
51
+ > exactly to those bounds (splits / `weightedSplit` / `placeRects`, or nest a
52
+ > primitive like `@m0saic/primitives/grid` for repeated thin rects), then **fill
53
+ > it** (a lavfi color) or **mask it** (an inline-mask path). The cell is the
54
+ > size+position; the source just fills the cell it lands in. This is the core of
55
+ > m0saic — see [`../m0saic-thesis.md`](../m0saic-thesis.md).
56
+
57
+ **If you know a region's rectangle at author time, encode it as a real m0 rect
58
+ (splits / `weightedSplit` / `placeRects`) — NOT as a full-canvas source
59
+ positioned by `drawtext` `xExpr`/`yExpr` or an inline mask.**
60
+
61
+ A template's job is **JS pixel-math → real rectangles → m0 DSL geometry.** When an
62
+ agent instead computes pixels and bakes them into full-frame drawtext/mask
63
+ sources, the output "renders right" but the document *carries no geometry* — which
64
+ defeats the structure preview, Render Hero (its whole job is to show the `{}`
65
+ frame tree), the layout corpus, and the entire point of the DSL.
66
+
67
+ - Splits / `placeRects` define **WHERE**; the source fills the cell it lands in.
68
+ - Full-frame overlay stacks + in-source positioning are the **FALLBACK**, reserved
69
+ for content that genuinely cannot be a static rect — e.g. an animated line
70
+ sweep, or per-frame-varying geometry.
71
+ - **Smell test:** if your `m0` is `buildOverlayStack(N)` with `N == element
72
+ count` and every source is full-canvas, you have pixel-math baked into
73
+ drawtext, not geometry. Preview + Render Hero will be empty/garbage. Rebuild
74
+ with real cells.
75
+ - **Reuse first-party primitives** for standard chrome — gridlines are
76
+ `@m0saic/primitives/grid/v2` (v1 is DEPRECATED since 2026-07-07: its per-pixel
77
+ basis silently drops when nested — the layout contract's presence check is what
78
+ catches that class of failure, see
79
+ [`layout-contract.md`](layout-contract.md)); do not re-emit gridline rects by
80
+ hand.
81
+
82
+ ### The animation exemption is narrow — even animated pieces are tightly placed
83
+
84
+ The "animated / per-frame-varying" exemption above is **NOT** a license for
85
+ full-canvas overlays. A piece that animates (a line sliver, a marker, an area)
86
+ still has a KNOWN bounding box — you generated its path, so you know its extent.
87
+ A masked source whose `bounds` == the full canvas is the lie: it tells the
88
+ document "this element is everywhere," which wrecks selection/hit-testing (one
89
+ sliver's box swallows every click beneath it), Render Hero, and the layout
90
+ corpus. **Complexity lives in the m0 string (geometry), not hidden inside
91
+ full-frame masks.** Place each animated piece as a tight rect:
92
+
93
+ 1. Compute the piece's bbox (incl. stroke / radius + ~1px anti-alias pad).
94
+ 2. Snap the bbox **OUTWARD** to a bounded basis grid (≤200 cells/split) so the
95
+ cell ⊇ the shape and never clips it.
96
+ 3. `placeRects()` the pieces (input in paint order — greedy first-fit packs them
97
+ into the fewest overlay layers; areas under lines under points).
98
+ 4. Generate the mask path in **cell-local** coords (absolute − cell origin); set
99
+ the mask `bounds` to the cell's px size → `scaleX==scaleY==1`, pixel-exact, no
100
+ clip. Per-piece `overlay.alpha` (`fadeInExpr`) works the same inside a placed
101
+ cell as on a full-frame overlay.
102
+
103
+ > Worked example: `charts/line-chart/v1/line-series.ts` (`placePiece`) — the
104
+ > animated line/area/points place as tight bbox cells (quantized to the same
105
+ > frame basis as the chrome), not full-canvas masked overlays. That template was
106
+ > first authored as ~78 full-frame overlays (one per title/label/gridline)
107
+ > positioned by `xExpr`, then rebuilt as a `weightedSplit` frame with only the
108
+ > animated line left as an overlay. Stat-card (`weightedSplit` bands) is the
109
+ > canonical small example.
110
+
111
+ ### The placement recipes
112
+
113
+ How to make the placed rects composable — precision that does NOT track the
114
+ canvas — is the recipe book at [`geometry-recipes.md`](geometry-recipes.md):
115
+
116
+ - Recipe 1: `SNAP_PX` snap-outward for tight-mask cells (seams + GCD collapse).
117
+ - Recipe 2: `placeInsetPieces` / `placeOptimizedPieces` for independent chrome.
118
+ - Recipe 3: gutterless ratio grid + `latticeCellInset` for uniform grids
119
+ (incl. the `gridCellInset` → `latticeCellInset` migration).
120
+ - Plus: the routing table, the feasibility/precision/quantization floors, and the
121
+ `placeRect` escape hatch for must-be-exact elements.
122
+ - Recipe 4 (the lattice, `@m0saic/template-utils` `lattice/`): weighted bands go
123
+ through `weightedSplit(latticeWeights(weights, { cap: 120 }), axis, { claimants })`
124
+ — `latticeWeights` judges the GCD-reduced total, never emits more slots than
125
+ the input had, keeps symmetry (`[pad, mid, pad]`) and alternation
126
+ (items/gutters), bounds drift to max(2 %, 2 slots, 1 slot of the target) and
127
+ otherwise returns the input untouched so the convention reports it. **Never
128
+ round each weight to a cap on its own** — the sum lands on cap ± (bands − 1),
129
+ which is how the whole Alpine pack sat on 121. When an intent must register
130
+ with the cell a split will actually produce (a chip inset, a tick font cap, a
131
+ nested child's slot), predict it with `quantizedSections(totalPx, weights)` —
132
+ the parser's outside-in remainder, an EDGE section absorbs it first — and hand
133
+ children 5-smooth slots (`nearestSmooth`, `snapSlot`, or the hero's `insetSlot`
134
+ pattern that nudges the rect until the predicted cell is smooth). Self-framed
135
+ soups take the pitch from `divisors(side)` nearest `side/K` (`ceilToSmooth`
136
+ fallback on a rough side), never `round(side/K)` + `ceil`. The rule behind it
137
+ (`latticeSmooth`, throw) and the `lattice` declarations:
138
+ [`reference/template-flags.md`](reference/template-flags.md).
139
+
140
+ ### Don't size/position with `size` expressions — carve the cell
141
+
142
+ To make an element small/short (a tick, a rule, a swatch, a badge), DON'T keep
143
+ the source full-tile and shrink it with `fitMode:"content"` + `size:{wExpr,hExpr}`
144
+ + `placement:{fit:"contain"}`. `fit` is only `contain`/`cover` — both **scale**:
145
+ a source small in BOTH axes upscales to fill its tile (blocks / distortion). It
146
+ only "stays small" when one dimension is already full (so a full-height 1px rule
147
+ via `wExpr:"max(1,TW*f)", hExpr:"TH"` is the one acceptable size-expr case).
148
+
149
+ **Carve the cell instead.** Geometry decides size+position; the source fills.
150
+
151
+ - **Smell test:** writing `size:{wExpr,hExpr}` to make something SMALL in both
152
+ axes → you're scaling, not placing. Carve a rect.
153
+ - Worked example: line-chart **tick marks**. First attempt sized stubs with
154
+ size-exprs → rendered as full-cell blocks (contain upscaled them). Fix: carve a
155
+ narrow RAIL cell beside the labels and nest a short `grid` — its thin
156
+ line-cells ARE the tick rects, exact and aligned. (`chrome.ts`)
157
+
158
+ ### Solid-colour tile sources — use `makeColorTile`
159
+
160
+ `@m0saic/template-utils` exports `makeColorTile(color, opts?)` as the shared factory for "paint this cell with a solid colour" sources (https://github.com/m0saic-project/m0saic-packages/blob/main/packages/template-utils/src/sources/makeColorTile.ts#L52). Adoption is now broad — `brand/logo/v1`/`v2`/`v3`, the whole alpine pack (via `alpine/_shared/alpine-card.ts`), and ~20 other templates use it. New templates should follow the same convention so the engine pipeline stays uniform. (Grep `makeColorTile` in https://github.com/m0saic-project/m0saic-packages/blob/main/packages/templates/src for the live consumer list.)
161
+
162
+ ```ts
163
+ import { makeColorTile } from "@m0saic/template-utils";
164
+
165
+ makeColorTile("#050314");
166
+ makeColorTile(color, { overlay: { alpha, enable, xExpr, yExpr, startAtSec, blendMode } });
167
+ makeColorTile(color, { mask });
168
+ makeColorTile(color, { overlay: { enable }, mask });
169
+ ```
170
+
171
+ Returns a `MosaicLavfiSource` using ffmpeg's `color=` generator. Prefer this over hand-rolling a `MosaicTextSource` with empty literal text + `visual.backgroundColor`:
172
+
173
+ - `color=` is essentially free per cell — no rasterized intermediate to share, so N independent tiles cost no more than one shared source with N refs would (and refs to lavfi at nested depth aren't wired in v1 anyway — see `rendering-model-contract.md` Rule 9's ref-source note).
174
+ - Lavfi sources are valid `MosaicRefSource` targets at root depth without needing the `renderMode: { kind: "image" }` escape hatch.
175
+ - Single API across templates — no per-template variants of `makeSolidColorTile` to drift.
176
+
177
+ Verify the export with: `grep -n "export function makeColorTile" https://github.com/m0saic-project/m0saic-packages/blob/main/packages/template-utils/src/sources/makeColorTile.ts.
178
+
179
+ ### Drawing shapes: SVG inline-mask on a **color** source
180
+
181
+ The Rect Thesis above says a cell is either *filled* or *masked*. This is how to mask
182
+ one — arrows, triangles, badge silhouettes, any non-rect shape.
183
+
184
+ **Two wrong reaches, in order of temptation:**
185
+
186
+ 1. **A drawtext glyph** (`▲` / `▼` / an emoji). ffmpeg silently drops these as tofu.
187
+ Nothing errors; the shape is just gone.
188
+ 2. **A `text` source with a `backgroundColor` rect + an inline-mask.** This *works*,
189
+ but routes a shape through the text path and distorts easily.
190
+
191
+ **Do this instead** — a lavfi colour source (via `makeColorTile` above), masked by
192
+ an SVG path:
193
+
194
+ ```ts
195
+ makeColorTile(color, { mask: { kind: "inline-mask", localPath, bounds } })
196
+ ```
197
+
198
+ No drawtext involvement, and it renders in **both** the engine and the Make preview
199
+ (LavfiTile applies masks). m0 controls the rect; SVG draws the shape into it. ffmpeg is
200
+ excellent at *composition* and the wrong tool for *drawing* — SVG is the specialist.
201
+
202
+ #### ⚠️ The load-bearing rule: `bounds` aspect MUST match the cell's aspect
203
+
204
+ The engine scales the authored path to the tile:
205
+
206
+ ```
207
+ scaleX = tileW / bounds.width
208
+ scaleY = tileH / bounds.height
209
+ ```
210
+
211
+ Those are computed **independently**. If `bounds` aspect ≠ tile aspect the scale is
212
+ **anisotropic** and the shape distorts — a right angle stops being a right angle. Author
213
+ the path in the cell's actual pixel space (or any matching ratio) so `scaleX === scaleY`.
214
+ Get that right and the shape is undistorted *and* crisp at any resolution: sharp
215
+ rasterizes at tile res, so it stays vector-clean.
216
+
217
+ This one rule is the whole difference between a crisp triangle and a smeared one, and
218
+ it fails silently — the render succeeds, it just looks wrong.
219
+
220
+ #### Two supporting habits
221
+
222
+ - **Keep the rect's split coarse.** Sizing the shape's cell with pixel-exact
223
+ `weightedSplit` weights can push the literal split past ~200 cells and hit
224
+ engine split-rounding. Percent-ish weights are enough — the mask does the precision.
225
+ - **Author variants in the template, select by prop.** Up / down / neutral arrows are
226
+ three authored paths; the template picks one from a `direction` prop. Don't compute
227
+ path geometry at render time.
228
+
229
+ #### Check the shared helpers before authoring a path
230
+
231
+ `@m0saic/dsl-stdlib` ships a small shape library at
232
+ https://github.com/m0saic-dsl/m0/blob/main/packages/dsl-stdlib/src/libraries/mask-shapes — each returns a ready `M0cMaskEntry`
233
+ (path + correctly-authored `bounds`, so the aspect rule above is handled for you):
234
+
235
+ | Helper | Signature |
236
+ |---|---|
237
+ | `circleMask` | `(width, height = width)` |
238
+ | `roundedRectMask` | `(width, height, radius…)` |
239
+ | `pillMask` | `(width, height)` |
240
+
241
+ **Prefer these over a hand-authored path** — they're where the bounds discipline lives.
242
+
243
+ There is **no `triangleMask`** yet; `charts/stat-card/v1` authors its arrow paths inline
244
+ and leaves a `// (Future: a triangleMask helper in dsl-stdlib/mask-shapes.)` marker at
245
+ `stat-card.ts:300`. If you need a second triangle, add the helper rather than
246
+ copy-pasting the path — that's the signal the library is missing one.
247
+
248
+ ### Canvas / card backgrounds: prefer `document.backgroundColor`
249
+
250
+ For a full-frame background fill, set `backgroundColor` on the returned
251
+ `MosaicDocument` — **don't** add a dedicated base layer via a `1{...}` overlay wrapper.
252
+
253
+ A base-layer overlay makes the doc **two layers** (L0 = surface, L1 = content). The
254
+ engine renders that correctly, but the Make-page preview then has to composite the
255
+ upper layer over the lower one, and **that compositing is best-effort** — it can show
256
+ black, or a stray cell background, where the lower layer should show through the
257
+ upper's transparent areas. The render is right and the preview lies, which is the worst
258
+ failure mode: the preview is the surface you and the human review on.
259
+
260
+ `document.backgroundColor` keeps the card a **single DSL layer**. The engine fills the
261
+ canvas wherever sources don't cover; the preview paints the same colour straight onto
262
+ `.gp-canvas` with no layer compositing. Render and preview agree, and the m0 is lighter
263
+ (no wrapper, no base tile source).
264
+
265
+ **Reach for a base tile only when `backgroundColor` can't express it** — rounded
266
+ corners, a stroke, per-region fills. It's a flat rect fill, nothing more.
267
+
268
+ **Two adjacent things, easily confused:**
269
+
270
+ - **`doc.backgroundImage`** (`MosaicBackgroundImage`) is the sibling for a *baked image*
271
+ background — bottom layer, beneath every source, **on top of** `backgroundColor`.
272
+ Carries `opacity` (ghost a reference at `0.3`), `fit` (default `"cover"`;
273
+ `"contain"` letterboxes against `backgroundColor`), and `focusX`/`focusY` crop
274
+ anchors with the same semantics as `placement.focusX`. This is the mechanism behind
275
+ the agent protocol's reference-background bake — build chrome on top of a reference
276
+ render, section by section.
277
+ - **`source.visual.backgroundColor`** is a *different field on a different object* — it
278
+ fills one source's tile, not the canvas. Same name, unrelated scope. Check which one
279
+ you're reading.
280
+
281
+ ### Text needs a WIDTH, chrome needs `min(H, 0.75·W)`
282
+
283
+ `drawtext` never shrinks a string and `fit:"contain"` only picks the anchor, so a
284
+ font sized from a bar's HEIGHT clips on any canvas narrower than the design
285
+ aspect. Fit every text under `textEmUnits(text) × fontSize × em` with a budget of
286
+ `cell × 0.94 − 2px` (quantization takes 1–2px per split level), prefer a degrade
287
+ ladder (shrink → compact wording → drop) over ellipsis at the floor, and size
288
+ chrome bars off `min(H, 0.75·W)` rather than a fraction of H (a 173px header slab
289
+ on 1080×1920 otherwise). The full rule set, em table, and the 7-canvas gate sweep:
290
+ [`layout-contract.md`](layout-contract.md) §"Text".
291
+
292
+ ### Verifying layout intent survived
293
+
294
+ The m0 is exact but disposable — stableKeys and tile order are per-string and
295
+ can't carry authored intent, and quantization squashes are invisible to
296
+ string-level tools. The label-keyed layout contract (`withLayoutContract` /
297
+ `checkLayout` / `assertLayout`) plus the three resolve-only audits close that
298
+ loop: [`layout-contract.md`](layout-contract.md).
299
+
300
+ ## Avoid overengineering
301
+
302
+ Just because the DSL allows `0{...}`, deep overlay stacks, logical owners `-{}`,
303
+ and nested containers everywhere does not mean every template should use them.
304
+ Prefer the simplest construct that expresses the layout clearly.
305
+
306
+ Weighted splits: prefer small basis values (10, 20, 32, 64); avoid extreme counts
307
+ unless intentional; ensure the render resolution supports the split granularity;
308
+ remember feasibility constraints. Only use `0`-runs for precise weighted
309
+ geometry, advanced prefix-overlay patterns, or algorithmic generation — avoid
310
+ them for everyday layouts.
311
+
312
+ A good template has predictable geometry, works at intended resolutions, uses
313
+ overlays intentionally, avoids fragile structural side-effects and unnecessary
314
+ deep nesting — and is deterministic, has safe defaults, and passes validation
315
+ cleanly.
316
+
317
+ ## Checklist
318
+
319
+ When designing a template:
320
+
321
+ - [ ] Do I know each region's rect at author time? If so, is it a REAL m0 cell (not a full-frame `xExpr`/mask overlay)? (See the Hard Rule.)
322
+ - [ ] Is this primarily structural layout or paint layering?
323
+ - [ ] Could this be simpler with direct splits?
324
+ - [ ] Am I using overlays intentionally?
325
+ - [ ] Is the string readable if humans will maintain it?
326
+ - [ ] Is this dictionary-level generation where readability does not matter?
327
+ - [ ] Does it validate and remain feasible at target resolution?
328
+ - [ ] Is every split count above 12 5-smooth (2ᵃ3ᵇ5ᶜ) — basis capped with `latticeWeights` / `precision`, never per-weight rounding? The gate (`latticeSmooth`, throw) and `m0saic doctor` will refuse it otherwise; a raster declares `lattice: { mode: "bitmap" }`, a content count `lattice.allow` with its reason.
329
+ - [ ] Did I route through the right placement recipe ([`geometry-recipes.md`](geometry-recipes.md)) instead of hand-rolling pixel math?
@@ -0,0 +1,324 @@
1
+ # Data pipeline: fetchers, adapters, handles, and wiring
2
+
3
+ The chain: a FETCHER template publishes data → the resolver threads it across
4
+ pipeline steps / sibling tiles → a CONSUMER template reads it from ctx → a cron
5
+ Job (or the CLI / Make page) renders the whole thing from a `.mosaicx`.
6
+ Copy-adaptable reference pair: `@m0saic/meta/fixture-fetcher/v1` +
7
+ `@m0saic/meta/upstream-echo/v1` (see Reference implementations).
8
+
9
+ ## The three patterns
10
+
11
+ All three ride the runtime upstream collection (`collectUpstream`,
12
+ the render engine source (not published) — pure, sync; the runner invokes it
13
+ per step transition before the step's `render`):
14
+
15
+ - **Data-fetcher** — a step-0 template whose render returns a doc publishing
16
+ structured data via `MosaicDataSource`; downstream reads `ctx.upstreamData[alias]`
17
+ / `ctx.upstreamVariables`.
18
+ - **Adapter** — a small pure template that reads upstream data, transforms it,
19
+ and re-publishes under a different alias. Cross-pack composition.
20
+ - **Handle** — a producer self-stamps `{ stepIndex, flattenedStableKey }` into
21
+ its variables; a downstream `MosaicRefSource` spreads the handle to mirror the
22
+ producer's rendered pixels across the step boundary — no re-render, no re-encode.
23
+
24
+ Fetchers/adapters carry *data* across the back-edge; handles carry *pixel pointers*.
25
+
26
+ ## Publish channels × read views
27
+
28
+ A producer publishes through three channels on its returned doc:
29
+
30
+ | # | Channel | Flat view | Namespaced view | Publication entry |
31
+ |---|---|---|---|---|
32
+ | 1 | Doc-level `doc.variables` | ✅ merges | — | — (no provenance) |
33
+ | 2 | `MosaicDataSource` WITHOUT `alias` | ✅ merges | — | ✅ |
34
+ | 3 | `MosaicDataSource` WITH `alias` | ✅ merges | ✅ `upstreamData[alias]` | ✅ |
35
+
36
+ Plus one authored (not template-published) source: `pipeline.variables`, the
37
+ pipeline-level seed — flat view only, visible from step 0 onward. (Data flow
38
+ WITHIN one template is plain function returns; the ctx views below exist for
39
+ crossing a step/tile boundary.)
40
+
41
+ - **`upstreamVariables` — the global bank.** Flat last-write-wins union of everything
42
+ above. Grounded example: THEMING — a theme step publishes tokens once
43
+ (`{ chromeDark: "#111", accent: "#e33", … }`); every downstream template reads
44
+ `ctx.upstreamVariables.chromeDark`; swap the one producer (or the seed) to re-skin.
45
+ - **`upstreamData` — alias-namespaced blocks.** Populated ONLY by aliased
46
+ `MosaicDataSource`s. The consumer must know the namespace; that's the point — it's
47
+ a contract. The alias stays FIXED across versions (part of `outputsSchema`);
48
+ varying data goes in the payload, never in the key. Consumers declare needs in
49
+ `upstreamDataSchema`, so drift is a resolve-time error and keys can't shadow.
50
+ - **`upstreamPublications` — the provenance escape hatch.** Lossless ordered list,
51
+ one entry per data source, stamped with `stepIndex`, `tileStableKey`, and
52
+ `templateId`. Same-alias colliders BOTH survive here. Same back-edge scope as the
53
+ merged views, just unmerged — not a wider window; doc-level `variables` mint no
54
+ publication. Use for geometry-addressed reads
55
+ (`publications.find(p => p.tileStableKey === "r/fc0")`) when "which tile said
56
+ this" is the real question; otherwise prefer the alias. Rule of thumb: alias
57
+ anything another pack might read; doc-level variables are fine for single-producer
58
+ flows, themes, and handles; publications are the audit channel, not the read path.
59
+
60
+ ## Author a fetcher (producer)
61
+
62
+ - `defineMosaicTemplate` with `capabilities: { tier: "capability", caps: {…} }`.
63
+ Without that tier the Level 1 gate STRIPS `ctx.secrets` / `ctx.connections` before
64
+ your render runs — long the #1 silent failure. It is no longer silent: a core-tier
65
+ template that READS a handle the host actually supplied gets a `console.warn`
66
+ naming the template, the field and the declaration that would grant it, plus a
67
+ `warn`-level `capability_denied_read` template log on `ctx.telemetry`. The read
68
+ still returns `undefined` and the ctx key set is unchanged — the tripwire is a
69
+ non-enumerable getter, so only a real read trips it. Both handles are withheld
70
+ together, which is the tell: `ctx` keys `[analysis, cache, media, mode, output,
71
+ target, telemetry]` with `connections`/`secrets` absent means TIER, not a
72
+ missing host connection. (Raw template objects that skip `defineMosaicTemplate`
73
+ are not gated — don't rely on that.) Pure-derivation fetchers (data from props
74
+ alone) can stay `core`.
75
+ - The render return, sketched (the load-bearing fields):
76
+
77
+ // m0: "1" — one cell, for the carrier
78
+ // sources: [ { type: "lavfi", color: "#000000" }, // carrier
79
+ // { type: "data", alias: asAliasId("athleteData"),
80
+ // variables: { name, country, stats } } ] // the publish
81
+ // sidecars: { athleteData: data } // disk mirror
82
+
83
+ - **Include a 1-cell renderable carrier** (the lavfi above). A strictly data-only
84
+ doc cannot be planned when a `.mosaicx` wrapper's cell references it; inline
85
+ data-only step files DO work (the planner skips them), but the carrier makes the
86
+ fetcher safe in both shapes. Data sources occupy no m0 cell — the planner filters
87
+ them from layout — so the m0 counts renderables only.
88
+ - Secrets: `ctx.secrets.get("env:NAME")` (CLI/jobs resolve env vars; desktop
89
+ also resolves `keychain:` refs). NEVER put cleartext in variables/sidecars —
90
+ publish derived facts only. Connections: `ctx.connections.get("publisher@profile")`
91
+ (CLI needs `M0SAIC_CONNECTIONS_FILE=<path to connections.json>`).
92
+ - Declare `outputsSchema` — all-optional keys unless you can always deliver;
93
+ required keys are validated post-render (VARIABLES_SCHEMA_MISMATCH, error).
94
+ Mirror the payload to `sidecars.<key>` — lands as `<output>.<key>.json` next
95
+ to every render, your byte-assertable proof. Opt out only for sensitive data.
96
+ - Naming: announce the role in the id — `<scope>/<pack>/<name>-fetcher/v1` (or
97
+ `/intel/`, `/prologue/`); `role: "data-fetcher"` is a real `TEMPLATE_ROLES`
98
+ value (the opt-in, warning-only `TEMPLATE_ROLE_NAMING_MISMATCH` pins the hint).
99
+
100
+ ## Author a consumer
101
+
102
+ - Core tier is fine — consumers of capability-produced data stay core; the data
103
+ is opaque input, so the consumer stays deterministic given it.
104
+ - Read via the `@m0saic/template-utils` upstream helpers — NOT raw ctx fields
105
+ (optional + generic-typed; they decay into casts). Full set at
106
+ https://github.com/m0saic-project/m0saic-packages/blob/main/packages/template-utils/src/data/upstream.ts#L36-135` (verified 2026-07-27):
107
+ `hasUpstream`, `listUpstreamAliases`, `hasUpstreamBlock`, `getUpstreamBlock`,
108
+ `requireUpstreamBlock`, `getUpstreamVariables`, `getUpstreamVariable`,
109
+ `getUpstreamPublications`, `findUpstreamPublications`.
110
+
111
+ if (!hasUpstream(ctx)) return renderMissingCard(); // wired at all?
112
+ const athlete = requireUpstreamBlock(ctx, "athleteData"); // throws w/ available aliases
113
+ const soft = getUpstreamBlock(ctx, "designTokens"); // undefined when absent
114
+ const accent = getUpstreamVariable<string>(ctx, "accent") ?? "#e33"; // flat union
115
+ listUpstreamAliases(ctx) // sorted, for diagnostics
116
+ findUpstreamPublications(ctx, { tileStableKey: "r/fc0" }) // provenance escape hatch
117
+
118
+ Helpers never transform values, and "no upstream threaded" stays
119
+ distinguishable from "threaded but empty" (`undefined` vs `{}`).
120
+ - Declare `upstreamDataSchema` / `upstreamVariablesSchema` — required keys make
121
+ mis-wiring a resolve-time ERROR. Standalone renders (no resolver) skip the
122
+ check, so always handle absent upstream with a deterministic, VISIBLE missing
123
+ state (à la upstream-echo's red card); `requireUpstreamBlock` is the fail-fast
124
+ complement for consumers meaningless without their producer.
125
+
126
+ ## Adapter (re-emission)
127
+
128
+ Sketch — core tier, `role: "adapter"` (id-hint `adapter`|`bridge`):
129
+
130
+ // render(_props, ctx):
131
+ // const upstream = ctx.upstreamData?.teamAData;
132
+ // return doc with sources: [{ type: "data", alias: asAliasId("teamBData"),
133
+ // variables: { athlete: { name: upstream?.hero?.fullName, … } } }]
134
+
135
+ Why the template layer, not the type layer: the impulse is to let `MosaicRefSource`
136
+ target a `MosaicDataSource` (ref relays data). We deliberately don't — a ref carries
137
+ pixel-affecting props (`placement`, `effects`, `mask`, …) that would be silently
138
+ meaningless on a data target, and a render() can transform / filter / merge far
139
+ beyond static renames. Enforced structurally: data sources occupy no m0 cell, so a
140
+ ref at a data source's "slot" surfaces `MOSAIC_REF_NOT_FOUND`
141
+ (`MOSAIC_DATA_SOURCE_VISIBLE_USE` stays registered in `@m0saic/types` for ABI
142
+ stability but is emitted from nowhere). Write an adapter for: cross-pack renames,
143
+ subset filtering, merging two aliased sources, computed fields. Writing the same
144
+ one-key rename a fifth time is the signal to propose a re-emission variant.
145
+
146
+ ## Handle (cross-step pixel reuse)
147
+
148
+ Sketch — producer stamps a self-describing pointer; consumer spreads it into a ref:
149
+
150
+ // producer (owns the geometry, so the key is correct by construction):
151
+ // variables: { introHero: { stepIndex: ctx.pipelineStep!.index,
152
+ // flattenedStableKey: "intro_hero" } }
153
+ // consumer:
154
+ // sources: [{ type: "ref", ...ctx.upstreamVariables!.introHero,
155
+ // placement: { fit: "cover" }, playback: { loopMode: "freeze" } }]
156
+
157
+ The producer self-stamps (never the consumer hardcoding) because only the producer
158
+ knows its `stepIndex` and cell stableKeys at author time — v2 can rename
159
+ "intro_hero" without breaking consumers. Channel choice: doc-level `variables`
160
+ (flat — single-producer flows) or an aliased `MosaicDataSource` (namespace-safe —
161
+ consumer reads `ctx.upstreamData.introHero`); both spread into the ref identically.
162
+ Data sources sit beside renderables without occupying cells, so ONE producer can
163
+ publish pixels + a data block + a handle in one doc.
164
+
165
+ Resolution matrix (engine behavior, 3c.4):
166
+
167
+ | # | Locality | Size | Duration | Behavior |
168
+ |---|---|---|---|---|
169
+ | 1 | Same doc | match | match | Precursor reuse, identity. Ref's `placement` applies. |
170
+ | 2 | Same doc | differ | match | Precursor reuse + `placement.fit` + offset. |
171
+ | 3a | Cross-step | match | match | Precursor reuse across step boundary; `stepIndex` disambiguates. |
172
+ | 3b | Cross-step | differ | match | Precursor reuse + `placement.fit`; target in earlier step. |
173
+ | 3c | Cross-step | (either) | differ | `playback.loopMode` (`"loop"`/`"freeze"`/`"cut"`) + `clipStartMs`/`clipDurationMs`. |
174
+
175
+ All five reuse the producer's intermediate precursor file. Each consumer's own
176
+ `placement` / `playback` / `effects` / `mask` / `visual` / `audio` / `overlay`
177
+ decorate the mirrored stream independently — N consumers, N decoration chains,
178
+ one decode. Cross-step pixel-format / fps / alpha bridging is engine-internal
179
+ (the ref consumer's chain inserts `scale=` / `fps=` / `setpts=` / `format=` as
180
+ needed — https://github.com/m0saic-project/m0saic-packages/blob/main/packages/types/src/source/source.ts#L1198-1201`).
181
+ Handle vs fetcher + re-render: handle when you want **the same rendered pixels**
182
+ back (cheapest, frame-identical); fetcher + downstream re-render when you want
183
+ **the same logical content rendered differently** per step. They compose.
184
+
185
+ ## Wire the pipeline (.mosaicx)
186
+
187
+ TWO equivalent shapes. Both resolve to the same `mosaic_pipeline`; the fixtures
188
+ render byte-identical mp4s + sidecars, and the e2e suite gates that.
189
+
190
+ **Flat (preferred for new chains)** —
191
+ the CLI source (not published):
192
+ `kind: "mosaicx_pipeline"` at the root, steps each wrap ONE invocation. No `m0`,
193
+ no `sources`, no `children` — a pipeline root carries no layout of its own.
194
+ Stamp `size`/`fps`/`durationMs` on the pipeline (the self-describing-artifact
195
+ rule) where the wrapped form stamps them on the wrapper document.
196
+
197
+ **Wrapped (legacy; still fully supported)** —
198
+ the CLI source (not published):
199
+ mosaicx_document → single `{type:"mosaic", ref}` source → `mosaic_pipeline` child
200
+ whose steps each wrap ONE invocation. The resolver PROMOTES the pipeline child to
201
+ the top-level renderable.
202
+
203
+ Either way the steps read:
204
+
205
+ step 0: fetcher invocation, "intermediate": true ← required; its carrier
206
+ renders to workspace but never reaches the deliverable
207
+ step 1+: consumer invocations — each sees (earlier steps ∪ earlier
208
+ siblings); strict back-edge, no forward references
209
+
210
+ The wrapped form is promoted to the top level automatically; the flat form is
211
+ already there. Invocation ids differ between the two (they are slugs of the path
212
+ into the source tree, so the wrapper contributes a `children.<ref>.` segment) —
213
+ internal handles only, not deliverable names.
214
+
215
+ Cross-TILE also works without a pipeline: an earlier sibling invocation's
216
+ published data reaches later siblings in the same doc.
217
+
218
+ The flat root is what makes a chain visible to every surface that iterates a
219
+ **top-level** pipeline's steps. As of 2026-08-14 the CLI (`m0saic make`,
220
+ `m0saic resolve`) accepts both roots; Compose rejects `mosaicx_pipeline` at load
221
+ and `m0saic open --make` can't open one (it needs a top-level invocation).
222
+ `MosaicXPipeline` carries `runner` but deliberately NOT `vocab` / `agent` /
223
+ `suggestedOutput` — those are read only by surfaces that require a top-level
224
+ `template_invocation` source.
225
+
226
+ ## Run it
227
+
228
+ - CLI: `M0SAIC_FIXTURE_SECRET=… m0saic make thing.mosaicx -w 640 -h 360 -o out.mp4`
229
+ (env vars ARE the CLI secret store; sidecars land next to out.mp4)
230
+ - Jobs: input kind `"mosaicx-file"` (UI infers it from the extension). Cron jobs
231
+ auto-suffix colliding outputs; `validateJob` is strict — unknown template ids
232
+ / schema drift FAIL validation.
233
+ - Make page: open the .mosaicx and render — same lowering.
234
+ - Smoke: `npm run smoke:test-templates -w @m0saic/cli -- fixture-pipeline`
235
+
236
+ ### Jobs window tokens (host-injected time)
237
+
238
+ A cron/scheduled Job interpolates window tokens into invocation props at TRIGGER
239
+ time, before `resolveMosaicx` runs — the template still receives a plain explicit
240
+ `{startISO,endISO}` window and stays clock-free. Token set (UTC): `{{todayISO}}`,
241
+ `{{lastFullWeekStartISO}}`, `{{lastFullWeekEndISO}}` — the last COMPLETE Mon..Sun
242
+ week strictly before the trigger instant. Exact-substring replacement inside
243
+ string values only.
244
+
245
+ - the Mosaic Desktop / Web app source (not published) — `windowTokenMap(triggerInstant)`
246
+ (reuses `lastFullWeek` from the github pack's week-math) +
247
+ `interpolateWindowTokens` (nested string walk).
248
+ - `runJob.js` threads the trigger instant and interpolates BOTH a `.mosaicx` doc
249
+ (before resolve) and template `mergedProps` (before render).
250
+ - `validateJob.js` mirrors it; unrecognized `{{…}}` → `UNKNOWN_WINDOW_TOKEN` warning
251
+ (never an error; unknown tokens pass through verbatim).
252
+
253
+ Timezone: v1 is UTC. The cron `tz` schedules WHEN a job fires but does NOT shift
254
+ the tokens — a weekly Monday job always renders the just-completed week.
255
+ Coupling note: `windowTokens.js` imports week math from a template pack; the
256
+ cleaner long-term home is `@m0saic/platform` — flagged for a future hoist.
257
+
258
+ ## Diagnostics you'll meet
259
+
260
+ | Code | Means | Fix |
261
+ |---|---|---|
262
+ | VARIABLES_SCHEMA_MISMATCH (error) | upstream payload ≠ declared schema, or producer under-delivered outputsSchema | fix the wiring/alias/types |
263
+ | PIPELINE_DATA_ONLY_STEP_NOT_INTERMEDIATE (error) | data-only step not marked intermediate | add `"intermediate": true` |
264
+ | MOSAIC_DATA_SOURCE_OUTSIDE_PIPELINE (warning) | data source on a non-pipeline root — nobody can read it | move it into a step / invocation child |
265
+ | VARIABLES_NOT_YET_IMPLEMENTED (warning) | raw authored pipeline sets step variables; only the resolver can thread | run it via .mosaicx invocations |
266
+ | MOSAICX_RESOLVE_FAILED (jobs validate, error) | an invocation failed (unknown id / render threw) | fix the template id / props |
267
+
268
+ ## The github/ connector pack (the shipped exemplar)
269
+
270
+ https://github.com/m0saic-project/m0saic-packages/blob/main/packages/templates/src/m0saic/github (verified 2026-07-27): `connection.ts`
271
+ (registerHostConnection "github@default" + /rate_limit probe), `client.ts`
272
+ (injectable-fetch, Link pagination, 202-retry, per-run call budget, committer-date
273
+ windowing), `week-math.ts` (pure ISO-week / Mon-start helpers),
274
+ `repo-facts-fetcher/v1` (capability, publishes `githubRepoFacts`),
275
+ `weekly-pulse-adapter/v1` (core, PURE, publishes `weeklyPulse`), `weekly-pulse/v1`
276
+ (self-contained app-runnable capability consumer — the clock-rule exception below),
277
+ `__fixtures__/` (a frozen real week + record-facts.mjs). No generic connector
278
+ substrate until connector #2.
279
+
280
+ **Coverage record (never silently zero).** A fetcher hitting a rate-limited /
281
+ partial API publishes an explicit `coverage` block (authenticated?,
282
+ commitDetails full/partial/none, which KPIs are real; every cap in
283
+ `coverage.truncated`). The adapter selects KPIs ADAPTIVELY from it — a mirror
284
+ repo drops PR/issue KPIs instead of emitting zeros; degraded basis is LABELED.
285
+
286
+ **The clock rule (hosts inject time).** Templates NEVER read the wall clock;
287
+ identical facts replay → byte-identical publish; all tests run replay or stubbed
288
+ fetch; CI never touches the network. Scheduling is the host's job (window tokens
289
+ above). One documented exception: an app-runnable capability template's live-mode
290
+ default window (`capability-templates.md` §"App-runnable capability templates").
291
+
292
+ **Secret handling.** Token → request headers ONLY. `coverage.authenticated` is the
293
+ only signal a token was used; the cleartext never enters variables, sidecars,
294
+ diagnostics, errors, or logs. tokenRef resolution order: `props.tokenRef` (when it
295
+ resolves) → connection keychain via `mintConnectionSecretRef` → anonymous.
296
+
297
+ ## Forward references
298
+
299
+ - **TemplateRole** (layer 1 shipped) — `data-fetcher` and `adapter` are real
300
+ `TEMPLATE_ROLES` values (https://github.com/m0saic-project/m0saic-packages/blob/main/packages/types/src/template/template.ts#L1169);
301
+ `handle-producer` is NOT a role. Render-return-shape enforcement (3j) parked.
302
+ - **Schema validation** (Phase 3h) — once template generics (`<P, O, U, D, S>`)
303
+ thread end-to-end, producer/consumer mismatches become type-level errors.
304
+ - **Cross-step mirror render** (Phase 3c.4 — ✅ **SHIPPED**, re-verified
305
+ 2026-07-27) — the handle pattern's ref consumer mirrors precursor pixels across
306
+ step boundaries for real: 20 `ref-*` capability fixtures exercise it in the
307
+ always-on E2E gate (the CLI source (not published)), and
308
+ the render engine source (not published) asserts
309
+ `MOSAIC_REF_NOT_YET_IMPLEMENTED` is NOT emitted. **Do not treat the handle
310
+ pattern as non-functional — it is one of the best-tested surfaces in the
311
+ repo.** (Fetchers/adapters produce no pixels — no visual-golden mandate.)
312
+ - **Sibling docs** — `recursion-nested-rendering.md`: `renderNestedTemplate` (how
313
+ a parent invokes producer/fetcher templates); `philosophy-and-contract.md`: the
314
+ `capabilities.tier` contract; `emission-patterns.md`: the sidecar/telemetry
315
+ *mechanism* the fetcher role applies.
316
+
317
+ ## Reference implementations
318
+
319
+ - Producer: https://github.com/m0saic-project/m0saic-packages/blob/main/packages/templates/src/m0saic/meta/fixture-fetcher/v1
320
+ - Consumer: https://github.com/m0saic-project/m0saic-packages/blob/main/packages/templates/src/m0saic/meta/upstream-echo/v1 (self-evidencing green/red status card)
321
+ - Wiring (flat root): the CLI source (not published)
322
+ - Wiring (wrapped root): the CLI source (not published)
323
+ — the same chain; the e2e matrix asserts both produce identical deliverables
324
+ - Headless jobs usage: the Mosaic Desktop / Web app source (not published) (stub pattern + cron-style run)