@m0saic/knowledge 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +71 -0
  3. package/dist/index.d.ts +8 -0
  4. package/dist/index.js +7 -0
  5. package/docs/README.md +60 -0
  6. package/docs/file-formats/m0-iteration-protocol.md +120 -0
  7. package/docs/file-formats/m0p-and-custom-field.md +195 -0
  8. package/docs/handbook/README.md +27 -0
  9. package/docs/handbook/composition-arithmetic.md +278 -0
  10. package/docs/handbook/dsl-complexity.md +75 -0
  11. package/docs/handbook/dsl-rules.md +367 -0
  12. package/docs/handbook/feasibility-precision-quantization.md +591 -0
  13. package/docs/handbook/m0-construction-methods.md +201 -0
  14. package/docs/handbook/precision-tiers.md +84 -0
  15. package/docs/m0saic-thesis.md +95 -0
  16. package/docs/runtime/README.md +17 -0
  17. package/docs/runtime/cli-usage.md +372 -0
  18. package/docs/runtime/ffmpeg-expression-limits.md +117 -0
  19. package/docs/runtime/reduce-to-one.md +96 -0
  20. package/docs/skills/README.md +40 -0
  21. package/docs/skills/axis-and-geometry.md +103 -0
  22. package/docs/skills/dsl-stdlib-method-catalog.md +7 -0
  23. package/docs/skills/identity.md +123 -0
  24. package/docs/skills/labels-and-masks.md +170 -0
  25. package/docs/skills/m0saic-string-generation.md +251 -0
  26. package/docs/skills/more-atoms-not-bigger-atoms.md +77 -0
  27. package/docs/skills/overlay-semantics.md +194 -0
  28. package/docs/skills/parse-apis.md +79 -0
  29. package/docs/skills/passthrough-semantics.md +136 -0
  30. package/docs/skills/structural-construction.md +86 -0
  31. package/docs/skills/text-in-templates.md +126 -0
  32. package/docs/skills/zero-overlay-analysis.md +87 -0
  33. package/docs/templates/README.md +65 -0
  34. package/docs/templates/capability-templates.md +72 -0
  35. package/docs/templates/construction-strategy.md +329 -0
  36. package/docs/templates/data-pipeline.md +324 -0
  37. package/docs/templates/emission-patterns.md +130 -0
  38. package/docs/templates/geometry-recipes.md +248 -0
  39. package/docs/templates/layout-contract.md +168 -0
  40. package/docs/templates/output-resolution-tree.md +202 -0
  41. package/docs/templates/patterns/case-study-lessons.md +69 -0
  42. package/docs/templates/patterns/perf-authoring-rules.md +100 -0
  43. package/docs/templates/patterns/primitive-extraction-pattern.md +103 -0
  44. package/docs/templates/philosophy-and-contract.md +310 -0
  45. package/docs/templates/recursion-nested-rendering.md +138 -0
  46. package/docs/templates/reference/grid.md +104 -0
  47. package/docs/templates/reference/json-prop-type.md +169 -0
  48. package/docs/templates/reference/mosaic-color.md +81 -0
  49. package/docs/templates/reference/mosaic-placement-props.md +103 -0
  50. package/docs/templates/reference/prop-bindings.md +203 -0
  51. package/docs/templates/reference/template-flags.md +205 -0
  52. package/docs/templates/render-lifecycle.md +117 -0
  53. package/docs/templates/rendering-model-contract.md +392 -0
  54. package/docs/templates/standalone-pack-authoring.md +233 -0
  55. package/docs/templates/theming.md +81 -0
  56. package/docs/templates/ui-controls.md +150 -0
  57. package/package.json +37 -0
