@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,77 @@
|
|
|
1
|
+
# More atoms, never bigger atoms — the scaling contract with ffmpeg
|
|
2
|
+
|
|
3
|
+
> The measured cliffs behind atom 1 (and the "graph-size OOM" debunk) live in
|
|
4
|
+
> [`../runtime/ffmpeg-expression-limits.md`](../runtime/ffmpeg-expression-limits.md).
|
|
5
|
+
> This is the general principle; that is the case study.
|
|
6
|
+
|
|
7
|
+
m0saic renders arbitrarily complex templates by splitting work into pieces.
|
|
8
|
+
That works because ffmpeg capacity scales with the NUMBER of things it is
|
|
9
|
+
given (commands, inputs, filters, files). It breaks when a feature grows a
|
|
10
|
+
single UNPARTITIONABLE ATOM with content. Chunking cannot rescue a bigger
|
|
11
|
+
atom — the atom is smaller than a command.
|
|
12
|
+
|
|
13
|
+
## The four atoms (with measured walls)
|
|
14
|
+
|
|
15
|
+
1. **One expression string.** `av_expr_parse` has a ~100 recursion budget:
|
|
16
|
+
~98 flat `+` terms or ~100 nesting levels, independent of char length.
|
|
17
|
+
Failure is a phantom `Cannot allocate memory` (−12) or `Invalid
|
|
18
|
+
argument` (−22) at graph init, in <80 ms. Applies to EVERY expression:
|
|
19
|
+
`enable`, `geq`, crop/scale/drawtext params, `%{eif}`.
|
|
20
|
+
2. **One filter's per-frame work.** `geq` interprets its expression per
|
|
21
|
+
pixel per plane per frame (nodes × W × H × fps). No crash — render time
|
|
22
|
+
explodes. Chunking does not reduce it: same pixels either way.
|
|
23
|
+
3. **One frame's memory.** frames-in-flight × W × H × 4 B. Buffering
|
|
24
|
+
filters (`loop=size=N`, `split`, framesync queues, `tpad`) and
|
|
25
|
+
resolution multiply it; filter COUNT does not (12,000 chained filters /
|
|
26
|
+
1.19 MB single graph verified fine).
|
|
27
|
+
4. **One process's argv.** Windows caps ~32 K → `ENAMETOOLONG`.
|
|
28
|
+
|
|
29
|
+
## The classification test (apply to any content-scaled feature)
|
|
30
|
+
|
|
31
|
+
When something scales with content (N tiles, steps, keyframes, data
|
|
32
|
+
points), ask: **which quantity grows?**
|
|
33
|
+
|
|
34
|
+
- Count of commands / inputs / filters / workspace files grows → MORE
|
|
35
|
+
ATOMS → fine; capacity, chunkable.
|
|
36
|
+
- A single expression, a single filter's per-pixel cost, a single frame's
|
|
37
|
+
resident bytes, or argv grows → BIGGER ATOM → redesign before shipping.
|
|
38
|
+
|
|
39
|
+
## Bigger-atom anti-patterns → more-atoms replacements
|
|
40
|
+
|
|
41
|
+
| anti-pattern (bigger atom) | replacement (more atoms / bounded atom) |
|
|
42
|
+
|---|---|
|
|
43
|
+
| nested `if(cond,…,else)` per keyframe (`expr = if(...,${expr})` builders) | flat gated sum: `lt(t,t0)*(v0)+(gte·lt)*seg+…+gte(t,tN)*(vN)` |
|
|
44
|
+
| `.join("+")` on a content-scaled window list, emitted raw | route through the funnels (below) or `rebalanceAdditiveChains` |
|
|
45
|
+
| `geq` on a canvas/panel-sized stream | static SVG/PNG mask + `alphamerge`; keep geq on small intermediates |
|
|
46
|
+
| animated values as ever-larger exprs | pre-render to a small intermediate (value `.mov`s, text renders) |
|
|
47
|
+
| content-scaled string inline in argv (`-i lavfi`, drawtext text) | workspace file (`-filter_complex_script`, `-graph_file`, `textfile=`) |
|
|
48
|
+
| full-res lossless-alpha (qtrle/argb) intermediates on opaque nodes | alpha intermediates only where alpha flows |
|
|
49
|
+
|
|
50
|
+
## Existing enforcement (route new work through these; do not bypass)
|
|
51
|
+
|
|
52
|
+
- `rebalanceAdditiveChains` (`@m0saic/platform` ffexpr) auto-applied at the
|
|
53
|
+
funnels: `quoteEnableArg` (all `:enable=`), `buildOverlayAlphaFilter`
|
|
54
|
+
(enable→geq alpha fold), `buildCameraFilters` (zoom/focus). >32 flat
|
|
55
|
+
terms → balanced tree (O(log n) depth, verified past 1024 terms); ≤32
|
|
56
|
+
passes byte-identical. It CANNOT fix nesting — builders must emit flat
|
|
57
|
+
shapes.
|
|
58
|
+
- Input-count split (`maxInputsPerCommand`) — more commands per node.
|
|
59
|
+
- Graph/text file lowering — argv atom stays constant.
|
|
60
|
+
- SVG rounding rasterizer (default) — rounding is a mask input, not geq.
|
|
61
|
+
|
|
62
|
+
Known bypasses to watch: hand-rolled lavfi `enable=` strings (dsl-string
|
|
63
|
+
caret track, dsl-canvas drawbox — currently ≤3 terms) and nested-if
|
|
64
|
+
builders in other templates (e.g. line-chart anim) that will hit atom 1 if
|
|
65
|
+
their datasets grow.
|
|
66
|
+
|
|
67
|
+
## Review greps
|
|
68
|
+
|
|
69
|
+
`join("+")` on generated arrays; recursive expr accumulation
|
|
70
|
+
(`= \`if(…,${expr})\``); `geq` fed by canvas-sized labels; content
|
|
71
|
+
interpolation into `-i`/argv rather than a workspace file.
|
|
72
|
+
|
|
73
|
+
---
|
|
74
|
+
|
|
75
|
+
*Per-pixel eval cost (atom 2) is the next practical wall at denser grids — a
|
|
76
|
+
perf ceiling, not a crash; fix it representationally (masks, pre-rendered
|
|
77
|
+
intermediates), not by chunking.*
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
## Overlay Semantics & Paint Order
|
|
2
|
+
|
|
3
|
+
Overlays are recursive layout subtrees rendered inside an owner's rect — a first-class
|
|
4
|
+
compositional primitive, not just "layers". This doc covers attachment, paint order,
|
|
5
|
+
deferred zero-overlay paint, the overlay-body validity rule, and composition at scale.
|
|
6
|
+
|
|
7
|
+
> **Source of truth:** overlay parsing in [https://github.com/m0saic-dsl/m0/blob/main/packages/dsl/src/parse/m0StringParser.ts](https://github.com/m0saic-dsl/m0/blob/main/packages/dsl/src/parse/m0StringParser.ts);
|
|
8
|
+
> overlay-body validation in [https://github.com/m0saic-dsl/m0/blob/main/packages/dsl/src/validate/m0StringValidator.ts](https://github.com/m0saic-dsl/m0/blob/main/packages/dsl/src/validate/m0StringValidator.ts)
|
|
9
|
+
> (relaxation comment at lines 479–486); error codes in [https://github.com/m0saic-dsl/m0/blob/main/packages/dsl/src/errors/errors.ts](https://github.com/m0saic-dsl/m0/blob/main/packages/dsl/src/errors/errors.ts).
|
|
10
|
+
> Every accept/reject example below was runtime-verified against https://github.com/m0saic-dsl/m0/blob/main/packages/dsl/dist
|
|
11
|
+
> via `validateM0String` on 2026-07-27.
|
|
12
|
+
|
|
13
|
+
## Attachment: any node can own an overlay
|
|
14
|
+
|
|
15
|
+
Form:
|
|
16
|
+
|
|
17
|
+
node{ overlay_body }
|
|
18
|
+
|
|
19
|
+
`node` may be ANY node kind — not just `1`:
|
|
20
|
+
|
|
21
|
+
- `1` (tile), `0` (passthrough), `-` (null — child position only; see Logical Owner)
|
|
22
|
+
- `N(...)` / `N[...]` (splits)
|
|
23
|
+
- a node that is itself an overlay owner (chains)
|
|
24
|
+
|
|
25
|
+
No base `1` is required for an overlay to exist. A chain of full layouts is legal:
|
|
26
|
+
|
|
27
|
+
2(1,1){3(1,1,1){2(1,1)}} — base 2-col, overlay 3-col, overlay-on-overlay 2-col
|
|
28
|
+
|
|
29
|
+
Rules:
|
|
30
|
+
|
|
31
|
+
- At most ONE immediate overlay object per node — `1{1}{1}` rejects
|
|
32
|
+
(`OVERLAY_CHAIN`). Stack by nesting instead: `1{1{1}}`.
|
|
33
|
+
- The overlay body must validate independently as a full m0 string, recursively.
|
|
34
|
+
- Nesting depth is structurally unbounded — but see the render-cost caveat below.
|
|
35
|
+
|
|
36
|
+
Valid: `1{1}` · `2(1,1){1}` · `1{1{1}}` · `2(1,1){3(1,1,1){2(1,1)}}`
|
|
37
|
+
|
|
38
|
+
## Overlay rect = owner rect
|
|
39
|
+
|
|
40
|
+
The overlay subtree is parsed against owner.x / owner.y / owner.width / owner.height.
|
|
41
|
+
|
|
42
|
+
- It does NOT inherit sibling geometry.
|
|
43
|
+
- It does NOT escape its parent rect.
|
|
44
|
+
|
|
45
|
+
Applies to tile, group, null, and zero overlays alike.
|
|
46
|
+
|
|
47
|
+
## Overlays never mutate geometry
|
|
48
|
+
|
|
49
|
+
Overlay stacking, at any depth:
|
|
50
|
+
|
|
51
|
+
- does NOT change splitEven results or sibling sizes
|
|
52
|
+
- does NOT change structuralDepth
|
|
53
|
+
- does NOT alter stableKeys of the base structure
|
|
54
|
+
|
|
55
|
+
It only increments `overlayDepth` and adds paint layers. The base layout defines
|
|
56
|
+
geometry; overlay chains define drawing.
|
|
57
|
+
|
|
58
|
+
## Paint order
|
|
59
|
+
|
|
60
|
+
Deterministic, defined by engine traversal.
|
|
61
|
+
|
|
62
|
+
### Tile overlay
|
|
63
|
+
|
|
64
|
+
1{1}
|
|
65
|
+
|
|
66
|
+
1. Base tile
|
|
67
|
+
2. Overlay subtree (immediately after owner)
|
|
68
|
+
|
|
69
|
+
### Group overlay
|
|
70
|
+
|
|
71
|
+
2(1,1){1}
|
|
72
|
+
|
|
73
|
+
1. Child 0
|
|
74
|
+
2. Child 1
|
|
75
|
+
3. Group overlay — group overlays paint AFTER all base children.
|
|
76
|
+
|
|
77
|
+
### Zero overlay (deferred paint)
|
|
78
|
+
|
|
79
|
+
3(0{1},1,1)
|
|
80
|
+
|
|
81
|
+
Zero overlays do NOT paint immediately; they defer until the claimant renders:
|
|
82
|
+
|
|
83
|
+
1. Claimant tile
|
|
84
|
+
2. Deferred zero overlay
|
|
85
|
+
|
|
86
|
+
Multiple zero overlays in one run:
|
|
87
|
+
|
|
88
|
+
3(0{1},0{1},1)
|
|
89
|
+
|
|
90
|
+
1. Claimant
|
|
91
|
+
2. Larger-area zero overlay
|
|
92
|
+
3. Smaller-area zero overlay (top-most)
|
|
93
|
+
|
|
94
|
+
Deferred zero overlays are sorted by area DESCENDING → smaller overlays paint on top.
|
|
95
|
+
|
|
96
|
+
### Claimant with its own overlay
|
|
97
|
+
|
|
98
|
+
3(0{1},1{1},1)
|
|
99
|
+
|
|
100
|
+
1. Claimant tile
|
|
101
|
+
2. Claimant's own overlay
|
|
102
|
+
3. Deferred zero overlay — always after the claimant AND the claimant's overlay.
|
|
103
|
+
|
|
104
|
+
### Nested overlays
|
|
105
|
+
|
|
106
|
+
1{1{1}}
|
|
107
|
+
|
|
108
|
+
1. Base tile
|
|
109
|
+
2. First overlay
|
|
110
|
+
3. Nested overlay
|
|
111
|
+
|
|
112
|
+
## Logical owner `-`
|
|
113
|
+
|
|
114
|
+
`-{X}` marks `isLogicalOwner = true`: the base does not render, but the overlay rect
|
|
115
|
+
exists and carry is still absorbed if it is a claimant. Use for editor semantics and
|
|
116
|
+
labeling containers.
|
|
117
|
+
|
|
118
|
+
Position matters: `-{...}` is valid in a child slot or inside an overlay body, NOT as
|
|
119
|
+
the whole-string root — `2(-{1},1)` and `1{-{1}}` accept; bare `-{1}` rejects
|
|
120
|
+
(`TOKEN_RULE`), as does a bare `-` root (`INVALID_EMPTY`).
|
|
121
|
+
|
|
122
|
+
## Overlay depth vs structural depth
|
|
123
|
+
|
|
124
|
+
Two independent counters:
|
|
125
|
+
|
|
126
|
+
- `structuralDepth` — increases entering numeric containers; root = 0.
|
|
127
|
+
- `overlayDepth` — increases entering `{...}`; base layout = 0, first overlay = 1.
|
|
128
|
+
|
|
129
|
+
Overlay frames inherit the structuralDepth of their context and use a separate frame
|
|
130
|
+
namespace.
|
|
131
|
+
|
|
132
|
+
## Overlay body node rule (`INVALID_EMPTY`)
|
|
133
|
+
|
|
134
|
+
An overlay body `{...}` must contribute at least ONE node to the graph, but does not
|
|
135
|
+
have to paint. Logical-owner anchors (`-{X}`) are the common shape.
|
|
136
|
+
|
|
137
|
+
Rejected — zero nodes, or a single root with nothing to act on:
|
|
138
|
+
|
|
139
|
+
1{} — empty body; no nodes at all
|
|
140
|
+
1{0} — bare passthrough at the overlay root; nothing to donate to
|
|
141
|
+
1{-{}} — recursive: the inner `{}` is rejected (deepest offender reported)
|
|
142
|
+
|
|
143
|
+
Accepted — at least one node, even if nothing paints:
|
|
144
|
+
|
|
145
|
+
1{-} — single null node; logical-owner anchor
|
|
146
|
+
1{2(-,-)} — all-null split; structural nodes, no paint
|
|
147
|
+
1{2(0,-)} — passthrough with a sibling to donate space to
|
|
148
|
+
1{-{-}} — nested logical owners
|
|
149
|
+
1{-{2(0,-)}} — nested with structural content
|
|
150
|
+
1{2(-,1)} — overlay body has a source (always valid)
|
|
151
|
+
1{2[1,1]{1}} — nested overlay body has a source
|
|
152
|
+
|
|
153
|
+
The legacy `ZERO_SOURCE_OVERLAY` rule (body must contain at least one leaf `1`) was
|
|
154
|
+
relaxed 2026-06-03 — overlay bodies no longer have to paint, only exist as nodes
|
|
155
|
+
([`m0StringValidator.ts`](https://github.com/m0saic-dsl/m0/blob/main/packages/dsl/src/validate/m0StringValidator.ts):479–486).
|
|
156
|
+
The whole-string `NO_SOURCES` check still requires the OUTER (root) layout to produce
|
|
157
|
+
at least one source tile, so the renderer is never asked to draw a fully empty canvas.
|
|
158
|
+
|
|
159
|
+
Error code when rejected: `INVALID_EMPTY` (kind `SYNTAX`).
|
|
160
|
+
|
|
161
|
+
## Composition at scale: deep overlay stacks
|
|
162
|
+
|
|
163
|
+
Overlay chains are how large rect sets are composed: each `{}` wraps a full valid
|
|
164
|
+
subtree, and each layer paints inside the owner's rect. A 109-overlay brand mark —
|
|
165
|
+
each overlay an independently calculated m0saic drawing one precise rectangle — is a
|
|
166
|
+
measured working example; its serialized string ran to tens of thousands of
|
|
167
|
+
characters, which parse/validate/walk handle without issue.
|
|
168
|
+
|
|
169
|
+
Deep stacks are editor-clean: each overlay is its own layer, filterable by
|
|
170
|
+
`overlayDepth`, structurally independent, with no geometry coupling.
|
|
171
|
+
|
|
172
|
+
Use deep overlays for programmatic rect emission — dictionary-based layouts, glyphs,
|
|
173
|
+
masks, strict layering control. Prefer a single split when it expresses intent more
|
|
174
|
+
clearly or raw-string readability matters.
|
|
175
|
+
|
|
176
|
+
> ⚠️ **Render-cost caveat** (measured 2026-07-02): depth is free structurally, NOT at
|
|
177
|
+
> render time. Each overlay level is one sequential blend pass on ffmpeg's single
|
|
178
|
+
> graph thread, and past ~25 nested overlay layers inline-masks silently degrade
|
|
179
|
+
> (circles→squares, text→tofu) — engine wall W3. Deep stacks are cheap to WRITE
|
|
180
|
+
> and inspect, not cheap to RENDER — collapse time-disjoint line geometry into
|
|
181
|
+
> lavfi tracks first. Author-facing rules:
|
|
182
|
+
> [`../templates/patterns/perf-authoring-rules.md`](../templates/patterns/perf-authoring-rules.md)
|
|
183
|
+
> (R6). Deep dives are engine-internal: `.ai/moat/runtime/ffmpeg-limitations.md`,
|
|
184
|
+
> `.ai/moat/runtime/render-cost-model.md` — absent in the shipped copy.
|
|
185
|
+
|
|
186
|
+
## Invalid constructions (verified, with codes)
|
|
187
|
+
|
|
188
|
+
1{} — INVALID_EMPTY (empty body)
|
|
189
|
+
1{0} — INVALID_EMPTY (bare passthrough body)
|
|
190
|
+
1{-{}} — INVALID_EMPTY (recursive)
|
|
191
|
+
1{1}{1} — OVERLAY_CHAIN (two immediate overlays; nest instead)
|
|
192
|
+
1{{1}} — TOKEN_RULE (no owner token for the inner overlay)
|
|
193
|
+
1{2(1,0)} — PASSTHROUGH_TO_NOTHING (trailing `0` inside overlay body)
|
|
194
|
+
-{1} — TOKEN_RULE (`-{...}` cannot be the whole-string root)
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# Choosing the right parse API (and mapping frames → sources)
|
|
2
|
+
|
|
3
|
+
> **Source of truth:** [https://github.com/m0saic-dsl/m0/blob/main/packages/dsl/src/parse/m0StringParser.ts](https://github.com/m0saic-dsl/m0/blob/main/packages/dsl/src/parse/m0StringParser.ts).
|
|
4
|
+
> All four parse exports re-exported from `@m0saic/dsl`. (A fifth,
|
|
5
|
+
> `parseM0StringToFullGraphWithTraversal`, was removed in the public-API prune —
|
|
6
|
+
> use `parseM0StringComplete` with `opts.trace`.)
|
|
7
|
+
|
|
8
|
+
## The canonical entry point: `parseM0StringComplete`
|
|
9
|
+
|
|
10
|
+
`parseM0StringComplete(input, width, height, opts?)` returns (on success):
|
|
11
|
+
|
|
12
|
+
- `ok: true`
|
|
13
|
+
- `ir.renderFrames` — RenderFrame[] in **paint order**
|
|
14
|
+
- `ir.editorFrames` — EditorFrame[] (full structural graph, always present)
|
|
15
|
+
- `ir.traversal` — optional DFS event stream if `opts.trace`
|
|
16
|
+
- `precision`, `resolutionDiagnostics`, `warnings`
|
|
17
|
+
|
|
18
|
+
Frames are one level deep under **`ir`**, not top-level. On failure:
|
|
19
|
+
`{ ok: false, error, precision, warnings }`. Other options:
|
|
20
|
+
`opts.materialize: "renderOnly"` (the cheap mode `parseM0StringToLogicalFrames`
|
|
21
|
+
rides on) and `opts.precisionNorm` (default 100; gates `PRECISION_EXCEEDS_NORM`
|
|
22
|
+
warnings). Prefer Complete for tooling, editor logic, anything non-trivial.
|
|
23
|
+
|
|
24
|
+
## The three cheaper tiers
|
|
25
|
+
|
|
26
|
+
| Tier | API | Returns | Use for |
|
|
27
|
+
|---|---|---|---|
|
|
28
|
+
| 1 | `parseM0StringToLogicalFrames(s, w, h)` | rendered leaves only, **logical order**; each has `rect`, `logicalIndex` (array index === logicalIndex), `meta` | template tile mapping — `cell-<logicalIndex>`, labels indexing, "Nth tile" logic |
|
|
29
|
+
| 2 | `parseM0StringToRenderFrames(s, w, h)` | renderable frames in **paint order**; each has `rect`, `paintOrder`, `logicalIndex` | engine paint plan / ffmpeg stacking |
|
|
30
|
+
| 3 | `parseM0StringToFullGraph(s, w, h)` | EVERYTHING as EditorFrame[] — roots, groups, leaves, passthroughs, nulls, overlay subtrees | editor UIs, structural analysis |
|
|
31
|
+
|
|
32
|
+
Key invariant (Tier 2): `paintOrder` is the compositing order; `logicalIndex` is
|
|
33
|
+
the logical tile index. There is **no `sourceIndex` field** — build sources by
|
|
34
|
+
`logicalIndex`, paint by `paintOrder`.
|
|
35
|
+
|
|
36
|
+
Common mistakes the wrong tier causes: confusing paint order with logical order,
|
|
37
|
+
attaching sources to the wrong index, losing overlays/group nodes in a UI.
|
|
38
|
+
|
|
39
|
+
## Identity on every frame
|
|
40
|
+
|
|
41
|
+
All frame views carry `meta` identity: deterministic StableKey, structuralDepth,
|
|
42
|
+
overlayDepth (separate axes; overlay keys live in the `/ov{depth}c{k}` namespace and
|
|
43
|
+
never shift structural keys). The full contract: [`identity.md`](identity.md).
|
|
44
|
+
|
|
45
|
+
## Frames → sources → children (the template bridge)
|
|
46
|
+
|
|
47
|
+
The standard pattern for turning geometry into a `MosaicDocument`:
|
|
48
|
+
|
|
49
|
+
1. Parse the layout — `parseM0StringToLogicalFrames` for leaf mapping. The public
|
|
50
|
+
frames APIs already exclude null-render nodes (the parser filters
|
|
51
|
+
`nullRender` internally — you do NOT hand-filter holes/containers).
|
|
52
|
+
2. For each frame: create a child renderable (often
|
|
53
|
+
`renderNestedTemplate(cellTemplateId, props, ctx)` —
|
|
54
|
+
https://github.com/m0saic-project/m0saic-packages/blob/main/packages/template-utils/src/render/renderNestedTemplate.ts#L35), attach it under
|
|
55
|
+
`children[id]`, push a `sources[]` entry referencing it.
|
|
56
|
+
3. Return `{ m0: <same layout>, config.sources, children }` (the document's layout
|
|
57
|
+
field is `m0` — https://github.com/m0saic-project/m0saic-packages/blob/main/packages/types/src/document/document.ts#L132).
|
|
58
|
+
|
|
59
|
+
Rules that keep the bridge deterministic:
|
|
60
|
+
|
|
61
|
+
- **Child IDs come from `frame.logicalIndex`** (e.g. `cell-${logicalIndex}`) — or
|
|
62
|
+
`frame.stableKey` when identity must survive structural edits. Never bare loop
|
|
63
|
+
order. ([`identity.md`](identity.md) owns the when-to-use-which.)
|
|
64
|
+
- **Size and time from `ctx.target`, never `ctx.output`** — `ctx.output` is
|
|
65
|
+
format/codec/alpha only ([`../templates/rendering-model-contract.md`](../templates/rendering-model-contract.md) Rule 5b).
|
|
66
|
+
- **Internal cell templates** (`internal: true`, id `…/cell/v1`) are the reusable
|
|
67
|
+
building block for per-tile content — wireframe-style label/debug tiles are the
|
|
68
|
+
canonical example.
|
|
69
|
+
- Props schemas: use `definePropsSchema` with typed defaults. (There is no
|
|
70
|
+
`constraints.isM0saicLayout` — an older revision of this guidance invented it;
|
|
71
|
+
the real constraint set is `MosaicPropConstraints`,
|
|
72
|
+
https://github.com/m0saic-project/m0saic-packages/blob/main/packages/types/src/template/template.ts#L108-146`.)
|
|
73
|
+
|
|
74
|
+
## Quick decision table
|
|
75
|
+
|
|
76
|
+
- Engine paint plan / ffmpeg stacking → `parseM0StringToRenderFrames`
|
|
77
|
+
- Template tile mapping / "Nth tile" logic → `parseM0StringToLogicalFrames`
|
|
78
|
+
- Full structural tree / editor UI → `parseM0StringToFullGraph`
|
|
79
|
+
- Traversal events, or anything multi-view → `parseM0StringComplete` (+ `opts.trace`)
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
## Passthrough & Carry Semantics
|
|
2
|
+
|
|
3
|
+
`0` (passthrough) is the zero-frame donation primitive. Misused, a layout is either
|
|
4
|
+
invalid (`PASSTHROUGH_TO_NOTHING`) or geometrically wrong. This doc covers donation
|
|
5
|
+
mechanics, claimant/carry scoping, overlay behavior on zero-runs, the trailing rule,
|
|
6
|
+
and the weighted-split encoding.
|
|
7
|
+
|
|
8
|
+
> **Source of truth:** passthrough handling in [https://github.com/m0saic-dsl/m0/blob/main/packages/dsl/src/parse/m0StringParser.ts](https://github.com/m0saic-dsl/m0/blob/main/packages/dsl/src/parse/m0StringParser.ts)
|
|
9
|
+
> and validator rules in [https://github.com/m0saic-dsl/m0/blob/main/packages/dsl/src/validate/m0StringValidator.ts](https://github.com/m0saic-dsl/m0/blob/main/packages/dsl/src/validate/m0StringValidator.ts).
|
|
10
|
+
> Trailing-passthrough error code `PASSTHROUGH_TO_NOTHING` in [`errors/errors.ts`](https://github.com/m0saic-dsl/m0/blob/main/packages/dsl/src/errors/errors.ts).
|
|
11
|
+
> Validity examples below runtime-verified against https://github.com/m0saic-dsl/m0/blob/main/packages/dsl/dist on 2026-07-27.
|
|
12
|
+
|
|
13
|
+
## What `0` means
|
|
14
|
+
|
|
15
|
+
`0`:
|
|
16
|
+
|
|
17
|
+
- Consumes a split slot
|
|
18
|
+
- Produces no rendered frame
|
|
19
|
+
- Donates its space forward to the next claimant
|
|
20
|
+
|
|
21
|
+
It does NOT render. It only contributes geometry.
|
|
22
|
+
|
|
23
|
+
> Design intent: `0` exists primarily to encode weighted / percentage splits in a
|
|
24
|
+
> deterministic integer-only way.
|
|
25
|
+
|
|
26
|
+
## What is a claimant?
|
|
27
|
+
|
|
28
|
+
A claimant is the first non-zero node after a run of `0`s:
|
|
29
|
+
|
|
30
|
+
- `1` (tile)
|
|
31
|
+
- `-` (null)
|
|
32
|
+
- `N(...)` / `N[...]` (group)
|
|
33
|
+
|
|
34
|
+
When a claimant appears:
|
|
35
|
+
|
|
36
|
+
- It absorbs ALL donated space from the run
|
|
37
|
+
- It absorbs its own slot
|
|
38
|
+
- Its position snaps back to the start of the run
|
|
39
|
+
|
|
40
|
+
## Donation run example
|
|
41
|
+
|
|
42
|
+
3(0,0,1)
|
|
43
|
+
|
|
44
|
+
- First `0` → carry = 1 slot
|
|
45
|
+
- Second `0` → carry = 2 slots
|
|
46
|
+
- `1` appears → absorbs carry + own slot = 3 slots
|
|
47
|
+
|
|
48
|
+
Result: the tile spans the entire width of the classifier.
|
|
49
|
+
|
|
50
|
+
## Carry is scoped per classifier
|
|
51
|
+
|
|
52
|
+
Carry does NOT leak outside the classifier body, and resets after each claimant:
|
|
53
|
+
|
|
54
|
+
7(0{1},1,0{1},1,1,1,1)
|
|
55
|
+
|
|
56
|
+
Two separate runs:
|
|
57
|
+
|
|
58
|
+
- Run 1: `0{1}` starts carry → first `1` absorbs the run
|
|
59
|
+
- Run 2: new `0{1}` → fresh carry → next `1` absorbs it
|
|
60
|
+
|
|
61
|
+
## Overlay behavior on `0`
|
|
62
|
+
|
|
63
|
+
### `0{...}` targets the merged region so far
|
|
64
|
+
|
|
65
|
+
4(0,0{2(1,1)},0,1)
|
|
66
|
+
|
|
67
|
+
1. First `0` — carry = 1 slot
|
|
68
|
+
2. Second `0{2(1,1)}` — carry = 2 slots; overlay canvas = merged 2-slot region
|
|
69
|
+
3. Third `0` — carry = 3 slots
|
|
70
|
+
4. `1` — absorbs 4 slots total
|
|
71
|
+
|
|
72
|
+
Overlay size is fixed at the moment it appears. Later carry growth does NOT resize
|
|
73
|
+
earlier overlays.
|
|
74
|
+
|
|
75
|
+
### Multiple zero overlays grow monotonically
|
|
76
|
+
|
|
77
|
+
5(0{1},0{1},0,0,1)
|
|
78
|
+
|
|
79
|
+
- First `0{1}` → overlay spans 1 slot
|
|
80
|
+
- Second `0{1}` → overlay spans 2 slots
|
|
81
|
+
- Remaining `0`s → carry grows
|
|
82
|
+
- `1` absorbs all 5
|
|
83
|
+
|
|
84
|
+
Each overlay anchors to the region size at its creation time. Zero-overlay prefix
|
|
85
|
+
geometry as a deliberate technique (progressive region staging, area-ordered
|
|
86
|
+
layering) has its own doc: [`zero-overlay-analysis.md`](zero-overlay-analysis.md).
|
|
87
|
+
|
|
88
|
+
## Trailing passthrough rule (`PASSTHROUGH_TO_NOTHING`)
|
|
89
|
+
|
|
90
|
+
A trailing `0` inside a classifier is **always** invalid — even if it carries an
|
|
91
|
+
overlay. No claimant absorbs the donation; an overlay on `0` does not change that.
|
|
92
|
+
|
|
93
|
+
Invalid:
|
|
94
|
+
|
|
95
|
+
2(1,0) — bare trailing passthrough
|
|
96
|
+
2(1,0{1}) — trailing passthrough with overlay (still invalid)
|
|
97
|
+
3(1,1,0{1}) — trailing passthrough with overlay
|
|
98
|
+
0{1} — whole-string passthrough: no sibling exists to claim
|
|
99
|
+
|
|
100
|
+
Valid:
|
|
101
|
+
|
|
102
|
+
2(0,1) — passthrough donates to claimant
|
|
103
|
+
2(0{1},1) — passthrough with overlay, not trailing
|
|
104
|
+
3(1,0,1) — passthrough in middle position
|
|
105
|
+
|
|
106
|
+
The rule applies recursively inside nested classifiers AND overlay bodies:
|
|
107
|
+
`1{2(1,0)}` rejects with `PASSTHROUGH_TO_NOTHING`.
|
|
108
|
+
|
|
109
|
+
## Weighted split encoding
|
|
110
|
+
|
|
111
|
+
Zero-runs are how weighted splits are encoded. To encode a 60/40 split:
|
|
112
|
+
|
|
113
|
+
[60% region][40% region]
|
|
114
|
+
|
|
115
|
+
DSL representation (conceptual pattern):
|
|
116
|
+
|
|
117
|
+
100( 59 zeros, 1, 39 zeros, 1 )
|
|
118
|
+
|
|
119
|
+
The zero-run before each `1` encodes how much space that tile absorbs. Mechanical
|
|
120
|
+
and deterministic.
|
|
121
|
+
|
|
122
|
+
### The stdlib GCD-reduces this for you
|
|
123
|
+
|
|
124
|
+
Never hand-roll the 100-slot form. `weightedSplit` (and the unified `split`
|
|
125
|
+
transform's `weights` option) default to `mode: "optimized"`, which divides all
|
|
126
|
+
weights by their GCD before emitting slots:
|
|
127
|
+
|
|
128
|
+
weightedSplit([60, 40], "col") → 5(0,0,1,0,1) — not 100(...)
|
|
129
|
+
weightedSplit([50, 25, 25], "col") → 4(0,1,1,1)
|
|
130
|
+
|
|
131
|
+
Verified 2026-07-27: GCD reduction in
|
|
132
|
+
[https://github.com/m0saic-dsl/m0/blob/main/packages/dsl-stdlib/src/builders/weightedSplit.ts](https://github.com/m0saic-dsl/m0/blob/main/packages/dsl-stdlib/src/builders/weightedSplit.ts):119–122;
|
|
133
|
+
the `split` transform routes through the same reduction in
|
|
134
|
+
[`buildSplitFragment.ts`](https://github.com/m0saic-dsl/m0/blob/main/packages/dsl-stdlib/src/transforms/primitives/split/_internal/buildSplitFragment.ts)
|
|
135
|
+
(default `mode = "optimized"`). `mode: "literal"` keeps raw weights (legacy);
|
|
136
|
+
`precision` rescales weights to an exact slot budget first (largest-remainder).
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# Structural construction — emitting valid canonical m0
|
|
2
|
+
|
|
3
|
+
Operational construction rules. The Handbook is canonical for grammar
|
|
4
|
+
([`../handbook/dsl-rules.md`](../handbook/dsl-rules.md)) — this doc is the build-time
|
|
5
|
+
checklist form of the same rules; on any conflict, the handbook wins.
|
|
6
|
+
|
|
7
|
+
> **Source of truth:** [https://github.com/m0saic-dsl/m0/blob/main/packages/dsl/src/validate/m0StringValidator.ts](https://github.com/m0saic-dsl/m0/blob/main/packages/dsl/src/validate/m0StringValidator.ts)
|
|
8
|
+
> and [https://github.com/m0saic-dsl/m0/blob/main/packages/dsl/src/errors/errors.ts](https://github.com/m0saic-dsl/m0/blob/main/packages/dsl/src/errors/errors.ts).
|
|
9
|
+
|
|
10
|
+
## 1. Canonical output only
|
|
11
|
+
|
|
12
|
+
Emit `1` not `F`, `0` not `>`, no whitespace; allowed characters: `0–9 ( ) [ ] { } , -`.
|
|
13
|
+
Never emit pretty aliases.
|
|
14
|
+
|
|
15
|
+
## 2. Numeric containers must match exactly
|
|
16
|
+
|
|
17
|
+
`N(child1, …, childK)` requires `K === N`. No compression, no implicit slots, no
|
|
18
|
+
empty classifiers (`2()`, `3[]` invalid). Build children first, count them, then
|
|
19
|
+
prefix with N.
|
|
20
|
+
|
|
21
|
+
Valid: `2(1,1)` · `3[1,0,1]` Invalid: `2(1)` · `3(1,1)` · `2(1,1,1)` · `3()`
|
|
22
|
+
|
|
23
|
+
## 3. First token rule
|
|
24
|
+
|
|
25
|
+
A valid string starts with `1`, or a NUMBER > 1 followed immediately by `(` or `[`.
|
|
26
|
+
`0`, `-`, `,1`, `{1}` are invalid openers. `1(...)` / `1[...]` is
|
|
27
|
+
`ILLEGAL_ONE_SPLIT` — never wrap a single child in a pseudo-container.
|
|
28
|
+
|
|
29
|
+
## 4. NUMBER must be followed by a classifier
|
|
30
|
+
|
|
31
|
+
`2,1` and `2{1}` are invalid; `2(1,1)` and `2[1,1]` are valid.
|
|
32
|
+
|
|
33
|
+
## 5. Overlay discipline — objects vs nesting
|
|
34
|
+
|
|
35
|
+
- **At most one immediate overlay object per node**: `1{1}`, `2(1,1){1}`, `0{1}`,
|
|
36
|
+
`-{1}` valid; `1{1}{1}` invalid.
|
|
37
|
+
- **Layer via nesting**: `1{1{1}}` — base tile, overlay subtree, overlay on the
|
|
38
|
+
overlay.
|
|
39
|
+
- **Multiple overlay layers/regions via structure inside the body**:
|
|
40
|
+
`1{2(1,-){2(-,1)}}` — the overlay body is a container that owns its own overlay;
|
|
41
|
+
holes (`-`) isolate which regions show tiles. "One overlay object per node" does
|
|
42
|
+
not limit overlay depth.
|
|
43
|
+
|
|
44
|
+
## 6. No trailing passthrough
|
|
45
|
+
|
|
46
|
+
A trailing `0` inside a classifier is always invalid — **even with an overlay**
|
|
47
|
+
(`PASSTHROUGH_TO_NOTHING`): `2(1,0)` and `2(1,0{1})` invalid; `2(0,1)` and
|
|
48
|
+
`2(0{1},1)` valid. A passthrough must donate to a claimant; an overlay does not
|
|
49
|
+
exempt it.
|
|
50
|
+
|
|
51
|
+
## 7. Overlay bodies — valid, but sourceless is LEGAL
|
|
52
|
+
|
|
53
|
+
Bodies validate recursively as full m0 sub-expressions. A body must contain at
|
|
54
|
+
least one node, but it does NOT need a leaf `1` — the legacy `ZERO_SOURCE_OVERLAY`
|
|
55
|
+
rule was **relaxed 2026-06-03** and is no longer raised
|
|
56
|
+
(`m0StringValidator.ts:485`). Canonical overlay rules:
|
|
57
|
+
[`overlay-semantics.md`](overlay-semantics.md).
|
|
58
|
+
|
|
59
|
+
Invalid: `1{}` (INVALID_EMPTY) · `1{0}` (INVALID_EMPTY) · `1{2(1,0)}` (trailing
|
|
60
|
+
passthrough in body)
|
|
61
|
+
Valid: `1{1}` · `1{2(1,1)}` · `1{1{1}}` · `1{2(-,1)}` · `1{-}` · `1{2(-,-)}`
|
|
62
|
+
|
|
63
|
+
## 8. No-sources rule
|
|
64
|
+
|
|
65
|
+
The layout as a whole needs ≥1 leaf `1` (`NO_SOURCES`) — a source inside an
|
|
66
|
+
overlay counts: `2(-,-)` invalid, `2(-,-){1}` valid.
|
|
67
|
+
|
|
68
|
+
## Quick reference
|
|
69
|
+
|
|
70
|
+
```
|
|
71
|
+
VALID: 1 · 2(1,1) · 2(1{1},1) · 3[1,0{1},1] · 1{1{1}} · 1{2(1,-){2(-,1)}} · 1{-}
|
|
72
|
+
INVALID: 0 · - · 2(1) · 2(1,1,1) · 1{{1}} · 1{1}{1} · 2(1,0) · 2(1,0{1}) · 1{} · 1{0} · 2(-,-)
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## Construction procedure
|
|
76
|
+
|
|
77
|
+
1. Decide the structure shape (tree of splits + leaves).
|
|
78
|
+
2. Build child arrays per split; count them; prefix the exact numeric classifier.
|
|
79
|
+
3. Attach at most one overlay object per node; get extra layers via nesting or
|
|
80
|
+
split/hole structure inside the body.
|
|
81
|
+
4. Canonicalize.
|
|
82
|
+
5. Validate with `isValidM0String` / `validateM0String` from `@m0saic/dsl`
|
|
83
|
+
(returns `{ok: true} | {ok: false, error}` — singular `error`).
|
|
84
|
+
6. On failure: discard and rebuild. Never patch a broken string by hand — fix the
|
|
85
|
+
generator ([`m0saic-string-generation.md`](m0saic-string-generation.md) §3 has
|
|
86
|
+
the per-code repair moves).
|