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