@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,392 @@
|
|
|
1
|
+
# Rendering-model contract
|
|
2
|
+
|
|
3
|
+
**Anchor doc for the 12-rule contract spanning `MosaicDocument`, `MosaicDocumentPipeline`, `MosaicOutputEncode`, `MosaicEngineContext`, the engine planner, and the CLI.**
|
|
4
|
+
|
|
5
|
+
Verified against (at promotion time, 2026-05-15):
|
|
6
|
+
- https://github.com/m0saic-project/m0saic-packages/blob/main/packages/types/src/document/document.ts (MosaicDocument)
|
|
7
|
+
- https://github.com/m0saic-project/m0saic-packages/blob/main/packages/types/src/document/document-pipeline.ts (MosaicDocumentPipeline, PipelineStepBase)
|
|
8
|
+
- https://github.com/m0saic-project/m0saic-packages/blob/main/packages/types/src/output/encode.ts (MosaicOutputEncode)
|
|
9
|
+
- https://github.com/m0saic-project/m0saic-packages/blob/main/packages/types/src/output/format.ts / `audio-config.ts` / `color-config.ts` / `container-metadata.ts` / `target.ts` (sub-field types)
|
|
10
|
+
- https://github.com/m0saic-project/m0saic-packages/blob/main/packages/types/src/engine-context/engine-context.ts (MosaicEngineContext)
|
|
11
|
+
- https://github.com/m0saic-project/m0saic-packages/blob/main/packages/types/src/document/pipeline-helpers.ts (helpers + predicates)
|
|
12
|
+
- the render engine source (not published) (planner)
|
|
13
|
+
- the render engine source (not published) (output-format precedence chain)
|
|
14
|
+
- the render engine source (not published) (ctx assembly)
|
|
15
|
+
- the render engine source (not published) (multi-encode transcode passes)
|
|
16
|
+
- the render engine source (not published) (workspace + multi-output handling)
|
|
17
|
+
- the CLI source (not published) (capability fixtures gating each rule)
|
|
18
|
+
- the CLI source (not published) (E2E gate)
|
|
19
|
+
- the render engine source (not published) (visual gate)
|
|
20
|
+
|
|
21
|
+
## Core architectural insights
|
|
22
|
+
|
|
23
|
+
Two facts drive the whole contract:
|
|
24
|
+
|
|
25
|
+
1. **A `MosaicDocument` has exactly one `m0` string → exactly one geometry.** Width, height, fps, durationMs, target preset, format, audio, color, metadata, backgroundColor are properties of the *one render* the doc describes. A doc cannot have "multiple outputs at different geometries"; that's a contradiction with one `m0`.
|
|
26
|
+
|
|
27
|
+
2. **A `MosaicDocumentPipeline` is a sequence of `MosaicDocument`s, each with their own `m0`.** Multi-geometry is therefore a natural property of pipelines (not just of `emit:"multi"`). Different steps can have different canvases; in `emit:"single"` the engine concatenates them onto one pipeline-level canvas (letterbox / element-wise-max default), in `emit:"multi"` each step emits its own file at its own canvas.
|
|
28
|
+
|
|
29
|
+
The shape of the types reflects this directly: output knobs are flattened onto Document and Pipeline (no map of "named outputs"), and multi-encode is a separate top-level field.
|
|
30
|
+
|
|
31
|
+
## Shape
|
|
32
|
+
|
|
33
|
+
```ts
|
|
34
|
+
type MosaicDocument = {
|
|
35
|
+
kind: "mosaic_document";
|
|
36
|
+
version: 1;
|
|
37
|
+
m0: M0String;
|
|
38
|
+
assets: MosaicAssetManifest;
|
|
39
|
+
sources: MosaicSource[];
|
|
40
|
+
children?: Record<string, MosaicRenderable>;
|
|
41
|
+
meta?: MosaicFileMeta;
|
|
42
|
+
labels?: ...; // out of rendering-model scope
|
|
43
|
+
variables?: ...; sidecars?: ...;
|
|
44
|
+
editor?: ...; engine?: ...;
|
|
45
|
+
created?: string; app?: ...; appVersion?: ...;
|
|
46
|
+
|
|
47
|
+
// -- Rendering-model output knobs (one config per doc) --
|
|
48
|
+
size?: { width: number; height: number };
|
|
49
|
+
fps?: number;
|
|
50
|
+
durationMs?: number;
|
|
51
|
+
target?: MosaicOutputTarget;
|
|
52
|
+
format?: MosaicOutputFormat;
|
|
53
|
+
audio?: MosaicAudioConfig;
|
|
54
|
+
color?: MosaicColorConfig;
|
|
55
|
+
metadata?: MosaicContainerMetadata;
|
|
56
|
+
backgroundColor?: MosaicColor;
|
|
57
|
+
|
|
58
|
+
// -- Multi-deliverable transcode pass (post-render) --
|
|
59
|
+
encodes?: Record<string, MosaicOutputEncode>;
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
type MosaicDocumentPipeline = {
|
|
63
|
+
kind: "mosaic_pipeline";
|
|
64
|
+
version: 1;
|
|
65
|
+
steps: MosaicPipelineStep[];
|
|
66
|
+
defaultTransition?: MosaicPipelineTransition;
|
|
67
|
+
meta?: ...; created?: ...; app?: ...; appVersion?: ...;
|
|
68
|
+
variables?: ...; sidecars?: ...;
|
|
69
|
+
editor?: ...; engine?: ...;
|
|
70
|
+
|
|
71
|
+
// -- Rendering-model output knobs (one config per pipeline) --
|
|
72
|
+
size?: { width: number; height: number };
|
|
73
|
+
fps?: number;
|
|
74
|
+
durationMs?: number;
|
|
75
|
+
target?: MosaicOutputTarget;
|
|
76
|
+
format?: MosaicOutputFormat;
|
|
77
|
+
audio?: MosaicAudioConfig;
|
|
78
|
+
color?: MosaicColorConfig;
|
|
79
|
+
metadata?: MosaicContainerMetadata;
|
|
80
|
+
backgroundColor?: MosaicColor;
|
|
81
|
+
|
|
82
|
+
// -- Pipeline-specific --
|
|
83
|
+
emit?: "single" | "multi"; // single = concat to 1 file; multi = N files
|
|
84
|
+
|
|
85
|
+
encodes?: Record<string, MosaicOutputEncode>;
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
type MosaicOutputEncode = {
|
|
89
|
+
size?: { width: number; height: number }; // optional ffmpeg `scale` (stretches)
|
|
90
|
+
format?: MosaicOutputFormat;
|
|
91
|
+
audio?: MosaicAudioConfig;
|
|
92
|
+
color?: MosaicColorConfig;
|
|
93
|
+
metadata?: MosaicContainerMetadata;
|
|
94
|
+
// NOT settable: fps, durationMs, target, emit (inherited from master)
|
|
95
|
+
};
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
The `outputs: Record<string, MosaicOutput>` map and the `MosaicOutput` type are **gone**.
|
|
99
|
+
|
|
100
|
+
---
|
|
101
|
+
|
|
102
|
+
## Rule 1 — Top-level return types
|
|
103
|
+
|
|
104
|
+
A template returns exactly one of:
|
|
105
|
+
|
|
106
|
+
MosaicDocument | MosaicDocumentPipeline
|
|
107
|
+
|
|
108
|
+
- `MosaicDocument` → always exactly 1 master render at 1 geometry.
|
|
109
|
+
- `MosaicDocumentPipeline` → a sequence of documents, possibly with different geometries.
|
|
110
|
+
|
|
111
|
+
Runtime discrimination: `isPipelineFile()` / `isDocumentFile()`.
|
|
112
|
+
|
|
113
|
+
## Rule 2 — Pipeline emit mode
|
|
114
|
+
|
|
115
|
+
A pipeline declares `emit?: "single" | "multi"` (default `"single"`).
|
|
116
|
+
|
|
117
|
+
- `"single"` → render every step, concatenate the output-steps into 1 video. Step geometries reconcile to the pipeline's canvas (explicit `pipeline.size`, or element-wise max of step canvases).
|
|
118
|
+
- `"multi"` → render every step, emit each output-step as its own file at its own step's geometry. **Top-level only**: nested pipelines silently downgrade to `"single"` (`PIPELINE_EMIT_MULTI_DOWNGRADED`).
|
|
119
|
+
|
|
120
|
+
The relevant fan-out test:
|
|
121
|
+
|
|
122
|
+
isMultiOutputCapable = pipeline is top-level
|
|
123
|
+
&& emit === "multi"
|
|
124
|
+
&& outputStepCount > 1
|
|
125
|
+
|
|
126
|
+
## Rule 3 — Step model
|
|
127
|
+
|
|
128
|
+
Each pipeline step:
|
|
129
|
+
|
|
130
|
+
- has `durationMs: number` — exact rendered length.
|
|
131
|
+
- may carry `intermediate?: boolean` (default `false`).
|
|
132
|
+
- may carry `name?: string` — friendly-slug used for filename basis under `emit:"multi"` and for CLI per-step addressing. Falls back to `step-{index}`.
|
|
133
|
+
- may carry `label?: string` — template-author hint for `{{label}}` token in the CLI's `--output-pattern` (e.g. the user-supplied input filename when a template fans 1→N variants).
|
|
134
|
+
|
|
135
|
+
Derived:
|
|
136
|
+
|
|
137
|
+
outputSteps = steps.filter(s => !s.intermediate)
|
|
138
|
+
outputCount = outputSteps.length
|
|
139
|
+
|
|
140
|
+
At least one step must be non-intermediate (`PIPELINE_NO_OUTPUT_STEPS` otherwise).
|
|
141
|
+
|
|
142
|
+
## Rule 4 — Pipeline execution
|
|
143
|
+
|
|
144
|
+
**Top-level pipeline:**
|
|
145
|
+
- Renders all steps (intermediates included — they back ref-sources).
|
|
146
|
+
- `emit:"single"` → concat output-steps; pipeline-level output knobs govern the concat canvas + final format.
|
|
147
|
+
- `emit:"multi"` → each output-step emits its own file at its own step's canvas. Pipeline-level knobs are mostly inert for canvas/fps/duration (steps win); pipeline-level `encodes` apply to **each** emitted file.
|
|
148
|
+
|
|
149
|
+
Each step renders at its own declared `step.file.size`; the pipeline-level / CLI `-w/-h` target is the default canvas, used only when a step doesn't declare its own (`buildMosaicPlanFromFile.ts` step loop).
|
|
150
|
+
|
|
151
|
+
**Per-cell tap intermediates (Phase 3c.4).** When a `media`-source cell is the target of one or more `MosaicRefSource`s, the planner emits an additional per-cell `.mov` render (the "tap") via `tapNodeIdFor(stepIndex, flattenedStableKey)`. The tap captures that cell's post-decoration, pre-composite pixel stream; ref consumers (same-doc and cross-step) read from it. Cells that aren't ref targets are unaffected — the tap is opt-in via a pre-pass (`collectRefTargets`) that scans the whole renderable once before plan-build. `text` and `mosaic` ref targets reuse their existing per-cell intermediates; `lavfi` ref targets re-emit the same lavfi expression at the consumer (hash-deduped by graph string).
|
|
152
|
+
|
|
153
|
+
**Nested pipeline** (appears in a parent's `children`):
|
|
154
|
+
- Must produce exactly one clip for the parent slot's effective duration `T`.
|
|
155
|
+
- Engine sums step durations to meet `T`; trims last step; falls back to embedding source's `MosaicPlaybackProps.loopMode` (`"loop"` / `"freeze"` / `"cut"`) when the pipeline is shorter.
|
|
156
|
+
- Per-step canvases are overridden under nesting; parent slot's canvas wins (`PIPELINE_NESTED_CANVAS_COLLAPSED`).
|
|
157
|
+
- `emit:"multi"` silently downgrades to `"single"`.
|
|
158
|
+
|
|
159
|
+
## Rule 5 — Output resolution (single render)
|
|
160
|
+
|
|
161
|
+
Applies to:
|
|
162
|
+
- `MosaicDocument` (always — a doc is always one render).
|
|
163
|
+
- `MosaicDocumentPipeline` with `emit:"single"` or `outputCount === 1` (the pipeline produces one concatenated file).
|
|
164
|
+
|
|
165
|
+
Each output field resolves via a **3-tier chain** (highest first):
|
|
166
|
+
|
|
167
|
+
1. User intent (CLI flags + .m0v defaults, assembled by the CLI)
|
|
168
|
+
2. Template intent (the fields set on doc / pipeline by render())
|
|
169
|
+
3. Engine default (target preset → engine hardcoded defaults)
|
|
170
|
+
|
|
171
|
+
`.m0v` is CLI-consumed default user input — not part of the template/engine contract. Templates never read `.m0v` files. The CLI loads `.m0v` (when supplied) and uses its named output presets as the User tier's default layer; CLI flags override `.m0v` defaults within the same tier; the merged result is the User tier for the document/pipeline.
|
|
172
|
+
|
|
173
|
+
### Rule 5b — Size off `ctx.target`, never `ctx.output`
|
|
174
|
+
|
|
175
|
+
**`ctx.target.{width,height}` is THIS template's canvas. `ctx.output` is the top-level
|
|
176
|
+
render envelope.** They are not the same thing once a template is nested.
|
|
177
|
+
|
|
178
|
+
| | `ctx.target` | `ctx.output` |
|
|
179
|
+
|---|---|---|
|
|
180
|
+
| Means | the canvas *this* template is drawing into | the root render's envelope (codec / container / pixel format + root dims) |
|
|
181
|
+
| Top level | identical to `ctx.output` | identical to `ctx.target` |
|
|
182
|
+
| Nested via `renderNestedTemplate(id, props, ctx, { slot })` | **the slot** (e.g. 640×216) | **still the parent root** (e.g. 1920×1080) |
|
|
183
|
+
|
|
184
|
+
`renderNestedTemplate` deliberately overrides **only** `ctx.target`
|
|
185
|
+
(https://github.com/m0saic-project/m0saic-packages/blob/main/packages/template-utils/src/render/renderNestedTemplate.ts — `fps` and `durationMs`
|
|
186
|
+
default to the parent's target too). Time already comes from `ctx.target.durationMs` by
|
|
187
|
+
convention; **size must come from `ctx.target` for the same reason.**
|
|
188
|
+
|
|
189
|
+
**Use `ctx.target` for:**
|
|
190
|
+
- font sizes — `Math.round(ctx.target.height * FRAC)`
|
|
191
|
+
- px → fraction conversions
|
|
192
|
+
- `makeErrorMosaic` / `makeStubMosaic` dimensions
|
|
193
|
+
|
|
194
|
+
**Use `ctx.output` only for** format / codec / alpha decisions — never geometry.
|
|
195
|
+
|
|
196
|
+
#### ⚠️ Why this hides
|
|
197
|
+
|
|
198
|
+
At top level `ctx.target === ctx.output`, so a template that reads `ctx.output` looks
|
|
199
|
+
completely correct — every standalone render, every preview, every golden. The bug
|
|
200
|
+
appears **only** when someone composes it: the child sizes to the parent's canvas and
|
|
201
|
+
blows out of its cell. `stat-card` rendered ~5× too large this way.
|
|
202
|
+
|
|
203
|
+
That means **a passing test suite is not evidence.** The check is a grep, not a render:
|
|
204
|
+
|
|
205
|
+
```
|
|
206
|
+
grep -rn "ctx.output.width\|ctx.output.height" https://github.com/m0saic-project/m0saic-packages/blob/main/packages/templates/src
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
Every hit is either an error-card dimension (should be `ctx.target`) or geometry (must
|
|
210
|
+
be `ctx.target`). As of 2026-07-26 this still returns hits across ~23 template files —
|
|
211
|
+
including `charts/donut/v4`, which is `primitive: true` (i.e. explicitly meant to be
|
|
212
|
+
composed) and sets `const W = ctx.output.width` for its entire layout. Latent today
|
|
213
|
+
because nothing nests it in-repo; a live 5× bug the moment something does.
|
|
214
|
+
|
|
215
|
+
## Rule 6 — Multi-output (pipelines only)
|
|
216
|
+
|
|
217
|
+
Multi-output happens only when a top-level pipeline has `emit:"multi"` and more than one output-step. There is no multi-output for individual documents (one m0 = one geometry).
|
|
218
|
+
|
|
219
|
+
Rules:
|
|
220
|
+
- The template authors a pipeline with N output-steps; each step is its own `MosaicDocument` with its own geometry.
|
|
221
|
+
- The engine emits N files, named by `step.name` (or `step-{index}` fallback): `{base}-{stepName}.{ext}`.
|
|
222
|
+
- Pipeline-level `encodes` apply to each emitted file (each file → N transcoded variants).
|
|
223
|
+
- User intent (`ctx.userIntent.outputs`) is surfaced to templates that want to map presets to specific steps (rule 8).
|
|
224
|
+
|
|
225
|
+
## Rule 7 — Exact per-output override
|
|
226
|
+
|
|
227
|
+
Special case for pipeline `emit:"multi"`:
|
|
228
|
+
|
|
229
|
+
if (Object.keys(userIntent.outputs).length === outputStepCount)
|
|
230
|
+
|
|
231
|
+
Then user overrides are applied **positionally** to each output-step (joined by index, not name). This overrides whatever the template authored on each step's MosaicDocument output fields. The CLI also emits an `OUTPUT_NAME_NOT_FOUND` warning per stale key when the user's keys drift from the template's step names — the override still fires (because count matched), but the warning surfaces the drift.
|
|
232
|
+
|
|
233
|
+
## Rule 8 — Preset semantics
|
|
234
|
+
|
|
235
|
+
The user can supply (via CLI flags directly, or via `.m0v` defaults the CLI loads):
|
|
236
|
+
- A default output config.
|
|
237
|
+
- Named presets (e.g. `"desktop"`, `"mobile"`, `"alpha-master"`).
|
|
238
|
+
|
|
239
|
+
Behavior:
|
|
240
|
+
- **Single render (document, or pipeline emit:single)** → presets resolve directly through the 3-tier chain (rule 5). Only one preset's worth of fields is actually applied.
|
|
241
|
+
- **Multi render (pipeline emit:multi)** → presets are input to the template via `ctx.userIntent.outputs`. The template decides how to map preset names onto its own step `name`s. The exact-override path (rule 7) is the only hard-override.
|
|
242
|
+
|
|
243
|
+
## Rule 9 — Intermediate steps + back-edge refs
|
|
244
|
+
|
|
245
|
+
Steps with `intermediate: true`:
|
|
246
|
+
- DO render. The engine produces a real video file in `workspaceDir`.
|
|
247
|
+
- The workspace file is deleted at the end of the render job — not delivered to the user.
|
|
248
|
+
- ARE available to later steps via `MosaicRefSource` (back-edge invariant; see below).
|
|
249
|
+
- Do NOT count toward `outputCount`.
|
|
250
|
+
- Are NOT concatenated under `emit:"single"`.
|
|
251
|
+
- Are NOT exposed as per-file under `emit:"multi"`.
|
|
252
|
+
- Their `transitionToNext` is ignored.
|
|
253
|
+
|
|
254
|
+
**`MosaicRefSource` scope (post-3c.4).** Refs are back-edge mirrors of another cell's pixels. They work in both directions intermediate steps unlock — but the surface is broader than pipelines:
|
|
255
|
+
|
|
256
|
+
- **Locality**: same-document (`stepIndex` omitted) AND cross-step (`stepIndex` set; must be strictly earlier).
|
|
257
|
+
- **Targets**: any cell that produces pixels — `media`, `text`, `mosaic`, top-level same-doc `lavfi`, and overlay frames (`r/.../ovNcN`). Inner cells of nested mosaics resolve at any depth via `flattenedStableKey` (e.g. `r/gcolc0/fc1`, `r/growc0/fc0`).
|
|
258
|
+
- **Not targets**: `data` sources (structurally hidden from layout), other ref sources (chains — editor flattens them; engine rejects with `MOSAIC_REF_TARGET_NOT_SUPPORTED`), cross-step lavfi (deferred — same code emits `MOSAIC_REF_TARGET_NOT_SUPPORTED` and falls back to placeholder).
|
|
259
|
+
- **Matrix normalization**: when the ref's slot dims / fps / duration / pixfmt differ from the target's, the consumer's own filter chain inserts `scale=` / `fps=` / `setpts=` / `format=` to bridge. Ref's own `placement` / `playback` / `effects` / `mask` then apply on top.
|
|
260
|
+
|
|
261
|
+
Use case: a "header strip" rendered once into workspace, referenced from multiple downstream steps for pixel reuse. Or: a hero clip rendered in step 0 echoed at a different size in step 3 with audio sync preserved. Or: a same-doc overlay frame reused as the visual content of another cell.
|
|
262
|
+
|
|
263
|
+
### Note — `MosaicRefSource` lavfi-target gap (v1)
|
|
264
|
+
|
|
265
|
+
Lavfi ref targets have a narrower v1 surface than the other source kinds: they resolve only when the target cell is at the root of the same doc (`descendPath.length === 0`). Refs to lavfi cells at deeper nesting silently fall back to black placeholders with a `MOSAIC_REF_TARGET_NOT_SUPPORTED` warning — only surfaced in `--validate-only` or `--report` output. Media / mosaic / text-with-image-render-mode targets all walk nested depth fine (see the `ref-same-doc-multi-level-inner-cell` and `ref-cross-step-nested-inner-cell` fixtures).
|
|
266
|
+
|
|
267
|
+
For "one solid colour reused across many cells", prefer N independent `MosaicLavfiSource` entries — ffmpeg's `color=` generator is essentially free, so the ref optimization has no payoff there. Reserve refs for targets with a real intermediate to share (media files, text-as-image PNGs, nested mosaics).
|
|
268
|
+
|
|
269
|
+
If a use case for nested-lavfi refs surfaces (e.g. mirroring a complex procedural gradient across deep cells), the gate at the render engine source (not published) is where the resolver is deferred. Verify with: `grep -nE "descendPath\.length > 0" the render engine source (not published).
|
|
270
|
+
|
|
271
|
+
### Solid-colour tile sources — use `makeColorTile`
|
|
272
|
+
|
|
273
|
+
Full section (API forms, why lavfi `color=` beats an empty text source, adoption list) moved to
|
|
274
|
+
[`construction-strategy.md`](construction-strategy.md) § "Solid-colour tile sources — use `makeColorTile`". Source: https://github.com/m0saic-project/m0saic-packages/blob/main/packages/template-utils/src/sources/makeColorTile.ts#L52.
|
|
275
|
+
|
|
276
|
+
## Rule 10 — Single vs multi render
|
|
277
|
+
|
|
278
|
+
| Mode | Source | User authority |
|
|
279
|
+
|---------|------------------------------------------------------------------|----------------|
|
|
280
|
+
| Single | `MosaicDocument`, or pipeline with `emit:"single"` / 1 output | Full (rule 5) |
|
|
281
|
+
| Multi | Pipeline with top-level `emit:"multi"` and `outputCount > 1` | Template-driven (rule 6); user only via rule 7 |
|
|
282
|
+
|
|
283
|
+
## Rule 11 — Multi-encode (separate axis)
|
|
284
|
+
|
|
285
|
+
`encodes?: Record<string, MosaicOutputEncode>` on `MosaicDocument` and `MosaicDocumentPipeline` declares N transcoded variants of the master render(s).
|
|
286
|
+
|
|
287
|
+
1 render → 1 master + N encodes
|
|
288
|
+
|
|
289
|
+
Engine implementation: master renders into a workspace `{base}.__master.{ext}` (never the user-facing path, so encodes can stream-copy/transcode from it), then post-process ffmpeg passes `ffmpeg -i master.<ext> ...` produce each encode entry. `runPlanInWorkspace` skips its "final command" sweep when `multiOutput: true`, so every command writes to its own `outputPath` hint instead of getting rerouted to the user-facing path.
|
|
290
|
+
|
|
291
|
+
### What an encode CAN change
|
|
292
|
+
- Codec, container, pixel format, encoder tuning (the primary axis).
|
|
293
|
+
- Width/height (optional `scale` filter pass — **stretches** content; no aspect-aware padding or re-layout).
|
|
294
|
+
- Audio, color tagging, container metadata.
|
|
295
|
+
|
|
296
|
+
### What an encode CANNOT change
|
|
297
|
+
- `fps`, `durationMs` (inherited from master).
|
|
298
|
+
- The render's `target` preset or `emit` mode.
|
|
299
|
+
|
|
300
|
+
### Pipeline + encodes
|
|
301
|
+
- `emit:"single"` → `encodes` apply to the one concat output (one master, N encodes).
|
|
302
|
+
- `emit:"multi"` → `encodes` apply to **each** emitted file (each of N step outputs gets N encodes — `outputCount × encodeCount` files total).
|
|
303
|
+
|
|
304
|
+
### Container/codec compatibility defaults
|
|
305
|
+
- WebM rejects AAC; when no audio override is supplied and the encode targets WebM, `buildEncodeCommand` defaults to `-c:a libopus` (master AAC can't be stream-copied into WebM).
|
|
306
|
+
- `matroska` / `quicktime` / `mpegts` are ffmpeg muxer names; `containerToExt` aliases them to `.mkv` / `.mov` / `.ts` so the writer emits valid file extensions.
|
|
307
|
+
|
|
308
|
+
## Rule 12 — Template / engine / CLI responsibilities
|
|
309
|
+
|
|
310
|
+
**Templates:**
|
|
311
|
+
- Author one `MosaicDocument` or one `MosaicDocumentPipeline` per `render()` call.
|
|
312
|
+
- Set output knobs (`size`, `fps`, etc.) on the doc/pipeline as **template intent** (tier 2 of rule 5).
|
|
313
|
+
- For multi-output (pipeline emit:multi) authors: read `ctx.userIntent.outputs` and map preset names onto step `name`s.
|
|
314
|
+
- Never read `.m0v` files directly.
|
|
315
|
+
|
|
316
|
+
**Engine:**
|
|
317
|
+
- Resolves output knobs per rule 5 (User > Template > Engine default).
|
|
318
|
+
- Branches on `emit` at top level: concat for single, fan-out for multi.
|
|
319
|
+
- Filters `intermediate` steps from output count / concat / per-file emission.
|
|
320
|
+
- Honors the exact-override path (rule 7) when triggered.
|
|
321
|
+
- Issues post-render transcode passes per `encodes` (rule 11).
|
|
322
|
+
- Concat path always emits audio (silent AAC track when sources are image-only) so downstream encodes carry an audio stream regardless of source mediaType.
|
|
323
|
+
|
|
324
|
+
**CLI:**
|
|
325
|
+
- Loads `.m0v` (when supplied) as default user input.
|
|
326
|
+
- Merges CLI flags on top (CLI flags > `.m0v` defaults within the User tier).
|
|
327
|
+
- Assembles `ctx.userIntent` and bakes user values into the doc/pipeline before plan-build.
|
|
328
|
+
|
|
329
|
+
---
|
|
330
|
+
|
|
331
|
+
## Per-rule wiring status (2026-05-15, end of Phase 5)
|
|
332
|
+
|
|
333
|
+
| # | Rule | Typed? | Wired? | Tested? | Notes |
|
|
334
|
+
|---|----------------------------|--------|--------|---------|--------------------------------------------------------------------|
|
|
335
|
+
| 1 | Top-level return types | ✓ | ✓ | ✓ | Document/Pipeline dispatch wired in `buildMosaicPlanFromFile.ts`. |
|
|
336
|
+
| 2 | Pipeline emit single/multi | ✓ | ✓ | ✓ | `multi-output-minimal` fixture; `test-templates.e2e.test.js`. |
|
|
337
|
+
| 3 | Step model (durationMs, intermediate, name, label) | ✓ | ✓ | ✓ | label honored in `--output-pattern`'s `{{label}}` token. |
|
|
338
|
+
| 4 | Pipeline execution | ✓ | ✓ | ✓ | Per-step `size` honored; nested-downgrade emits diagnostic. |
|
|
339
|
+
| 5 | Output resolution (single) | ✓ | ✓ | ✓ | 3-tier chain in `resolveOutputFormat.ts`. |
|
|
340
|
+
| 6 | Multi-output (pipeline emit:multi) | ✓ | ✓ | ✓ | Fan-out + workspace→user-facing copy in CLI. |
|
|
341
|
+
| 7 | Exact per-output override | ✓ | ✓ | ✓ | Length-match triggers positional merge; warns on key drift. |
|
|
342
|
+
| 8 | Preset semantics | ✓ | ✓ | ✓ | CLI `--m0v` + flag merge; `ctx.userIntent.outputs` surfaced. |
|
|
343
|
+
| 9 | Intermediate steps | ✓ | ✓ | ✓ | Filter in step loop; verified absent from user-facing output. |
|
|
344
|
+
| 10 | Single vs multi distinction | ✓ | ✓ | ✓ | Follows from rules 6+7. |
|
|
345
|
+
| 11 | Multi-encode (separate axis) | ✓ | ✓ | ✓ | Transcode chain + matroska alias + WebM audio default. |
|
|
346
|
+
| 12 | Template / engine / CLI responsibilities | ✓ | ✓ | ✓ | `ctx.userIntent` wired through `createEngineContext`. |
|
|
347
|
+
|
|
348
|
+
Legend: ✓ fully present · partial present-but-incomplete · ✗ absent.
|
|
349
|
+
|
|
350
|
+
---
|
|
351
|
+
|
|
352
|
+
## Test coverage by rule
|
|
353
|
+
|
|
354
|
+
> ⚠️ **Fixture set has roughly quadrupled since this table was written (2026-05-15).**
|
|
355
|
+
> As of 2026-07-26 the always-on gate renders **49 fixtures**, including **20 `ref-*`**
|
|
356
|
+
> (cross-step / same-doc mirror coverage for Rule 9), **15 `effects/*`**, the data-
|
|
357
|
+
> pipeline fixtures (`fixture-pipeline-via-mosaicx`, `github-pulse-replay`), and 4 audio
|
|
358
|
+
> fixtures with `minMeanVolumeDb` floors. The rows below name the original anchor
|
|
359
|
+
> fixtures only — the `FIXTURES` array in
|
|
360
|
+
> the CLI source (not published) is the live inventory.
|
|
361
|
+
|
|
362
|
+
| Rule | Gate type | Where |
|
|
363
|
+
|---|---|---|
|
|
364
|
+
| 1 | unit | the render engine source (not published) |
|
|
365
|
+
| 2/3/4/9/10 | E2E + visual | the CLI source (not published) (multi-output-minimal); the render engine source (not published) |
|
|
366
|
+
| 5 | unit | the render engine source (not published) |
|
|
367
|
+
| 6 | E2E | the CLI source (not published) (multi-output-minimal) |
|
|
368
|
+
| 7 | unit + E2E | the render engine source (not published) exact-override branch; the CLI source (not published) --m0v plumbing |
|
|
369
|
+
| 8 | unit | the CLI source (not published); CLI `cli.flags.test.js` |
|
|
370
|
+
| 11 | E2E | the CLI source (not published) (doc-with-encodes, pipeline-single-encodes, pipeline-multi-encodes, codec-matrix-video, codec-matrix-audio) |
|
|
371
|
+
| 12 | structural | See the render-path walkthrough (internal: `.ai/moat/templates/cli-template-lifecycle.md`) |
|
|
372
|
+
|
|
373
|
+
---
|
|
374
|
+
|
|
375
|
+
## Migration notes (what disappeared)
|
|
376
|
+
|
|
377
|
+
This contract represents a flatten refactor from a previous design:
|
|
378
|
+
|
|
379
|
+
- **`MosaicOutput` type — DELETED.** Its fields are flat on `MosaicDocument` and `MosaicDocumentPipeline` now.
|
|
380
|
+
- **`outputs: Record<string, MosaicOutput>` map — DELETED.** Documents have one output config; multi-geometry deliverables go through pipelines.
|
|
381
|
+
- **`outputsRef?: string` — DELETED.** Templates don't reference `.m0v` files; the CLI loads `.m0v` as default user input.
|
|
382
|
+
- **`labelsRef?: string` — DELETED.** Same principle as `outputsRef`. The canonical path for `.m0c` / `.m0p` content into a doc is via the template `m0c` / `m0p` prop type — the CLI/host loads the file, supplies it as a prop value, and the template extracts the labels into `doc.labels`. See `MosaicTemplatePropType` in https://github.com/m0saic-project/m0saic-packages/blob/main/packages/types/src/template/template.ts.
|
|
383
|
+
- **`encodes?: ...` — MOVED to top-level** on Document and Pipeline (was nested inside MosaicOutput).
|
|
384
|
+
- **`emit?: ...` — MOVED to top-level** on Pipeline (was on MosaicOutput).
|
|
385
|
+
- **`ctx.userIntent.requestedOutputs` — REMOVED.** Subsumed by `ctx.userIntent.outputs` (the engine reads `Object.keys(outputs)` for the count signal in rule 7).
|
|
386
|
+
|
|
387
|
+
## Forward references
|
|
388
|
+
|
|
389
|
+
- `MosaicPipelineStep / PipelineStepBase` JSDoc — full `intermediate` / `label` semantics.
|
|
390
|
+
- `MosaicOutputEncode` JSDoc — full multi-encode contract.
|
|
391
|
+
- Concrete render walkthrough for templates consulting `ctx.userIntent` — engine-internal: `.ai/moat/templates/cli-template-lifecycle.md` (absent in the shipped copy).
|
|
392
|
+
- Data-fetcher / adapter / handle patterns — rely on intermediate steps and data-only docs in pipelines. Promoted: [`data-pipeline.md`](data-pipeline.md) (sibling doc in this folder).
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
# Standalone data-viz pack authoring — the sandbox playbook
|
|
2
|
+
|
|
3
|
+
A *pack* (`@<publisher>/<pack>/<slug>/vN`) is a cohesive set of **standalone**
|
|
4
|
+
templates sharing one brand look. Do **not** compose the generic `@m0saic/charts/*`
|
|
5
|
+
primitives — **reuse their pure geometry MATH** and build your own chrome on a small
|
|
6
|
+
shared kit. Canonical reference: `@m0saic/alpine/*` (bar-graph · commit-feed ·
|
|
7
|
+
contributor-table · donut · heatmap · kpi-card · leaderboard · line-chart ·
|
|
8
|
+
progress-card · stat-card · timeline · treemap), a **12-template** pack (as of
|
|
9
|
+
2026-07-27 — `ls` the folder) built end-to-end via the sandbox protocol. Pairs with
|
|
10
|
+
https://github.com/m0saic-project/m0saic-sandbox/blob/main/packages/sandbox/agent.md and this folder's `construction-strategy.md` + `reference/`.
|
|
11
|
+
|
|
12
|
+
## 0. Constraints first — the authoring loop (founder direction, 2026-09-05)
|
|
13
|
+
|
|
14
|
+
"Template design should move towards: you start by defining your constraints and
|
|
15
|
+
general direction, then build layout and check against various canvases and prop
|
|
16
|
+
combinations in a loop. The layout contract allows you to drive towards a
|
|
17
|
+
solution. And the build-time check throws before you even have a chance to open
|
|
18
|
+
Mosaic Desktop or invoke the CLI."
|
|
19
|
+
|
|
20
|
+
1. **Declare the contract before the layout.** Write the props schema with its
|
|
21
|
+
defaults (every optional boolean / closed-set knob has a `defaultProps` value,
|
|
22
|
+
every plain string/number a default or a placeholder — §3 "Defaults are the
|
|
23
|
+
contract"), tag the pieces that carry design intent, and declare their
|
|
24
|
+
`LayoutConstraint`s + `textFits` under `debugLayout` — canvas-independent
|
|
25
|
+
ratios, not pixels. State the general direction (the aspect it is designed for,
|
|
26
|
+
how it degrades) in the description.
|
|
27
|
+
2. **Build the layout against those constraints.** Real geometry first
|
|
28
|
+
([`construction-strategy.md`](construction-strategy.md)); fit text under the
|
|
29
|
+
same ruler the contract checks with ([`layout-contract.md`](layout-contract.md)
|
|
30
|
+
§"Text").
|
|
31
|
+
3. **Loop: check, don't eyeball.** `assertLayout` at the 7-canvas set
|
|
32
|
+
(1920×1080 · 1280×720 · 1080×1920 · 1080×1080 · 3840×2160 · 640×360 · 480×270)
|
|
33
|
+
× the prop combinations that change geometry (long titles, max items, every
|
|
34
|
+
closed-set value) in the gate test; `auditDefaultProps(T)` empty. A violation is
|
|
35
|
+
a design decision to make (shrink, compact wording, drop), not noise — the
|
|
36
|
+
contract drives toward the solution. Exemplar:
|
|
37
|
+
https://github.com/m0saic-project/m0saic-packages/blob/main/packages/templates/src/m0saic/dsl-tutorial/v1/dsl-tutorial.gate33.test.ts.
|
|
38
|
+
4. **The build is the first gate.** `npm run build` in https://github.com/m0saic-project/m0saic-packages/blob/main/packages/templates (and
|
|
39
|
+
in the community pack and both starter repos) ends by loading the built
|
|
40
|
+
registry first-party (`tools/check-registry.mjs`): a template that hides a
|
|
41
|
+
default throws — naming the knobs and the fix — and the build fails before the
|
|
42
|
+
template can load in Mosaic Desktop or the CLI. Only then does the sandbox loop
|
|
43
|
+
(candidate → human verdict, §4) begin.
|
|
44
|
+
|
|
45
|
+
The layout contract stays debug-only at render time (2026-08-22 ruling — a render
|
|
46
|
+
with slightly clipped text beats no render); the gate test and the build step are
|
|
47
|
+
where it bites during authoring.
|
|
48
|
+
|
|
49
|
+
## 1. Stand up the seam first (once per pack)
|
|
50
|
+
|
|
51
|
+
Three `_shared/` files (verified 2026-07-27 in https://github.com/m0saic-project/m0saic-packages/blob/main/packages/templates/src/m0saic/alpine/_shared):
|
|
52
|
+
|
|
53
|
+
- `<pack>-theme.ts` — tokens (surface/card/border, title/subtitle/label/muted,
|
|
54
|
+
primary/positive/negative, grid/axis), a categorical `PALETTE`,
|
|
55
|
+
`PRESETS{light,dark}`, a `theme(preset)` resolver. **Dark is not an afterthought:**
|
|
56
|
+
every color, wash, and contrast must be retuned for it (leaderboard `MEDALS_DARK`,
|
|
57
|
+
heatmap `EMPTY_DARK`) — a wash tuned for white glares or vanishes on navy.
|
|
58
|
+
- `<pack>-card.ts` — the Node combinator kit lifted from `charts/bar-graph/v2` (the
|
|
59
|
+
canonical tight-rect template): `paint` / `EMPTY` / `rowSplit` / `colSplit` /
|
|
60
|
+
`overlay` / `insetNode` / `textCell`, plus `resolveColor`, and a
|
|
61
|
+
`card({theme, W, H, title, subtitle})` returning `{contentRect, compose(content,
|
|
62
|
+
...extraLayers), backgroundColor}`. Every template wraps its body in this card.
|
|
63
|
+
- `<pack>-anim.ts` — shared reveal choreography + prop-schema field factories:
|
|
64
|
+
`revealGate` / `revealSlide` / `revealFade` (source and Node flavors), the `fBool`
|
|
65
|
+
/ `fStr` / `fNum` / `fFrac` / `fEnum` schema-field helpers, and the shared
|
|
66
|
+
`ALPINE_ANIM_FIELDS` block every template spreads into its props schema.
|
|
67
|
+
|
|
68
|
+
A `Node` is `{ m0: string; sources: MosaicSource[] }` built together so source
|
|
69
|
+
emission order always matches the DSL's DFS order.
|
|
70
|
+
|
|
71
|
+
**And the front door (the hello-world convention, 2026-09-14).** Every template
|
|
72
|
+
repo names the template a newcomer renders first: `repo.helloWorld` on the repo
|
|
73
|
+
descriptor (`src/repo.ts` in the starters). The default is the canonical m0saic
|
|
74
|
+
card, one call from `@m0saic/template-utils`:
|
|
75
|
+
|
|
76
|
+
```ts
|
|
77
|
+
const card = defineHelloWorldTemplate({ id: "@acme/basics/hello-world/v1", subline: `by ${TEMPLATE_REPO.displayName}` });
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
The field, the M, the wordmark and the greeting stay the brand ("made with
|
|
81
|
+
m0saic"); `subline` is the repo's own line, seeded into the `caption` prop. A pack
|
|
82
|
+
that wants its own look writes its own template and points `repo.helloWorld` at
|
|
83
|
+
it. The gate warns (`repoFrontDoor`, record posture — never fatal) when a repo
|
|
84
|
+
names no front door or names an id it does not register. `m0saic hello-world
|
|
85
|
+
--template-repo .` renders it; the manifest carries the field zero-exec.
|
|
86
|
+
|
|
87
|
+
Two authoring facts, both learned on the starters:
|
|
88
|
+
|
|
89
|
+
- Spell the structural fields out in the repo module — a literal with
|
|
90
|
+
`version`, `propsSchema: { ...HELLO_WORLD_PROPS_SCHEMA }`, `defaultProps`, and a
|
|
91
|
+
`render` that delegates to the factory — because the no-install contract check
|
|
92
|
+
loads the entry with every `@m0saic/*` import stubbed to an identity proxy, under
|
|
93
|
+
which a bare factory call reads as an options bag. Copy either starter's
|
|
94
|
+
`src/basics/hello-world/v1/hello-world.ts`.
|
|
95
|
+
- A still of the card at t=0 is a bare navy canvas (the field wipes in, the M
|
|
96
|
+
assembles). Gallery stills are cut from the clip near its end — the starters'
|
|
97
|
+
`gen-previews.mjs` carries a per-id `STILL_AT_SEC` (hello-world → 2.45 s). The
|
|
98
|
+
card also WIDENS for a long line (to 92% of the canvas) before type shrinks or
|
|
99
|
+
truncates; the default card never widens, so the shipped fingerprint holds.
|
|
100
|
+
|
|
101
|
+
## 2. Two construction modes — pick per template
|
|
102
|
+
|
|
103
|
+
- **Splits** (leaderboard, bar, progress) — real m0 cells via `weightedSplit`. Best
|
|
104
|
+
for row/column layouts. Snap nested weights to a multiple of 8 so they GCD-collapse
|
|
105
|
+
and stay quantization-feasible at any resolution. (Large splits with coprime pixel
|
|
106
|
+
weights deterministically spread the fractional remainder — that's precision, not
|
|
107
|
+
a bug; canonical: `handbook/feasibility-precision-quantization.md`.)
|
|
108
|
+
- **placeRects absolute** (donut, line, heatmap, timeline, treemap) — every cell is a
|
|
109
|
+
tight rect on the **SNAP_PX (4px) lattice**, packed full-canvas via `placeRects`.
|
|
110
|
+
Use for grids, rings, scatter, calendars, treemaps, spines — anything not a clean
|
|
111
|
+
split. **THE recurring bug (bit on heatmap, timeline, treemap):** pass the
|
|
112
|
+
placeRects body as a **full-canvas extraLayer** — `card.compose(EMPTY, body)` —
|
|
113
|
+
**never as the scaled `content`.** Scaled content goes through `insetNode`, which
|
|
114
|
+
re-quantizes the split weights into a smaller rect → uneven cells /
|
|
115
|
+
`SPLIT_EXCEEDS_AXIS` (0-size band). Also snap **pitch, gap, and all offsets** to
|
|
116
|
+
the lattice so every cell edge and gap-band boundary is grid-aligned.
|
|
117
|
+
|
|
118
|
+
## 3. The knobs every template gets right
|
|
119
|
+
|
|
120
|
+
- **Defaults are the contract ("a knob shows what it does", 2026-09-05).** Every
|
|
121
|
+
optional `boolean` and closed-set knob (`constraints.oneOf` / `control.options`)
|
|
122
|
+
carries a `defaultProps` value; every plain `string` / `number` a default OR a
|
|
123
|
+
`meta.control.placeholder` naming the unset behaviour ("auto"). A fallback that
|
|
124
|
+
lives only in `render()` (`props.title ?? "…"`, `props.show !== false`) makes the
|
|
125
|
+
Make panel LIE (empty / OFF for a value the render fills in). `defineMosaicTemplate`
|
|
126
|
+
throws `TemplateConventionError` for a first-party violation at import; an external
|
|
127
|
+
pack's is recorded, never fatal. A closed-set knob whose unset state is not one
|
|
128
|
+
of its options gets a `"none"` option (defaulted, normalised away in `render()`).
|
|
129
|
+
Rule + exemptions: [`philosophy-and-contract.md`](philosophy-and-contract.md)
|
|
130
|
+
§"Props as a UI-renderable schema".
|
|
131
|
+
- **`resolveColor(value, fallback)` on EVERY color knob.** A bare `props.color ??
|
|
132
|
+
fallback` keeps `""` (not nullish) → renders the mark in no color → invisible. A
|
|
133
|
+
cleared / "none" / blank picker must fall back.
|
|
134
|
+
- **Dual props.** Agent-canonical knobs (`anim`, `valueMode`, `orientation`, …) plus
|
|
135
|
+
human-facing toggles/sliders (`animate`, `showValue`, `introLength`, `cornerRadius`)
|
|
136
|
+
wired via `meta.control.syncsTo` (e.g. `animate` → `anim.reduceMotion` boolInvert).
|
|
137
|
+
Anything a human flips or drags belongs in `consumer:"human"` — they WILL ask.
|
|
138
|
+
- **Labels.** Compact big numbers (`compactNum`: `4500→4.5K`); size the font
|
|
139
|
+
length-aware or shrink-to-fit; ellipsis-truncate only when even the min font
|
|
140
|
+
overflows, bounding truncation width so labels can't collide. Do **not** use
|
|
141
|
+
`fit:"contain"` for a single line — it scales to HEIGHT and clips width. Fit
|
|
142
|
+
under `textEmUnits × fontSize × em` with a `cell × 0.94 − 2px` budget and lock it
|
|
143
|
+
with `textFits` — [`layout-contract.md`](layout-contract.md) §"Text".
|
|
144
|
+
- **Contrast auto-flip.** `onColor(hex, light, dark)` via relative luminance for any
|
|
145
|
+
text sitting on a colored fill (white on saturated, dark on pale/amber).
|
|
146
|
+
- **Animation.** Opacity rides `overlay.alpha` — `makeColorTile` IGNORES `visual`.
|
|
147
|
+
`fadeInExpr(start, dur)` cascades; `animateNumbersInText` for count-up;
|
|
148
|
+
`reduceMotion` → the static board. **Overlay-depth limit (~25 nested layers):** the
|
|
149
|
+
engine silently drops masks (circle markers → squares, text → tofu); use a single
|
|
150
|
+
curtain-wipe overlay, not a per-sliver cascade.
|
|
151
|
+
- **Aspect-awareness.** Stress vertical / square / horizontal. A wide card may need a
|
|
152
|
+
different layout (timeline's `orientation:"auto"`); clamp edge content so it can't
|
|
153
|
+
spill the card.
|
|
154
|
+
- **A canvas that is a knob → `resolveOutputHints(props)`.** When a prop picks the
|
|
155
|
+
output size (a `platform` knob: YouTube 1920×1080 vs TikTok 1080×1920), declare
|
|
156
|
+
the resolver on the template so hosts SEED the right target before rendering
|
|
157
|
+
(`resolveTemplateOutputHints` is the one helper CLI / Electron / web / Make use);
|
|
158
|
+
`render` still reads `ctx.target` and lays out at any size. At `defaultProps` it
|
|
159
|
+
must agree with the static `outputHints` (`outputHintsResolve`, throw). Rules:
|
|
160
|
+
[`philosophy-and-contract.md`](philosophy-and-contract.md) §"Output contract".
|
|
161
|
+
- **Prop bindings — the rect that SHOWS a prop is its handle in Make.**
|
|
162
|
+
`bindProp(src, key)` / `bindProps(src, entries)` / `bindPropPath(src, key, path,
|
|
163
|
+
kind)` / `bindPropRange(...)` (`@m0saic/template-utils`) stamp `editor.binding` on
|
|
164
|
+
the source that displays a prop; Make resolves them per render (`bindingsSound`
|
|
165
|
+
throws on a binding the schema refuses — booleans, closed pickers and whole lists
|
|
166
|
+
are never bindable) and the preview shows one glyph per thing the tile can do (T
|
|
167
|
+
text · 123 number · swatch colour · move rect · picture media), collapsing to ONE
|
|
168
|
+
dot on a small tile. Bind even when the value is empty (the rect is a handle to
|
|
169
|
+
ADD). The full contract — kinds, `onClear`, `seedDraft`, `kind: "media"` (a rect
|
|
170
|
+
that takes a dropped file), `companion`, starter media — is
|
|
171
|
+
[`reference/prop-bindings.md`](reference/prop-bindings.md).
|
|
172
|
+
**`kind: "rect"` (2026-09-15) — a rendered cell edited IN PLACE:** a `json` prop
|
|
173
|
+
with `picker: "regions"` and `regions.max: 1`, bound with `bindPropRect(src, key)`
|
|
174
|
+
(no path). Double-click / the move badge opens the regions draw session seeded
|
|
175
|
+
from the cell's painted rect; drag, 8 handles, Shift = aspect lock, Delete = back
|
|
176
|
+
to automatic placement, Esc / Enter = done; every gesture end writes `{ canvas,
|
|
177
|
+
regions: [rect] }` into the prop and the template re-renders — never a
|
|
178
|
+
destructive post-render edit. The template must still render a correct default
|
|
179
|
+
placement when the prop is empty: the binding is the handle, not the layout. A
|
|
180
|
+
cell may carry several bindings (`bindProps` with a rect entry AND text entries) —
|
|
181
|
+
that is the "move the cell OR change its contents" surface. First implementer:
|
|
182
|
+
`@m0saic-dev/creator/drop-calendar/v1` (`facecamRegion`).
|
|
183
|
+
|
|
184
|
+
## 4. The sandbox loop
|
|
185
|
+
|
|
186
|
+
The full iteration ritual is https://github.com/m0saic-project/m0saic-sandbox/blob/main/packages/sandbox/agent.md — read it before authoring
|
|
187
|
+
or iterating on any candidate; this doc does not duplicate it. Naming convention in
|
|
188
|
+
one line: `candidate-NNN.mosaicx` (one file = one question = one verdict), a frozen
|
|
189
|
+
`candidate-NNN-agent.mosaicx` snapshot before every open, and
|
|
190
|
+
`candidate-NNN_humanedits.mosaic` frozen from the live dist BEFORE editing the
|
|
191
|
+
template on negative feedback.
|
|
192
|
+
|
|
193
|
+
## 5. The lock is the full closeout — non-negotiable
|
|
194
|
+
|
|
195
|
+
Canonical: https://github.com/m0saic-project/m0saic-sandbox/blob/main/packages/sandbox/agent.md §"Session closeout" + §"Template-build
|
|
196
|
+
closeout (the `.mosaicx` flavor)". This doc intentionally does not summarize the
|
|
197
|
+
artifact list — read agent.md and follow it. Two rules earlier summaries
|
|
198
|
+
dangerously omitted:
|
|
199
|
+
|
|
200
|
+
1. **The master is `master_candidate-NN.mosaicx` until approval.** "Do NOT mint a
|
|
201
|
+
bare `master.mosaicx` before approval — that name is reserved for the approved
|
|
202
|
+
file." Rejected gates stay frozen; the fix ships as `master_candidate-02`, etc.;
|
|
203
|
+
only on approval is the file renamed to `master.mosaicx` + `master.mosaic`.
|
|
204
|
+
2. **"Gate EVERY output aspect, not just one."** Aspect-adaptive templates pass
|
|
205
|
+
desktop and still fail square/mobile — the final gate is the human rendering ALL
|
|
206
|
+
aspects (H / V / square) from the candidate and signing off. Verify the aspects
|
|
207
|
+
yourself first, but the gate is theirs.
|
|
208
|
+
3. **The layout contract comes BEFORE candidate-01.** Tag every fitted text source,
|
|
209
|
+
declare `textFits` under `debugLayout`, and sweep `assertLayout` at the 7-canvas
|
|
210
|
+
set in the gate test — [`layout-contract.md`](layout-contract.md)
|
|
211
|
+
§"Recommended". A gate that passed one aspect and clipped on portrait is the
|
|
212
|
+
scenario this rule was written for (gate 33).
|
|
213
|
+
|
|
214
|
+
## 6. Cross-cutting gotchas (bit on multiple templates)
|
|
215
|
+
|
|
216
|
+
- `cornerRadius` is a **0..0.5 fraction of the cell's shorter side** → px radius
|
|
217
|
+
(`borderRadius * min(w, h)`). At high radius, inset top-left labels must clear
|
|
218
|
+
the corner curve.
|
|
219
|
+
- **Make `--make` open adopts the file's `size`** over the template's `outputHints`
|
|
220
|
+
(a 1080² candidate must open square, not 1280×800). Beware: Make's save-back
|
|
221
|
+
rewrites the file's size/props to the panel state — re-freeze candidates that drift.
|
|
222
|
+
- **The render-hero preview rounds a cell by driving `border-radius !important`** on
|
|
223
|
+
the matched ViewFrame cell (`Frame.tsx` wires `var(--vf-tile-radius, 0px)`, so a
|
|
224
|
+
plain assign is overridden). Measure the radius via `ResizeObserver` — cells can be
|
|
225
|
+
0×0 on the first effect pass.
|
|
226
|
+
- **The 119/121 fencepost.** `weights.map(w => round(w / sum * 120))` sums to
|
|
227
|
+
120 ± (bands − 1): every Alpine card carried `121×2`, the pulse heroes `121×8`,
|
|
228
|
+
and 121 = 11² poisons every composition (LCM with the core = 14,520). The kit
|
|
229
|
+
line is `weightedSplit(latticeWeights(weights, { cap: 120 }), axis, { claimants })`
|
|
230
|
+
(Hamilton to an exact 5-smooth total, GCD to a divisor: `[59,1802,59] → [1,28,1]`).
|
|
231
|
+
Since 2026-09-16 the build gate and `m0saic doctor` refuse a rough count above 12
|
|
232
|
+
(`latticeSmooth`, throw — [`reference/template-flags.md`](reference/template-flags.md))
|
|
233
|
+
— for a pack it is the publish requirement, so run the doctor before tagging.
|