@@ -0,0 +1,103 @@
1
+ ## Axis & Geometry (Splits, Pixels, Remainders)
2
+
3
+ The math layer of the walk: exact rectangle geometry from `()` and `[]`,
4
+ deterministic pixel-remainder distribution (`splitEven`), and how `0`-runs and `-`
5
+ holes shape final rects.
6
+
7
+ > **Source of truth:** `splitEven` 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
+ > (line 478 as of 2026-07-27). Locked behavior: outside-in remainder distribution.
9
+ > Test fixtures in [https://github.com/m0saic-dsl/m0/blob/main/packages/dsl/src/parse/outside-in-remainder.test.ts](https://github.com/m0saic-dsl/m0/blob/main/packages/dsl/src/parse/outside-in-remainder.test.ts).
10
+
11
+ ## Axis semantics
12
+
13
+ - Column split `N( ... )` — splits **width** into N segments; height stays the
14
+ container's height.
15
+ - Row split `N[ ... ]` — splits **height** into N segments; width stays the
16
+ container's width.
17
+
18
+ Children are laid out in order:
19
+
20
+ - Columns advance **x** (left → right)
21
+ - Rows advance **y** (top → bottom)
22
+
23
+ ## splitEven pixel distribution (deterministic)
24
+
25
+ When splitting an integer pixel length (`total`) into `parts`:
26
+
27
+ - `base = floor(total / parts)`
28
+ - `remainder = total - base * parts`
29
+ - The `remainder` extra pixels are distributed **outside-in**: indices
30
+ 0, P-1, 1, P-2, 2, P-3, ...
31
+ - The remaining segments get `base`
32
+
33
+ Example:
34
+
35
+ - `total = 1080`, `parts = 7`
36
+ - `base = 154`, `remainder = 2`
37
+ - Outside-in order: index 0 gets +1, then index 6 gets +1
38
+ - Sizes: `[155, 154, 154, 154, 154, 154, 155]`
39
+
40
+ Key consequence: remainders distribute symmetrically from the edges inward, keeping
41
+ layouts visually balanced.
42
+
43
+ ## Coordinate propagation (rects)
44
+
45
+ Every node owns a rectangle `(x, y, width, height)`. Splits carve that rect into
46
+ child rects:
47
+
48
+ - Column split: child width from `splitEven(width, N)`; child x = cumulative sum of
49
+ prior child widths; y/height unchanged.
50
+ - Row split: child height from `splitEven(height, N)`; child y = cumulative sum of
51
+ prior child heights; x/width unchanged.
52
+
53
+ Overlays always reuse the **owner rect** (same x/y/width/height) and never escape it.
54
+
55
+ ## How `0` and `-` affect geometry
56
+
57
+ ### `0` (passthrough)
58
+
59
+ - Consumes a split slot; adds its slot size into "carry"
60
+ - The claimant absorbs carry + its own slot, and snaps back to the run start position
61
+
62
+ Geometry impact: `0` changes the eventual claimant rect size and origin. `0` itself
63
+ creates no visible rect (but may create overlay rects via `0{...}`).
64
+
65
+ ### `-` (null)
66
+
67
+ - Consumes a split slot; does NOT donate forward
68
+ - Creates a hole / gutter region; can still host overlays (`-{...}`)
69
+
70
+ Geometry impact: a stable region that does not affect siblings beyond consuming its
71
+ share of split space. Useful for isolating sub-layouts and masks.
72
+
73
+ ## Why resolution matters (integer pixels)
74
+
75
+ All geometry is integer-pixel based. Two implications:
76
+
77
+ 1. A layout can look "perfect" at one size and slightly biased at another, because
78
+ remainder pixels distribute outside-in (edges first).
79
+ 2. Bitmap-like layouts (e.g. 64×64 encodings) require compatible output sizes,
80
+ otherwise meaning drifts due to integer rounding distribution.
81
+
82
+ Guidance: if a layout encodes literal grid art, render at sizes aligned to that grid
83
+ (multiples of the grid resolution). For feasibility guards, quantization strategy,
84
+ and quantization-free construction, see
85
+ [`../handbook/feasibility-precision-quantization.md`](../handbook/feasibility-precision-quantization.md).
86
+
87
+ ## Minimal examples
88
+
89
+ Even-ish columns — `3(1,1,1)` at width=10:
90
+
91
+ - sizes = `[4,3,3]` (1 remainder pixel goes to the first edge)
92
+
93
+ Rows — `2[1,1]` at height=5:
94
+
95
+ - sizes = `[3,2]`
96
+
97
+ Donation changes claimant geometry — `3(0,1,1)`:
98
+
99
+ - first tile claims 2 slots (`0` + its own)
100
+
101
+ Hole creates gutter — `3(1,-,1)`:
102
+
103
+ - middle region is an explicit empty gutter
@@ -0,0 +1,7 @@
1
+ # dsl-stdlib method catalog → see the package
2
+
3
+ The full `@m0saic/dsl-stdlib` method catalog (builders, transforms, queries, masks, SVG import, specialized generators) is **colocated with the package** so it can't drift from the code:
4
+
5
+ ➡️ [https://github.com/m0saic-dsl/m0/blob/main/packages/dsl-stdlib/.ai/method-catalog.md](https://github.com/m0saic-dsl/m0/blob/main/packages/dsl-stdlib/.ai/method-catalog.md)
6
+
7
+ That doc is regenerated/reviewed in the same change that alters the API. This file is only a pointer — don't duplicate the catalog here.
@@ -0,0 +1,123 @@
1
+ # Identity — StableKey, depth axes, and selection
2
+
3
+ Merged 2026-07-27 from four overlapping docs (stablekey-and-overlay-identity,
4
+ identity-model, structural-vs-overlay-depth, structural-identity_visual-identity —
5
+ deleted; git history has the long forms). One doc, one owner: everything about
6
+ *referring to the same node* across parses, edits, and renders.
7
+
8
+ > **Source of truth:** `StableKey` + `M0NodeIdentity` in
9
+ > [https://github.com/m0saic-dsl/m0/blob/main/packages/dsl/src/types.ts](https://github.com/m0saic-dsl/m0/blob/main/packages/dsl/src/types.ts); identity
10
+ > construction in [`parse/m0StringParser.ts`](https://github.com/m0saic-dsl/m0/blob/main/packages/dsl/src/parse/m0StringParser.ts)
11
+ > (search `buildIdentity`, `makeStableKeySegment`).
12
+
13
+ ## 1. The identity contract
14
+
15
+ Every parsed node receives a deterministic `stableKey` — the address of the parser's
16
+ walk through **structure**. Segments encode node kind + split axis + childIndex among
17
+ structural siblings. Segment grammar (verified at runtime): `r` (root) ·
18
+ `g{row|col}c{n}` (group) · `fc{n}` (frame) · `pc{n}` (passthrough) · `nc{n}` (null) ·
19
+ `/ov{d}c{k}` (overlay namespace).
20
+
21
+ Same canonical string → same StableKeys, same structuralDepth, same overlay
22
+ namespace. If the string changes, the tree changes and some keys change — that is
23
+ correct, not a bug.
24
+
25
+ **StableKey is NOT:** `paintOrder` / `stackOrder` (paint depends on overlay-deferral
26
+ rules), display/label numbering, or geometry. Geometry is *derived* —
27
+ `rect = f(m0, width, height)` — and shifts with resolution and `splitEven`
28
+ remainders while keys stay put. The DSL guarantees **structural** identity, never
29
+ **visual** identity.
30
+
31
+ ## 2. The five axes — keep them separate
32
+
33
+ | Axis | Increments when | Use it for |
34
+ |---|---|---|
35
+ | `structuralDepth` (`meta.structuralDepth`) | entering `N(...)` / `N[...]` | tree indentation, containment reasoning |
36
+ | `overlayDepth` | entering `{...}` | layer filtering |
37
+ | `paintOrder` / `stackOrder` | derived from traversal + overlay deferral | paint simulation ONLY |
38
+ | `logicalIndex` | per rendered leaf, logical order | source assignment (`sources[i]`) |
39
+ | `stableKey` | — (structural address) | persistent identity across edits |
40
+
41
+ Worked example — `2(2(1,1),1)` (verified at runtime): the outermost split **IS the
42
+ root** (`stableKey: r`, structuralDepth 0); the inner group is depth **1**
43
+ (`r/gcolc0`); its tiles are depth **2** (`r/gcolc0/fc0`, `fc1`); the outer `1` is
44
+ depth **1** (`r/fc1`). There is no extra root node above the outermost split.
45
+
46
+ `1{1{1}}` has structuralDepth 0 on all three nodes and overlayDepth 0/1/2 — the two
47
+ depth systems are orthogonal: deep structure with zero overlays, flat structure with
48
+ 100 overlays, or both.
49
+
50
+ Never assume `deeper == painted later` — overlay deferral (and area-descending
51
+ zero-overlay stacking) controls paint timing, not depth. See
52
+ [`overlay-semantics.md`](overlay-semantics.md).
53
+
54
+ > ⚠️ overlayDepth is structurally free but NOT render-free: each level is a
55
+ > sequential blend pass, and inline-masks silently drop past ~25 nested layers
56
+ > (engine wall W3 — internal: `.ai/moat/runtime/ffmpeg-limitations.md`).
57
+
58
+ ## 3. The overlay invariant
59
+
60
+ Overlay subtrees do NOT affect structural childIndex assignment:
61
+
62
+ - Adding/removing overlays never renumbers the base tree.
63
+ - Overlay frames live in a separate namespace — `<ownerStableKey>/ov<depth>c<k>`
64
+ (`r/fc0` → `r/fc0/ov1c0` → `r/fc0/ov1c0/ov2c0`) — so overlay keys cannot collide
65
+ with or shift structural keys.
66
+
67
+ This is why overlays can be used freely (debug layers, labels, reveals) without
68
+ destabilizing the base layout or any tool keyed on it.
69
+
70
+ ## 4. logicalIndex vs paintOrder
71
+
72
+ RenderFrames carry both `paintOrder` (stacking order) and `logicalIndex` (logical
73
+ tile index — same as `LogicalFrame.logicalIndex`; there is **no `sourceIndex`
74
+ field** in https://github.com/m0saic-dsl/m0/blob/main/packages/dsl). Build sources in logical order, paint in stack order —
75
+ the split keeps the source array clean of paint semantics. See
76
+ [`parse-apis.md`](parse-apis.md) for which parse API returns which.
77
+
78
+ ## 5. Stability limits — what edits do to keys
79
+
80
+ Keys **WILL change** when you: change split counts, insert/remove siblings in the
81
+ base tree, change axis/group nesting. Keys will **NOT change** when you: add/remove
82
+ overlays, deepen overlay nesting, reorder overlay-only layers.
83
+
84
+ Overlay data attaches to `ownerStableKey`, so across edits: split *inside* the owner
85
+ → owner survives → overlay stays valid; edit a sibling → unaffected; delete/replace
86
+ the owner → the overlay has no owner and cannot reattach. That last case is correct
87
+ behavior, not data loss to be papered over.
88
+
89
+ **Never reattach by geometry.** Preserving overlays via nearest-rect / similar-size /
90
+ pixel-overlap heuristics introduces non-determinism and unfixable edge cases. If the
91
+ owner is gone, the overlay is gone. Determinism > convenience.
92
+
93
+ ## 6. Selection policies (agent workflows)
94
+
95
+ When you need to "pick a tile" — recursive growth, demos, batch transforms — pick by
96
+ StableKey, not geometry or paint order. Deterministic policies:
97
+
98
+ - **Smallest StableKey lexicographically** — stable across edits, cheap.
99
+ - **Earliest leaf in StableKey-sorted order** — a "top-left-most" that survives
100
+ structural change.
101
+ - **`(seed + step) mod leafCount`** over a StableKey-sorted leaf list — round-robin
102
+ demos where each step targets a different tile.
103
+
104
+ Anti-patterns: selecting by pixel position (resolution change → different
105
+ selection), by paint order (adding overlays shifts it), or by unsorted traversal
106
+ order.
107
+
108
+ ## 7. Labeling
109
+
110
+ Label tiles by StableKey (`.m0c` labels are StableKey-keyed); use the label `text`
111
+ as the human-meaningful join key (`.m0p` regions reference labels by text). If a
112
+ label's key no longer exists in the current parse, `validateLabels`
113
+ (`@m0saic/dsl-file-formats`) flags it — see
114
+ [`../file-formats/m0p-and-custom-field.md`](../file-formats/m0p-and-custom-field.md) §5.
115
+
116
+ ## 8. Common mistakes
117
+
118
+ - Treating overlay tiles as structural leaves — scope "leaves" iterations to the
119
+ base namespace; overlays live under `/ov…`.
120
+ - Confusing overlayDepth for structuralDepth (UI tree depth ≠ "inside another split").
121
+ - Hardcoding StableKey strings — they're stable across *layout* edits but computed
122
+ from structure; always derive them from a parse, never string-match literals.
123
+ - Assuming paintOrder reflects structure — it's derived, overlay rules own it.
@@ -0,0 +1,170 @@
1
+ # Labels and Masks
2
+
3
+ How per-leaf **labels** and **masks** travel from a dictionary entry, through a
4
+ generator, into the editor, and back to disk. The surface spans four packages; this is
5
+ the single map.
6
+
7
+ ---
8
+
9
+ ## 1. What lives where
10
+
11
+ | Layer | Shape | Home |
12
+ |---|---|---|
13
+ | **On disk** | `M0cFile` — `m0` + `labels` (`Record<StableKey, M0Label>`) + `masks` (`Record<StableKey, M0cMaskEntry \| null>`) + `fill` + `size` | https://github.com/m0saic-dsl/m0/blob/main/packages/dsl-file-formats/src/types.ts |
14
+ | **Dictionary entry** | `MosaicDictionaryEntry.labels` / `.masks` — same maps at runtime | https://github.com/m0saic-project/m0saic-packages/blob/main/packages/types/src/dictionary/dictionary.ts |
15
+ | **Editor (live)** | `m0cExtrasRef` — `{ labels, masks, derive, custom }` for the open canvas | the Mosaic Desktop / Web app source (not published) |
16
+ | **Generator output** | `GeneratorResult.m0c?: string` — optional serialized blob | https://github.com/m0saic-project/m0saic-packages/blob/main/packages/dictionary/src/generators |
17
+
18
+ > **`labels[k].color` ≠ the rect fill.** `labels[k].color` tints the *badge*; `fill[k]`
19
+ > paints the *rect*. The inspector once conflated them — don't re-conflate.
20
+
21
+ ---
22
+
23
+ ## 2. Dictionary entry convention
24
+
25
+ - An entry MAY ship **`m0saic.m0c`** instead of `m0saic.m0`. The `.m0c` is
26
+ authoritative; `loadEntryM0()` auto-detects by filename and **throws if both exist**.
27
+ - **`m0saic_src.m0`** is a read-only sibling auto-regenerated by `validate.js`, carrying
28
+ a `# note:` header. It exists for grep-by-DSL and human inspection. **No runtime reads
29
+ it** — never edit it, never treat it as a source.
30
+ - **`entry.masks`** (StableKey-keyed) is the canonical mask path. `entry.maskSets` /
31
+ `getMaskSet()` is reserved for future *named alternate* sets — no current entry uses
32
+ it. Don't reach for it.
33
+ - Positional lookup goes through `getSourceOrderStableKeys(entry)`
34
+ (https://github.com/m0saic-project/m0saic-packages/blob/main/packages/dictionary/src/sourceOrder.ts#L50, memoized per `(entry.id, m0.length)`).
35
+ See logo v2/v3 and qr-animate.
36
+ ⚠️ **It throws when `entry.m0` isn't inlined.** Heavy entries carry a lazy
37
+ `entry.m0File` instead. For those, call `resolveM0saic(entry)` first and pass the
38
+ resolved string to the companion **`getSourceOrderStableKeysForM0(id, m0, w, h)`**.
39
+ The error message says this, but only after you've already hit it.
40
+ - One-shot migrations live in **https://github.com/m0saic-project/m0saic-packages/blob/main/packages/dictionary/tools** (not repo-root `tools/`):
41
+ `migrate-masks-to-m0c.mjs` converts legacy `masks.json` + `.m0` → `.m0c`;
42
+ `normalize-mask-paths.mjs` runs the drop-rect-shaped + snap-to-bounds cleanup. Both
43
+ route through `parseM0StringComplete` to turn positional masks into per-StableKey
44
+ entries.
45
+
46
+ ---
47
+
48
+ ## 3. Generator label tiers
49
+
50
+ Every label-emitting generator exposes `labels: LabelTier`
51
+ (`"silent" | "signposts" | "atlas"`) built via `labelTierParam({ defaultTier })`
52
+ (https://github.com/m0saic-project/m0saic-packages/blob/main/packages/dictionary/src/generators/labelTier.ts).
53
+
54
+ | Tier | Emits |
55
+ |---|---|
56
+ | `silent` | plain m0, no labels |
57
+ | `signposts` | named structural regions only ("hero", "feature") |
58
+ | `atlas` | signposts + per-cell / per-level enumeration |
59
+
60
+ `off` is accepted as a spelling alias for `silent`; **`silent` is the canonical wire
61
+ value**. Resolve untrusted input with `resolveLabelTier(raw, fallback)`.
62
+
63
+ ### Per-generator defaults and emission path
64
+
65
+ Two emission paths, and which one a generator uses is not arbitrary:
66
+
67
+ - **`emitLabeledM0c({ m0, size, app, tier, labelsBySourceIndex, keyedLabels })`** —
68
+ use when labels map **positionally** to logical source order. Handles
69
+ source-index → stableKey translation, including a feasibility-fallback probe
70
+ scale-up for high-precision layouts.
71
+ - **Direct `serializeM0cFile`** — use when the generator must **classify nulls by
72
+ position** or carries masks. Parse the m0, compute the placed-content bbox, classify
73
+ each `kind: "null"` editorFrame by centroid offset into `padding-top` /
74
+ `padding-right` / `padding-bottom` / `padding-left`. The same algorithm appears in
75
+ every padding-classifying generator — copy it, don't re-derive it.
76
+
77
+ | Generator | Default tier | Path |
78
+ |---|---|---|
79
+ | `qrCodeGenerator` | **`signposts`** | direct |
80
+ | `barcodeGenerator` | **`signposts`** | direct |
81
+ | `aspectFit` | `silent` | direct (padding classification) |
82
+ | `placeRect` | `silent` | direct (padding classification) |
83
+ | `snapGrid` | `silent` | direct (padding classification) |
84
+ | `aspectSafeGrid` | `silent` | direct (padding classification) |
85
+ | `grid` | `silent` | `emitLabeledM0c` |
86
+ | `spotlightGenerator` | `silent` | `emitLabeledM0c` |
87
+ | `magazineGenerator` | `silent` | `emitLabeledM0c` |
88
+ | `comparisonGenerator` | `silent` | `emitLabeledM0c` |
89
+ | `rankedListGenerator` | `silent` | `emitLabeledM0c` |
90
+ | `goldenLayout` | `silent` | `emitLabeledM0c` |
91
+ | `goldenSpiralGenerator` | `silent` | `emitLabeledM0c` |
92
+ | `safeCanvas` | `silent` | `emitLabeledM0c` |
93
+
94
+ **`silent` is the default everywhere except the two code generators** (QR, barcode),
95
+ which default to `signposts` to preserve their always-on label channel.
96
+
97
+ *Keep this table in sync when adding a generator — verify with
98
+ `grep -A3 "labelTierParam(" https://github.com/m0saic-project/m0saic-packages/blob/main/packages/dictionary/src/generators/*.ts.*
99
+
100
+ ---
101
+
102
+ ## 4. Editor rendering and drop rules
103
+
104
+ **Chip render gate** — `Frame.tsx`: `canShowLabel = hasLabel && minPx >= 70`. There is
105
+ **no kind filter here**: frames, plain nulls, and passthroughs all wear chips once
106
+ mounted. Visibility moved upstream to the `showNullFrames` toggle, which controls
107
+ whether nulls reach `displayFrames` at all.
108
+
109
+ **Drop-label remap** — `ViewFrame.tsx` `buildDropZone`:
110
+
111
+ ```ts
112
+ const isLabelable = (f) => f.kind === "frame" || f.kind === "null";
113
+ ```
114
+
115
+ ⚠️ **The root node is deliberately excluded.** Including it shifts every
116
+ position-slice mapping by one — the off-by-one that mislabeled aspectFit's `content`.
117
+ Bare-`F` drops (canvas was `"r"`) take a separate path: the new canvas *is* the dropped
118
+ m0, so labels apply with their original keys.
119
+
120
+ > **Name collision — two different `isLabelable`.** `ViewFrame`'s is **node-kind**
121
+ > based (above). `App.tsx`'s is **format** based
122
+ > (`loadedFormat === "m0c" || loadedFormat === "m0p"` — can this file carry labels at
123
+ > all). Same identifier, unrelated meanings, different files. Check which one you're
124
+ > reading.
125
+
126
+ **Why no `isLogicalOwner` requirement.** aspectFit-style generators label plain `-`
127
+ nulls (border padding). Requiring `-{...}` overlay anchors would force every
128
+ padding-labeling generator to wrap each null in an overlay purely to make it visible.
129
+ The relaxed rule lets any structural node carry a label, matching the `.m0c` format's
130
+ own permissiveness.
131
+
132
+ ---
133
+
134
+ ## 5. Undo participation
135
+
136
+ `useUndoRedoText(initial, { onCommit, onUndo, onRedo })` exposes side-channel
137
+ callbacks. `App.tsx` snapshots `{ labels, masks }` from `m0cExtrasRef` into a parallel
138
+ stack on every text commit, and restores on undo/redo.
139
+
140
+ - Manual `setFrameLabel` edits **between** commits don't push their own entry — they're
141
+ absorbed into the *next* commit's snapshot. So manual label → text edit → Ctrl-Z
142
+ restores the manual label too.
143
+ - Drop-derived labels vanish cleanly on Ctrl-Z because the snapshot at the drop's commit
144
+ is taken **before** the handler merges drop labels (`setProfileText` calls `onCommit`
145
+ synchronously, ahead of the `m0cExtrasRef` mutation). This ordering is load-bearing.
146
+
147
+ ---
148
+
149
+ ## 6. UI affordances
150
+
151
+ - **The dictionary panel's drag chip is the sole apply path.** Click-drag splices into a
152
+ frame; double-click replaces the canvas (confirm modal when the canvas has structure;
153
+ bare-`F` skips the prompt). The standalone "Use Layout" button was **removed** — it
154
+ duplicated the pipeline and kept drifting.
155
+ - **Save-As gates `.m0`** whenever labels or masks exist (`m0SaveBlocked` in `App.tsx`).
156
+ Both the Cmd/Ctrl+S guard and the format chooser respect it — a `.m0` save would
157
+ silently drop the sidecar maps.
158
+
159
+ ---
160
+
161
+ ## 7. Settled items (don't re-debate)
162
+
163
+ - `aspectSafeGrid` carries m0c only on the **landscape primary** variant; portrait
164
+ alternates don't support m0c yet. Noted in its labels-param description.
165
+ - `LabelTier` naming is **settled**. Renaming touches ~14 generators plus the panel UI.
166
+ - Outer-gutter nulls in `safeCanvas` are **not** labeled — canvas margin, not
167
+ addressable cells.
168
+
169
+ **Related:** `file-formats/m0p-and-custom-field.md` covers `.m0c`'s sibling format but
170
+ predates this convention — read this doc for the entry convention and tier param.
@@ -0,0 +1,251 @@
1
+ # m0 Generator Guidelines
2
+
3
+ Practical algorithm outline for generating valid m0 strings programmatically. **Prefer official helpers over hand-emitting tokens.**
4
+
5
+ > **Source of truth:**
6
+ > - Builders: [https://github.com/m0saic-dsl/m0/blob/main/packages/dsl-stdlib/src/builders](https://github.com/m0saic-dsl/m0/blob/main/packages/dsl-stdlib/src/builders)
7
+ > - Unified transforms: [https://github.com/m0saic-dsl/m0/blob/main/packages/dsl-stdlib/src/transforms/unified](https://github.com/m0saic-dsl/m0/blob/main/packages/dsl-stdlib/src/transforms/unified)
8
+ > - Validation: [https://github.com/m0saic-dsl/m0/blob/main/packages/dsl/src/validate](https://github.com/m0saic-dsl/m0/blob/main/packages/dsl/src/validate)
9
+ > - Feasibility: [https://github.com/m0saic-dsl/m0/blob/main/packages/dsl/src/feasibility](https://github.com/m0saic-dsl/m0/blob/main/packages/dsl/src/feasibility)
10
+
11
+ ---
12
+
13
+ ## 0. Prime directives
14
+
15
+ ### 0.1 Prefer library ops over hand-writing strings
16
+
17
+ If an official op/helper exists for the transformation you want, **use it** instead of emitting raw m0 tokens.
18
+
19
+ Three primary sources of safe generation:
20
+
21
+ - **`@m0saic/dsl-stdlib/builders`** — high-level builders (`grid`, `weightedSplit`, `strip`, `container`, `aspectFit`, `placeRect`/`placeRects`, `rectsToM0`, `safeCanvas`, `snapGridFit`, `spotlight`, `comparison`, `rankedList`, `goldenSplit`, `goldenSpiral`, etc.). Full surface: see [`dsl-stdlib-method-catalog.md`](./dsl-stdlib-method-catalog.md).
22
+ - **`@m0saic/dsl-stdlib/transforms`** — unified ops (`split`, `replace`, `addOverlay`, `removeOverlay`, `setTileType`, `measureSplit`, `swapFrames`).
23
+ - **`@m0saic/dsl`** — low-level parsing + validation; never the right tool for *generation*.
24
+
25
+ Raw string emission is a **last resort.**
26
+
27
+ ### 0.2 Use `grid` for grids
28
+
29
+ For row/column grids (equal cells, weighted cells, gutters, outer gutters), call:
30
+
31
+ ```ts
32
+ import { grid } from "@m0saic/dsl-stdlib";
33
+
34
+ const result = grid({ rows: 3, cols: 4, gutter: 0.05 });
35
+ // result.m0 — canonical m0 string
36
+ // result.totalX, result.totalY — split factors
37
+ // result.cellW, result.gutterW — resolved weights
38
+ ```
39
+
40
+ The builder handles weighted-split expansion via `0` carry runs, gutter `-` slots, the illegal `1(...)` / `1[...]` rule, exact classifier-count = child-slot count, and canonical output. **Don't hand-author split trees.**
41
+
42
+ Workflow: build the base layout with a builder, apply targeted edits with unified transforms, validate.
43
+
44
+ ---
45
+
46
+ ## 1. Validation and feasibility
47
+
48
+ ### 1.1 Always validate
49
+
50
+ ```ts
51
+ import { validateM0String } from "@m0saic/dsl";
52
+
53
+ const v = validateM0String(m0);
54
+ if (!v.ok) {
55
+ // v.error is a single structured violation: { code, kind, message, ... }
56
+ // (singular — https://github.com/m0saic-dsl/m0/blob/main/packages/dsl/src/errors/errors.ts M0ValidationResult)
57
+ }
58
+ ```
59
+
60
+ Boolean shortcut: `isValidM0String(m0)`.
61
+
62
+ The validator enforces grammar, token rules, overlay correctness, child-count exactness, the trailing-passthrough rule (trailing `0` is always invalid, even with overlay), and the no-sources rule (≥1 leaf `1` required somewhere in the layout). Overlay bodies do NOT need their own source — sourceless bodies like `1{-}` are legal (relaxed 2026-06-03; see [`overlay-semantics.md`](overlay-semantics.md)). See [`handbook/dsl-rules.md`](../handbook/dsl-rules.md) for the full grammar.
63
+
64
+ ### 1.2 Check feasibility when dimensions are known
65
+
66
+ ```ts
67
+ import { computeFeasibility } from "@m0saic/dsl";
68
+
69
+ const f = computeFeasibility(m0);
70
+ if (targetWidth < f.minWidthPx || targetHeight < f.minHeightPx) {
71
+ // Layout cannot render at this resolution — would produce 0-size frames.
72
+ throw new Error(`Below feasible: ${f.minWidthPx}×${f.minHeightPx}`);
73
+ }
74
+ ```
75
+
76
+ If you parse anyway, `parseM0StringComplete` emits `SPLIT_EXCEEDS_AXIS` at parse time:
77
+
78
+ ```ts
79
+ import { parseM0StringComplete } from "@m0saic/dsl";
80
+
81
+ const parsed = parseM0StringComplete(m0, width, height);
82
+ // parsed.precision, parsed.warnings, optional parsed.error
83
+ ```
84
+
85
+ ### 1.3 Feasibility vs. precision (important distinction)
86
+
87
+ - **`computeFeasibility`** — exact `{ minWidthPx, minHeightPx }`. Accounts for nested same-axis splits, passthrough carry chains, overlays. Use for all feasibility decisions.
88
+ - **Precision** is a separate, independent floor (neither bounds the other). For a combined verdict use `evaluateM0` from `@m0saic/dsl-stdlib` — it returns `feasible`, `meetsPrecision`, and `recommendedMin` (the per-axis max of both floors). (`computePrecisionFromString` is an internal dsl helper, not public API.)
89
+
90
+ See [`../handbook/feasibility-precision-quantization.md`](../handbook/feasibility-precision-quantization.md) (canonical) for the two-floor model.
91
+
92
+ ---
93
+
94
+ ## 2. Unified transforms — the canonical API for structural changes
95
+
96
+ When you mutate an existing string, use the unified transforms exported from `@m0saic/dsl-stdlib`. Every transform takes `(m0, target, options) → m0` where `target: TransformTarget` is a discriminated union.
97
+
98
+ ### TransformTarget
99
+
100
+ ```ts
101
+ type TransformTarget =
102
+ | { by: "logicalIndex"; index: number } // 0-based index of rendered frames (1/F only)
103
+ | { by: "span"; span: { start: number; end: number } } // exact char span
104
+ | { by: "stableKey"; key: string }; // structural identity path
105
+ ```
106
+
107
+ Pick the addressing mode that matches your invariant — `logicalIndex` for "the Nth rendered tile," `stableKey` for "this specific node, even after edits," `span` for parser-visible ranges. See [`../file-formats/m0p-and-custom-field.md`](../file-formats/m0p-and-custom-field.md) and [`identity.md`](identity.md) for identity semantics.
108
+
109
+ ### 2.1 Split
110
+
111
+ ```ts
112
+ import { split } from "@m0saic/dsl-stdlib";
113
+
114
+ // Uniform 3-way column split on the 0th tile
115
+ split(m0, { by: "logicalIndex", index: 0 }, { axis: "col", count: 3 });
116
+
117
+ // Weighted split: weights.length must equal count
118
+ split(m0, { by: "logicalIndex", index: 0 }, { axis: "col", count: 2, weights: [60, 40] });
119
+ ```
120
+
121
+ GCD-reduces emitted DSL by default. Pass `weightMode: "literal"` to preserve exact slot count.
122
+
123
+ ### 2.2 Measure-mode split
124
+
125
+ ```ts
126
+ import { measureSplit } from "@m0saic/dsl-stdlib";
127
+
128
+ // axis "col" or "row"; count = granularity; ranges are {a, b} objects (inclusive)
129
+ // (MeasureSplitOptions — https://github.com/m0saic-dsl/m0/blob/main/packages/dsl-stdlib/src/transforms/unified/measureSplit.ts#L9)
130
+ measureSplit(m0, { by: "logicalIndex", index: 1 }, { axis: "row", count: 12, ranges: [{ a: 2, b: 8 }] });
131
+ ```
132
+
133
+ ### 2.3 Set leaf type
134
+
135
+ ```ts
136
+ import { setTileType } from "@m0saic/dsl-stdlib";
137
+
138
+ // Change a leaf to "-" (null), ">" (passthrough), "F" (frame), or "1"
139
+ setTileType(m0, { by: "logicalIndex", index: 0 }, "-");
140
+ setTileType(m0, { by: "stableKey", key: "r/growc0/fc1" }, ">");
141
+ ```
142
+
143
+ ### 2.4 Add / remove overlay
144
+
145
+ ```ts
146
+ import { addOverlay, removeOverlay } from "@m0saic/dsl-stdlib";
147
+
148
+ addOverlay(m0, { by: "logicalIndex", index: 2 }); // visual overlay
149
+ removeOverlay(m0, { by: "stableKey", key: "r/fc2" });
150
+ ```
151
+
152
+ ### 2.5 Replace / swap
153
+
154
+ ```ts
155
+ import { replace, swapFrames } from "@m0saic/dsl-stdlib";
156
+
157
+ replace(m0, { by: "logicalIndex", index: 0 }, "2(F,F)"); // replace subtree at target
158
+ swapFrames(m0, 0, 1); // swap rendered tiles — takes two logicalIndex numbers
159
+ // directly, NOT a TransformTarget (the one exception to §2)
160
+ ```
161
+
162
+ ---
163
+
164
+ ## 3. Repair-loop strategy
165
+
166
+ When validation fails, apply targeted repairs instead of regenerating from scratch.
167
+
168
+ ### 3.1 ILLEGAL_ONE_SPLIT
169
+
170
+ Problem: `1(...)` or `1[...]` — wrapping a single child as a pseudo-container is illegal.
171
+
172
+ Fix: drop the `1(...)` wrapper or use `0`-runs instead.
173
+
174
+ ### 3.2 Trailing passthrough
175
+
176
+ Problem: last child in a split is `0` (even with overlay — `0{...}` trailing is always invalid).
177
+
178
+ Fix:
179
+ - Replace trailing `0` with `-`: `2(1,0)` → `2(1,-)`
180
+ - Or replace trailing `0` with `1`: `2(1,0)` → `2(1,1)`
181
+
182
+ Don't use `2(1,0{1})` — trailing passthrough with overlay is still invalid.
183
+
184
+ ### 3.3 TOKEN_COUNT mismatch
185
+
186
+ Problem: classifier `N` doesn't match the number of children.
187
+
188
+ Fix: adjust `N` to match children, or add/remove children to match `N`.
189
+
190
+ ### 3.4 SPLIT_EXCEEDS_AXIS
191
+
192
+ Problem: classifier count exceeds available pixels on an axis.
193
+
194
+ Fixes (in order):
195
+ 1. Reduce split count.
196
+ 2. Collapse subtree to `1`.
197
+ 3. Replace with fewer parts.
198
+
199
+ ### 3.5 NO_SOURCES
200
+
201
+ Problem: the layout as a whole contains no leaf `1` (overlay bodies are allowed to be
202
+ sourceless — `ZERO_SOURCE_OVERLAY` is no longer raised).
203
+
204
+ Fix: ensure at least one `1` / `F` exists somewhere (a source inside an overlay counts:
205
+ `2(-,-){1}` is valid). See [`overlay-semantics.md`](overlay-semantics.md).
206
+
207
+ ### 3.6 INVALID_CHAR / TOKEN_RULE
208
+
209
+ Problem: malformed output. **Generator bug** — fix the generation logic, don't band-aid the output.
210
+
211
+ ---
212
+
213
+ ## 4. Overlay body constraints
214
+
215
+ Bodies are parsed as complete m0 sub-expressions: non-empty, not a bare `0`
216
+ (`INVALID_EMPTY`), no trailing passthrough — but they do **NOT** need a leaf `1`
217
+ (sourceless `{-}` / `{2(-,-)}` are legal; relaxed 2026-06-03). Full rules:
218
+ [`overlay-semantics.md`](overlay-semantics.md) and
219
+ [`structural-construction.md`](structural-construction.md) §7.
220
+
221
+ ---
222
+
223
+ ## 5. Output format options
224
+
225
+ Most ops accept output options:
226
+
227
+ ```ts
228
+ { output: "canonical" } // 1, 0
229
+ { output: "pretty" } // F, >
230
+ ```
231
+
232
+ Agents should prefer canonical output for machine use unless pretty is explicitly required.
233
+
234
+ ---
235
+
236
+ ## 6. Precision warnings
237
+
238
+ High classifier counts emit `PRECISION_EXCEEDS_NORM` warnings (informational, never
239
+ blocking; raise the threshold via `parseM0StringComplete(s, w, h, { precisionNorm: 256 })`).
240
+ The two-floor feasibility/precision model:
241
+ [`../handbook/feasibility-precision-quantization.md`](../handbook/feasibility-precision-quantization.md).
242
+
243
+ ---
244
+
245
+ ## See also
246
+
247
+ - [`../handbook/dsl-rules.md`](../handbook/dsl-rules.md) — full grammar.
248
+ - [`../handbook/feasibility-precision-quantization.md`](../handbook/feasibility-precision-quantization.md) — feasibility rules (canonical).
249
+ - [`structural-construction.md`](structural-construction.md) — structural-build patterns.
250
+ - [`overlay-semantics.md`](overlay-semantics.md) — overlay rules.
251
+ - [`parse-apis.md`](parse-apis.md) — parser entry points.