@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,372 @@
|
|
|
1
|
+
# m0saic CLI usage
|
|
2
|
+
|
|
3
|
+
> Regenerated 2026-07-29 from the CLI source (not published) — re-verify with
|
|
4
|
+
> `m0saic --help` / `grep -c '.command(' the CLI source (not published) (14 as of
|
|
5
|
+
> 2026-07-29, **2 of them dev-gated** — `browse-templates` and `momo`; a 15th
|
|
6
|
+
> command, `telemetry`, registers via a helper — see below). So: 12 public
|
|
7
|
+
> commands + 2 dev-gated + `telemetry`.
|
|
8
|
+
>
|
|
9
|
+
> **2026-09-16:** `doctor` (added after this snapshot, dev-gated at 0.1.0) is now
|
|
10
|
+
> **public** — see its row; `browse-templates` and `momo` remain the only two
|
|
11
|
+
> `if (DEV_MODE)` commands. Public set is locked by `PUBLIC_COMMANDS` in
|
|
12
|
+
> the CLI source (not published) (15 incl. `doctor`, `hello-world`).
|
|
13
|
+
|
|
14
|
+
Source of truth: [the CLI source (not published)](the CLI source (not published))
|
|
15
|
+
(~5,866 lines). Inline-DSL arg parsing: [the CLI source (not published)](the CLI source (not published)).
|
|
16
|
+
Root `COMMANDS.md` (monorepo document, not published) documents the inline-DSL shorthand + test tiers.
|
|
17
|
+
|
|
18
|
+
## Entry routing — inline-DSL mode is first-class
|
|
19
|
+
|
|
20
|
+
Before commander ever runs, `index.ts:5915-5920` inspects `process.argv[2]`:
|
|
21
|
+
`isInlineM0()` (`inlineDsl.ts:36-41`) treats ANY first arg that is not a flag and
|
|
22
|
+
not in `KNOWN_SUBCOMMANDS` as an inline m0 string and dispatches to
|
|
23
|
+
`handleInlineDsl` instead of `program.parseAsync`.
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
m0saic "2(1,1)" # static wireframe → output.png (default mode "wire")
|
|
27
|
+
m0saic "2(1,1)" --anim # animated wireframe → output.mp4
|
|
28
|
+
m0saic "2(1,1)" --wire # explicit static (same as default)
|
|
29
|
+
m0saic layout.m0 # .m0 / .m0c paths also accepted (index.ts:5787-5805)
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
- Args after the string parse via `parseInlineArgs` (`inlineDsl.ts:123`): `-w/-h`
|
|
33
|
+
(default 1920×1080), `-o`, `--fps`, `--durationMs`, `--props`, `--save-mosaic`,
|
|
34
|
+
`--save-m0`, `--prefer-pretty-m0`/`--prefer-canonical-m0`, `--disable-ui`,
|
|
35
|
+
`--format`, `--alpha`, `--validate-only`, `--report`, `--toolchain`, plus the
|
|
36
|
+
dev-gated dump flags (below).
|
|
37
|
+
- The string is validated with `validateM0String` before render (index.ts:5808);
|
|
38
|
+
whitespace is stripped. Always shell-quote the DSL (parens/brackets).
|
|
39
|
+
- Output extension is coerced to match mode (`resolveInlineOutput`,
|
|
40
|
+
`inlineDsl.ts:86` — `--anim -o out.png` → `out.mp4`).
|
|
41
|
+
- Mode maps onto the wireframe handlers: `wire` → `handleWireframe`, `anim` →
|
|
42
|
+
`handleAnimatedWireframe` (index.ts:5846-5850).
|
|
43
|
+
|
|
44
|
+
## Command table
|
|
45
|
+
|
|
46
|
+
14 `.command(` registrations in `index.ts` + `telemetry` registered by
|
|
47
|
+
`registerTelemetryCommand(program)` (index.ts:5784 → `utils/telemetry.ts:372`).
|
|
48
|
+
Two of the 14 sit inside `if (DEV_MODE) { … }` and are absent from the public
|
|
49
|
+
help — see the DEV-GATED rows below.
|
|
50
|
+
|
|
51
|
+
| Command (index.ts line) | What it does |
|
|
52
|
+
|---|---|
|
|
53
|
+
| `make <input>` (4143) | Primary render: template id / `.mosaic` / `.mosaicx` → video or image. Full flag surface below. |
|
|
54
|
+
| `hello-world` | The first render — a thin alias for `make @m0saic/hello-world/v1` (the brand card; output defaults to `hello-world.mp4`). With `--template-repo <path...>` / `--community-repo <path>` it renders the loaded repo's **front door** instead: the first repo (load order) whose `repo.helloWorld` names a template that registered, announced as `Front door: <id> — <repo>'s hello-world`; no repos, or none that name one → the core card with a one-line why. Takes `-o -w -h --fps --durationMs --props --quiet --verbose` (no engine surface). Picker: the CLI source (not published). |
|
|
55
|
+
| `make-wireframe` (4372) | Static wireframe PNG from `--m0 <string>` or `--mfile <path>`. |
|
|
56
|
+
| `make-wireframe-animated` (4448) | Animated wireframe MP4 from `--m0`/`--mfile`; extra `--disable-ui`. |
|
|
57
|
+
| `flatten <input>` (4531) | Inline all `type:mosaic` children of a `.mosaic`/template into one flat JSON doc — no render. |
|
|
58
|
+
| `resolve <input>` (4642) | `.mosaicx` recipe → resolved `.mosaic` provenance doc, no ffmpeg; `--flatten` also inlines children. |
|
|
59
|
+
| `list-templates` (4751) | Print registered template ids (`--json`); annotates primitive/internal/deprecated. |
|
|
60
|
+
| `doctor <repoDir>` | **Public since 0.2.0** (dev-gated at 0.1.0; founder ruling 2026-09-16, BURN-DOWN D5 → publish). Run the template conventions over a template repo folder — the same checks its build gate (`tools/check-registry.mjs`) runs, from outside (agents, reviewers, forks, CI on a fork): definition-time + render-time conventions (the 13 throw-posture ones incl. `latticeSmooth`) and, when the repo commits `layout-fingerprints/` or `.layout.m0` sidecars, the fingerprint diff. `--json` (one object: `ok, repo, mode, loadDiagnostics, rendered, skipped, errors, warnings, notes, fingerprints`), `--sweep` (also render on the standard 1080p canvases → `canvasEnvelope`), `--entry <path>`, `--src <dir>`. Loads through `loadTemplateRepoFromPath` under the external origin scope, so conventions RECORD rather than throw and one bad template never hides the rest. **Runs the repo's template module bodies in-process** — prints a trust warning to stderr (not under `--json`). Exit 1 on any error-severity finding or a fatal load diagnostic. It is the pack-publishing contract for community/starter authors ("the publish requirement for packs"). Impl: the CLI source (not published); locked public by `__tests__/cli.surface.test.js`. |
|
|
61
|
+
| `browse-templates` (4794) | **DEV-GATED** (whole command inside `if (DEV_MODE)`, 4792): interactive TTY picker (template → variant → confirm → delegates to `make`). It drives the E2E variant matrix and writes to `test-output/` — a development harness, not a user surface. |
|
|
62
|
+
| `momo <message…>` (5042) | **DEV-GATED** (whole command inside `if (DEV_MODE)`, 5040): relay English geometry instruction to Momo in a running Mosaic Desktop over the loopback bridge; exit 0 applied / 2 not-applied / 1 couldn't-run. |
|
|
63
|
+
| `open [file]` (5345) | Open a file in Mosaic Desktop, or `--template` / `--make` to open the Make page via the bridge. See below. |
|
|
64
|
+
| `setup` (5496) | Download + install the pinned ffmpeg toolchain (`--gpl` default / `--lgpl` / `--yes` / `--platform-key`). Idempotent: detects installed golden slot or a PATH ffmpeg with libx264. **The only path that downloads** — renders never do. TTY asks `Download the pinned GPL ffmpeg build now?`; non-TTY / `CI` need explicit approval — `--yes` (or the `--gpl` / `--lgpl` repair flags), otherwise `No terminal to ask for approval on.` + exit 1, nothing downloaded. Ends `✓ ffmpeg is ready. Now run your m0saic command again.` |
|
|
65
|
+
| `activate <key>` (5612) | Validate + store a license key at `~/m0saic/license.json` (`M0SAIC_PRODUCT_KEY` env wins over the file). |
|
|
66
|
+
| `license` (5659) | Show tier/holder/expiry; `--remove` returns to free tier. |
|
|
67
|
+
| `update` (5711) | Check npm for a newer release; offers `npm i -g m0saic@latest`. |
|
|
68
|
+
| `versions` (5750) | Print package versions + ffmpeg baseline/runtime + resolved toolchain (`--json`). |
|
|
69
|
+
| `telemetry` (via helper) | Subcommands `status` (default) · `set-mode <standard\|local-only\|ghost>` · `list` · `preview` · `flush` · `clear`. Since 0.2.0 PUBLISHED builds transmit (Standard mode): day summary + today-so-far rollups after renders via a detached worker, to `https://m0saic.io/api/telemetry`. Workspace builds are dormant. Gates for harnesses: `M0SAIC_TELEMETRY=ghost` (nothing recorded), `M0SAIC_TELEMETRY_ENDPOINT=off` (nothing sent), `CI` (no send unless the endpoint is set by env). Full reference: `TELEMETRY.md` at the repo root. |
|
|
70
|
+
|
|
71
|
+
> **`decode-watermark` is GONE** (removed 2026-07-28). Recovering a forensic
|
|
72
|
+
> watermark is now an ordinary template — `@m0saic/forensic/watermark/verify/v1`,
|
|
73
|
+
> run through `make` — because a CLI can't grow a bespoke command per template
|
|
74
|
+
> family. The old `--ffmpeg <path>` override it carried is now the template's
|
|
75
|
+
> `ffmpegPath` prop.
|
|
76
|
+
|
|
77
|
+
## `make` input routing (index.ts:4348-4367)
|
|
78
|
+
|
|
79
|
+
1. Extension `.mosaic` → `handleMosaicFile` — parse JSON renderable, resolve
|
|
80
|
+
relative media against the file's dir, plan + render.
|
|
81
|
+
2. Extension `.mosaicx` → `handleMosaicxFile` (3419) — the **resolve-then-render
|
|
82
|
+
branch**: absolutize asset paths → apply the doc's `runner` block as option
|
|
83
|
+
defaults (explicit flags win) → sibling `.m0v` auto-discovery → route
|
|
84
|
+
`--inputs`/`--input-dir` into the single `template_invocation`'s
|
|
85
|
+
`props.sourceIds` (ambiguous multi-invocation → error; zero inputs on an
|
|
86
|
+
input-requiring template → renders a usage-card and exits non-zero) →
|
|
87
|
+
`resolveMosaicx` → write resolved doc to a tmp `.mosaic` → delegate to
|
|
88
|
+
`handleMosaicFile` (3631) so the post-resolve render path is shared.
|
|
89
|
+
**Two roots accepted** (2026-08-14): `mosaicx_document` and
|
|
90
|
+
`mosaicx_pipeline` (a root-level template chain — see
|
|
91
|
+
`templates/data-pipeline.md`). The header echoes which one it read
|
|
92
|
+
(`kind: mosaicx_pipeline`). A chain root has no top-level `sources`, so the
|
|
93
|
+
`--inputs` routing and the usage-card gap detection above are inert for it;
|
|
94
|
+
`runner` applies to both. Same for `resolve <input>`.
|
|
95
|
+
**Duration is an ASK on this path** (2026-09-05): `--durationMs ??
|
|
96
|
+
wrapper.durationMs` reaches the template as `userIntent.durationMs` via
|
|
97
|
+
`mosaicxUserIntent` (`mosaicxRunner.ts:405`) on both `make` and `resolve`, so a
|
|
98
|
+
self-timing template FITS its walk to the length (as Make does) instead of
|
|
99
|
+
being trimmed to it. Mint ledger wrappers mirroring the template's natural
|
|
100
|
+
length unless the variant is deliberately pinned.
|
|
101
|
+
3. No extension + starts with `@` → `handleTemplateMake` — template id, merge
|
|
102
|
+
`--props` over `defaultProps`, render.
|
|
103
|
+
4. Anything else → error, exit 1.
|
|
104
|
+
|
|
105
|
+
## Exit codes (2026-09-15)
|
|
106
|
+
|
|
107
|
+
| Code | Meaning | File at `-o`? |
|
|
108
|
+
|---|---|---|
|
|
109
|
+
| `0` | Success — the output is the render asked for. | Yes |
|
|
110
|
+
| `1` | Failure — plan build threw, ffmpeg exited non-zero, validation failed, zero commands. | No / unusable |
|
|
111
|
+
| `2` | `momo` ran but did not apply a change. | n/a |
|
|
112
|
+
| `3` | **RENDER DEGRADED** — the renderable carried `engine.renderStatus: "error"`, so the output is an **error mosaic**. | **Yes**, valid media |
|
|
113
|
+
|
|
114
|
+
**Why 3 exists.** A template that hits bad input does not crash: it returns
|
|
115
|
+
`makeErrorMosaic(...)`, a readable card stamped `engine.renderStatus: "error"`.
|
|
116
|
+
That card renders, so the CLI used to write it and exit 0 — a caller whose only
|
|
117
|
+
success test was "did a file appear at the expected path?" reported success on a
|
|
118
|
+
picture of an error. 3 is deliberately not 1: exit 1 means "there is no output,
|
|
119
|
+
retry or clean up", which is false here. The engine still never aborts on the
|
|
120
|
+
marker — it renders, writes the file, and *then* the CLI reports.
|
|
121
|
+
|
|
122
|
+
**Detection** (`src/utils/collectRenderErrors.ts`) walks the WHOLE renderable
|
|
123
|
+
tree for the marker: document level, source level, nested `children`
|
|
124
|
+
(recursively), and pipeline `steps` (dispatching per step, so a nested pipeline
|
|
125
|
+
is walked too). **One error mosaic anywhere in the tree degrades the whole run** —
|
|
126
|
+
a batch where 4 of 5 steps are fine still exits 3, because one deliverable is an
|
|
127
|
+
error card. `errors[].path` in the sidecar (`children.hero.sources[0]`,
|
|
128
|
+
`steps[1].sources[0]`) says which.
|
|
129
|
+
|
|
130
|
+
> ⚠️ Historical: the walker read `doc.config?.sources`, but
|
|
131
|
+
> `MosaicDocument.sources` was hoisted out of `config` long ago — so every
|
|
132
|
+
> source-level marker (exactly where `makeErrorMosaic` puts it) was invisible and
|
|
133
|
+
> every error-mosaic render exited 0. `mosaicxRunner` hand-promoted its error to
|
|
134
|
+
> doc level to work around it; both the bug and the workaround are gone.
|
|
135
|
+
|
|
136
|
+
**Classification** (`src/utils/renderOutcome.ts` — the single decision point; all
|
|
137
|
+
12 report call sites in `index.ts` route through it):
|
|
138
|
+
|
|
139
|
+
- **Hard errors beat engine errors.** A run with both is 1, not 3.
|
|
140
|
+
- A **zero-command plan** counts its engine errors as hard — there is no output
|
|
141
|
+
to call degraded — so that path stays 1.
|
|
142
|
+
- Free vs paid tier is **orthogonal**. The marker is read off the document, not
|
|
143
|
+
the file; the free-tier QR wrap runs before classification, so on free tier the
|
|
144
|
+
error card ships stamped and still exits 3. A stamp failure is swallowed as a
|
|
145
|
+
warning and never changes the exit code.
|
|
146
|
+
|
|
147
|
+
With `--report` the same verdict is machine-readable, so a caller that parses the
|
|
148
|
+
sidecar never needs the exit code and a caller that reads exit codes never needs
|
|
149
|
+
the JSON:
|
|
150
|
+
|
|
151
|
+
```json
|
|
152
|
+
{ "ok": false, "exitCode": 3, "renderStatus": "degraded",
|
|
153
|
+
"renderErrorCodes": ["ENGINE_ERROR"],
|
|
154
|
+
"errors": [{ "code": "ENGINE_ERROR", "message": "Quote Card: quote must not be empty", "path": "sources[0]" }] }
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
## `make` flags
|
|
158
|
+
|
|
159
|
+
**Core** — `-w/--width` and `-h/--height` are `requiredOption`s. Then:
|
|
160
|
+
`-o/--output` (defaults `out.mp4` / `out.png`, index.ts:172-173), `--fps` (1–120),
|
|
161
|
+
`--durationMs` (>0), `--format`/`--output-kind` (`video|image`),
|
|
162
|
+
`--alpha`/`--no-alpha`, `--props <jsonOrPath>` (inline JSON **or** `@path/to/json`
|
|
163
|
+
— `utils/readJsonArg.ts:18` strips the `@` and reads the file),
|
|
164
|
+
`--validate-only` (exit 0/1/3, no render — 3 = the renderable IS an error
|
|
165
|
+
mosaic; prints `Validation DEGRADED`), `--report` (`.output.json` /
|
|
166
|
+
`.validate.json` sidecars, both carrying `renderStatus` + `renderErrorCodes`),
|
|
167
|
+
`--save-mosaic [path]`, `--save-m0 [path]`,
|
|
168
|
+
`--keep-temp`, `--quiet`, `--verbose`, `--prefer-pretty-m0` /
|
|
169
|
+
`--prefer-canonical-m0` (printed-m0 form only; note: NOT `--prefer-*-m0saic`),
|
|
170
|
+
`--background-color <color>`, `--template-repo <path...>` +
|
|
171
|
+
`--template-repo-entry <file>` (external template repos), `--community-repo <path>`
|
|
172
|
+
(ONE checkout of the official community repo — see the community gate below).
|
|
173
|
+
|
|
174
|
+
**Encode surface** (index.ts:4231-4338, mirrors the Make page's Advanced panel;
|
|
175
|
+
precedence: flags > `.m0v` preset > doc-on-disk > engine default):
|
|
176
|
+
`--target <preset>` (`web-mp4|web-webm|alpha-mov|image-png|image-jpeg|animated-gif|audio-mp3|audio-wav`),
|
|
177
|
+
`--container`, `--video-codec`, `--audio-codec`, `--pixel-format`, `--bitrate`
|
|
178
|
+
(CRF wins on conflict), `--crf`, `--encoder-preset`, `--encoder-profile`,
|
|
179
|
+
`--encoder-level`, `--encoder-options <k=v,…>`, `--gop-size`, `--audio-bitrate`,
|
|
180
|
+
`--audio-sample-rate`, `--audio-channel-layout`, `--no-audio` (`-an`),
|
|
181
|
+
`--color-space`/`--color-range`/`--color-primaries`/`--color-transfer`
|
|
182
|
+
(metadata-only), `--metadata-title`/`-description`/`-author`/`-copyright`/`-comment`.
|
|
183
|
+
|
|
184
|
+
**Batching / multi-output**: `--inputs <files...>` (populates `sourceIds`; wins
|
|
185
|
+
over `--input-dir`), `--input-dir <path>` (+ `--recursive` for depth-first walk),
|
|
186
|
+
`--output-pattern <pattern>` (tokens `{{base}} {{ext}} {{stepName}} {{index}}
|
|
187
|
+
{{i}} {{date}} {{batch}} {{label}}`; collisions get `-1,-2,…`), `--batch <name>`.
|
|
188
|
+
|
|
189
|
+
**`.m0v`**: `--m0v <path>` loads a Mosaic Vocabulary file two ways — named
|
|
190
|
+
outputs onto `ctx.userIntent.outputs` (template-consultative) AND a post-render
|
|
191
|
+
merge onto the renderable **by index** when entry counts match
|
|
192
|
+
(index.ts:4218-4221; `BaseRenderOptions` docstring 541-557).
|
|
193
|
+
|
|
194
|
+
**Toolchain**: `--toolchain <name>` is a **global program option**
|
|
195
|
+
(index.ts:526), not make-specific — a named entry from `m0saic.local.json`, with
|
|
196
|
+
implicit `gpl`/`lgpl` golden slots merged in. Resolution precedence: `--toolchain`
|
|
197
|
+
flag > `M0SAIC_TOOLCHAIN` env > config `defaultToolchain` > PATH/golden fallback
|
|
198
|
+
(`utils/toolchainConfig.ts:129`). There is **no** `--ffmpeg` anywhere on the CLI
|
|
199
|
+
any more (it left with `decode-watermark`) — the equivalent is the verify
|
|
200
|
+
template's `ffmpegPath` prop; `--platform-key` only on `setup`.
|
|
201
|
+
|
|
202
|
+
**No-ffmpeg gate** (`utils/ensureFfmpeg.ts` via `ensureRenderToolchain()`, index.ts
|
|
203
|
+
~404, first thing in every render handler): resolved ffmpeg fails to probe → the
|
|
204
|
+
4-line `ffmpeg isn't detected.` message (points at `setup`) and **exit 1** — TTY,
|
|
205
|
+
piped and CI alike; no prompt, no inline download, no auto-install (founder
|
|
206
|
+
ruling 2026-09-16; supersedes cli-v1-publish.md Phase 2). `setup` is the only
|
|
207
|
+
downloader, and it too never fetches without approval (TTY prompt, or `--yes`).
|
|
208
|
+
Spell every `setup` hint with `cliSetupCommand()` (`utils/invocation.ts`):
|
|
209
|
+
`npx m0saic setup` under npx, `m0saic setup` installed.
|
|
210
|
+
|
|
211
|
+
**Perf**: `--perf [path]` writes a `.perf.json` sidecar — per-ffmpeg-command
|
|
212
|
+
timing rolled up by mosaic node (names the bottleneck panel/template).
|
|
213
|
+
|
|
214
|
+
**DEV-GATED** (each wrapped in `.hideHelp(!DEV_MODE)`; `DEV_MODE =
|
|
215
|
+
process.env.M0SAIC_DEV === "1"` at index.ts:58, compile-stripped to `false` in
|
|
216
|
+
the published artifact): `--dev` (commands + timings), `--print-commands`,
|
|
217
|
+
`--save-plan [path]`, `--save-commands [path]`. Same gating inside inline-DSL
|
|
218
|
+
mode (`inlineDsl.ts:148-154`). Also dev-gated: the whole `momo` command, the
|
|
219
|
+
whole `browse-templates` command, and `open`'s
|
|
220
|
+
`--agent-note`/`--agent-question`. `--save-mosaic` and `--save-m0` are NOT gated,
|
|
221
|
+
and neither is `doctor` (public since 0.2.0).
|
|
222
|
+
|
|
223
|
+
> Note: a dev-gated command's NAME still appears in `KNOWN_SUBCOMMANDS`
|
|
224
|
+
> (`inlineDsl.ts:11-29`), outside the gate — the inline-DSL router has to know
|
|
225
|
+
> `momo` is a subcommand and not an m0 string. So grepping a shipped tarball for
|
|
226
|
+
> `momo` / `browse-templates` finds hits; that is the router table, not the
|
|
227
|
+
> gated implementation.
|
|
228
|
+
|
|
229
|
+
## External template repos and the community gate (2026-09-16)
|
|
230
|
+
|
|
231
|
+
**Three reserved identities, exact-match, nothing else is checked:** template
|
|
232
|
+
namespaces `@m0saic/` and `@m0saic-dev/`, repo id `@m0saic-community`
|
|
233
|
+
(`index.ts:907-909`; registry compare is case-insensitive, `templateRegistry.ts:112`).
|
|
234
|
+
Every other namespace / repo id is open. `--template-repo` never grants anything.
|
|
235
|
+
|
|
236
|
+
**The grant.** `--community-repo <path>` says "this IS the official repo"; the
|
|
237
|
+
`@m0saic-dev/` grant comes only from `verifyCommunityRelease()` (`@m0saic/product`
|
|
238
|
+
`communityRelease.ts`) passing on the checkout's `release.json` against
|
|
239
|
+
`LICENSE_PUBLIC_KEYS` (`publicKeys.ts` — the licence k1 family; rotation = append
|
|
240
|
+
`k2`, ship, sign with `M0SAIC_SIGNING_KID=k2`; older CLIs report `unknown-kid`).
|
|
241
|
+
The gate runs BEFORE the entry module is imported. All gate/notice lines go to
|
|
242
|
+
stderr (survive `--quiet`, keep `--json` stdout clean):
|
|
243
|
+
|
|
244
|
+
- ok → `✓ community repo verified: <tag> (signed, <kid>)`
|
|
245
|
+
- else → `⚠ <path> is not a verified release of the official community repo
|
|
246
|
+
(<reason>). Loaded as an ordinary template repo — its @m0saic-dev/ ids are
|
|
247
|
+
refused. Get the official checkout: the link on m0saic.io/community` → the
|
|
248
|
+
reserved ids are refused in ONE collapsed line, `⚠ [template-repo]
|
|
249
|
+
TEMPLATE_ID_RESERVED_NAMESPACE: refused <N> ids in reserved namespace
|
|
250
|
+
(@m0saic-dev/…): <id1>, <id2>, <id3>, …` (other rejection codes stay one line
|
|
251
|
+
each) → the existing `❌ Refusing to use template repo` block → **exit 1**.
|
|
252
|
+
|
|
253
|
+
**`release.json` v1:** `{ schemaVersion: 1, tag, publishedAt, treeSha256,
|
|
254
|
+
signature: { kid, alg: "ed25519", sig: <base64url> } }` (`--unsigned` omits
|
|
255
|
+
`signature`; the legacy `{ tag, publishedAt }` reads as `missing-signature`).
|
|
256
|
+
Tree = every file under `dist/**` + `template-manifest.json` + `package.json`,
|
|
257
|
+
sorted POSIX paths, one `"<path>\0<sha256>\n"` line each; `*.json` hashed on
|
|
258
|
+
`JSON.stringify(JSON.parse(text))` (BOM stripped; CRLF / re-indent survive, a
|
|
259
|
+
key reorder does not), other files raw bytes; no symlink is ever followed.
|
|
260
|
+
Payload `"m0saic-community-release/v1\n<tag>\n<publishedAt>\n<treeSha256>\n"`
|
|
261
|
+
(`publishedAt` must be ISO 8601, else `malformed`). Ladder:
|
|
262
|
+
`missing-release → malformed → missing-signature → unknown-kid → bad-signature
|
|
263
|
+
→ symlink-in-tree / foreign-modules → tree-mismatch → ok` — signature before
|
|
264
|
+
tree, so `tree-mismatch` = authentic release with edited dist/manifests,
|
|
265
|
+
`bad-signature` = forged `release.json`; the two structural refusals
|
|
266
|
+
(`symlink-in-tree`: ANY symlink under `dist/**`, `dist` itself or a signed
|
|
267
|
+
root file; `foreign-modules`: a `node_modules` dir at the root or under
|
|
268
|
+
`dist/`) sit between them and carry the offending path in the notice —
|
|
269
|
+
`(symlink-in-tree: dist/node_modules)`, `(foreign-modules: node_modules)`.
|
|
270
|
+
`publishedAt` is signed but there is still no anti-rollback (an older
|
|
271
|
+
authentic release verifies).
|
|
272
|
+
|
|
273
|
+
**A checkout never supplies `@m0saic/*` to itself.** The loader
|
|
274
|
+
(`@m0saic/platform` `template-repos/hostFirstResolution.ts`, installed by
|
|
275
|
+
`loadTemplateRepoFromPath` before the entry import) resolves every
|
|
276
|
+
`@m0saic/<pkg>[/<subpath>]` require whose parent file lies under a loaded
|
|
277
|
+
repo root from the HOST's own `node_modules` walk — the CLI's vendored copies,
|
|
278
|
+
the desktop's bundle, the workspace under dev — never from the checkout. So a
|
|
279
|
+
clone with nothing installed loads anywhere (0.2.0 is the first CLI that
|
|
280
|
+
can), and a `node_modules/@m0saic/template-utils` planted beside a tree is
|
|
281
|
+
never evaluated, via `--community-repo` or `--template-repo` alike. Every
|
|
282
|
+
other specifier (`sharp`, `@twemoji/svg`, relative paths) resolves as before,
|
|
283
|
+
from the checkout. CommonJS only — an `.mjs` entry's static `import`s use the
|
|
284
|
+
ESM resolver, which has no hook.
|
|
285
|
+
|
|
286
|
+
**Minting:** `scripts/sync-community-templates.mjs --tag <tag> --out <dir>` signs
|
|
287
|
+
with `M0SAIC_SIGNING_KEY_FILE` (REQUIRED — no default path; production laptop
|
|
288
|
+
only) under `M0SAIC_SIGNING_KID` (default `k1`), self-verifies against the
|
|
289
|
+
embedded keys, writes `.gitattributes` (`* -text`). Unset or missing key =
|
|
290
|
+
refused, never a silent unsigned release; `--unsigned` = loud banner, a snapshot
|
|
291
|
+
no shipped CLI trusts.
|
|
292
|
+
|
|
293
|
+
**DEV seam (workspace builds only, inside `if (DEV_MODE)`):** reasons
|
|
294
|
+
`missing-release` / `missing-signature` are still granted — `✓ community repo:
|
|
295
|
+
<path> (dev build: unsigned community checkout trusted)` (the in-repo
|
|
296
|
+
https://github.com/m0saic-project/m0saic-community-templates/blob/main has no `release.json`) — and
|
|
297
|
+
`M0SAIC_COMMUNITY_TRUST_KEY_PEM` (+ `M0SAIC_COMMUNITY_TRUST_KEY_KID`, default `k1`)
|
|
298
|
+
adds a public key for tests. Tampered tree, forged release or unknown kid are
|
|
299
|
+
refused even in dev. The tarball folds `process.env.M0SAIC_DEV` to `"0"` and the
|
|
300
|
+
audit bans the token, so none of this ships.
|
|
301
|
+
|
|
302
|
+
**Lookalikes are a display concern** — one stderr line for UNSIGNED repos, never a
|
|
303
|
+
refusal: `⚠ [template-repo] <subject> is not an official m0saic namespace — only
|
|
304
|
+
@m0saic/ and @m0saic-dev/ are, and they are signed.` Subject: loaded publisher
|
|
305
|
+
namespaces starting with `@m0saic` that are not exactly the two reserved ones →
|
|
306
|
+
else a repoId matching `/m0saic|mosaic/i` → else `"<displayName>" (<repoId>)`.
|
|
307
|
+
Never for repoId exactly `@m0saic-community`: via `--template-repo` that gets
|
|
308
|
+
`⚠ [template-repo] repo id "@m0saic-community" is reserved for the official
|
|
309
|
+
community repo and this checkout is unverified — loading it under ordinary
|
|
310
|
+
rules.` A `--community-repo` checkout that fails the gate gets ONLY the
|
|
311
|
+
not-verified notice.
|
|
312
|
+
|
|
313
|
+
**Desktop:** `installVersion()` runs `validateCommunityTree` → `verifyCommunityTree`
|
|
314
|
+
(inline-requires `@m0saic/product`) → refuses `tag-mismatch`; failure =
|
|
315
|
+
`{ ok:false, error: "downloaded tree rejected: release signature not verified
|
|
316
|
+
(<reason>): <detail>", reason }` on the existing IPC path; `installed.json` gains
|
|
317
|
+
`signedBy` + `treeSha256`; seam `installVersion({ releasePublicKeys })`. The
|
|
318
|
+
seed/installed load path (`loadCommunityRepo`) does not re-verify; consent gate
|
|
319
|
+
unchanged.
|
|
320
|
+
|
|
321
|
+
Help wording for `make` / `hello-world` / `list-templates` is ONE string,
|
|
322
|
+
test-locked (`__tests__/cli.flags.test.js`): "…Grants the @m0saic-dev/ namespace
|
|
323
|
+
when its release signature verifies (unsigned or modified checkouts load as
|
|
324
|
+
ordinary repos); the community checkout loads only this way".
|
|
325
|
+
|
|
326
|
+
### Hardening (rounds 2 + 3, 2026-09-16)
|
|
327
|
+
|
|
328
|
+
**Host-first `@m0saic/*` resolution.** https://github.com/m0saic-project/m0saic-packages/blob/main/packages/platform/src/template-repos/hostFirstResolution.ts (wired in `loadTemplateRepoFromPath.ts` via `registerHostFirstRepoRoot(repoRoot)` before the native `import(file://…)` of the entry): ONE wrapper around Node's real `Module._resolveFilename` (found by walking the prototype chain — under Jest `node:module` exports a subclass copy), installed once per process, persistent so render-time lazy requires are covered. A request matching `/^@m0saic\/[^/\\]+(?:\/.*)?$/` whose requesting module lies under a registered root (realpath'd) is resolved from the HOST's own `node_modules` walk — never the checkout's. Everything else (relative paths, non-`@m0saic` deps, host code) is untouched. CJS only. Consequences: a community clone loads from ANY directory with no `node_modules` (before this, `--community-repo` only worked inside the monorepo); a planted `node_modules/@m0saic/*` beside a verified tree is never evaluated, via either flag; a starter's `file:` link to `@m0saic/template-utils` resolves to the same physical package — no drift.
|
|
329
|
+
|
|
330
|
+
**Fail-closed tree verification** (`inspectReleaseTree` in `communityRelease.ts`; desktop mirror `inspectCommunityTree` in the Mosaic Desktop / Web app source (not published), run by `installVersion`). A release tree is plain files and directories, nothing else. Refused, in this order after the signature check and before the hash compare: `symlink-in-tree` (ANY symlink — file or dir — under `dist/**`, `dist` itself, a signed root file, or ANY entry at the checkout root); `special-file-in-tree` (FIFO / socket / device anywhere in the same places — a FIFO named `dist/publishers.js` shadows the signed `dist/publishers/` dir and blocks or executes whoever opens it); `foreign-modules` (a `node_modules` dir at the root or under `dist/`, matched case-insensitively). `verifyCommunityRelease` lstats `release.json` before opening it (a FIFO planted there used to hang the verifier). The hasher THROWS `ReleaseTreeError` on a special entry rather than skipping it; `signCommunityRelease` refuses to sign such a tree. The signed payload is `"m0saic-community-release/v1\n<tag>\n<publishedAt>\n<treeSha256>\n"`. The CLI notice carries the detail for these reasons — `(foreign-modules: node_modules)`; for `special-file-in-tree` the CLI does NOT load the folder at all (an ordinary load provably hangs): notice + `❌ Refusing to use template repo` + exit 1.
|
|
331
|
+
|
|
332
|
+
**Community deps.** The `@m0saic-dev/community-m` pack (the only `sharp` / `@twemoji/svg` user) was deleted as defunct on 2026-09-16; both left the community `package.json` and BOTH allowlists (`dep-allowlist.json` ↔ `TEMPLATE_REPO_DEP_ALLOWLIST`, lockstep-tested). Community templates may import only what the hosts ship: `@m0saic/{types,template-utils,dsl-stdlib,platform,dsl}` + `node:path`. A native or third-party runtime dep is a founder decision. `sharp` survives only as a root devDependency for core/templates tests.
|
|
333
|
+
|
|
334
|
+
**Accepted residual.** A third-party `--template-repo` checkout with its own `node_modules` runs its own copies of non-`@m0saic` deps — third-party code, with the notice printed; it cannot obtain a reserved identity that way (adversary-verified). DEV builds (`M0SAIC_DEV=1`, compile-stripped from the tarball) still trust an unsigned checkout for `missing-release` / `missing-signature` only; a tampered or forged tree is refused even in dev.
|
|
335
|
+
|
|
336
|
+
## Wireframe commands
|
|
337
|
+
|
|
338
|
+
`make-wireframe` / `make-wireframe-animated` take the layout as `--m0 <string>`
|
|
339
|
+
(inline DSL — the flag is `--m0`, not `--m0saic`) or `--mfile <path>`, plus the
|
|
340
|
+
same core/save/report/`--m0v`/`--output-pattern` surface as `make` (no encode
|
|
341
|
+
surface, no `--inputs`). Animated adds `--disable-ui` (index.ts:4504).
|
|
342
|
+
|
|
343
|
+
## `m0saic open` — pointer only
|
|
344
|
+
|
|
345
|
+
The canonical protocol (candidate file naming, agent block, iteration ritual,
|
|
346
|
+
`--template` hot-reload loop) lives in
|
|
347
|
+
[https://github.com/m0saic-project/m0saic-sandbox/blob/main/packages/sandbox/agent.md](https://github.com/m0saic-project/m0saic-sandbox/blob/main/packages/sandbox/agent.md) — read that,
|
|
348
|
+
not this. Shape only: `open <file>` accepts `.m0 .m0c .m0p .m0v .mosaic
|
|
349
|
+
.mosaicx` and routes to Mosaic Desktop (auto-detects a running dev electron on
|
|
350
|
+
macOS; `--app <path>` override); `open --template <id> [--props/--props-file]`
|
|
351
|
+
opens the Make page via the loopback bridge, hot-reloading fresh
|
|
352
|
+
https://github.com/m0saic-project/m0saic-packages/blob/main/packages/templates/dist first (`--no-reload` to skip); `open --make
|
|
353
|
+
<file.mosaicx>` opens an authored `.mosaicx` in Make and adopts it for the
|
|
354
|
+
agent loop.
|
|
355
|
+
|
|
356
|
+
## Failure modes
|
|
357
|
+
|
|
358
|
+
- `spawnSync m0saic EACCES` — the CLI isn't built/linked on this machine:
|
|
359
|
+
`cd the CLI source (not published) && npm run build && npm link`.
|
|
360
|
+
- No ffmpeg at all → `ffmpeg isn't detected.` (4 lines), exit 1, nothing
|
|
361
|
+
downloaded (`ensureRenderToolchain()`, index.ts ~404); run `m0saic setup`
|
|
362
|
+
(`npx m0saic setup` under npx), then re-run. A broken golden slot prints
|
|
363
|
+
`❌ Toolchain "gpl" (golden) is broken … Re-install it: m0saic setup --gpl`.
|
|
364
|
+
- Free tier re-encodes every deliverable (QR stamp wrap) — codecs/durations
|
|
365
|
+
change, breaking probe-based assertions. Check `npm run tier:status` first;
|
|
366
|
+
full rule in the maintainers' agent contract §7.7 (not published).
|
|
367
|
+
- Non-zero ffmpeg exits print a core-computed failure classification after the
|
|
368
|
+
exit-code line (`printFfmpegFailure`, index.ts:98) — read it before rerunning
|
|
369
|
+
with `--verbose`.
|
|
370
|
+
- Invalid m0 strings fail fast at validation (`validateM0String`), not inside
|
|
371
|
+
ffmpeg; `SPLIT_EXCEEDS_AXIS` means the split count exceeds the pixel axis —
|
|
372
|
+
see `docs/handbook/feasibility-precision-quantization.md`.
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
# FFmpeg Expression Limits — the parse-depth cliff (a phantom "OOM")
|
|
2
|
+
|
|
3
|
+
**Source of truth:** `rebalanceAdditiveChains` in
|
|
4
|
+
[https://github.com/m0saic-project/m0saic-packages/blob/main/packages/platform/src/ffexpr/balance.ts](https://github.com/m0saic-project/m0saic-packages/blob/main/packages/platform/src/ffexpr/balance.ts)
|
|
5
|
+
(`DEFAULT_MAX_FLAT_ADDITIVE_TERMS = 32`); the funnels that apply it —
|
|
6
|
+
`quoteEnableArg` in [https://github.com/m0saic-project/m0saic-packages/blob/main/packages/platform/src/ffexpr/ffmpeg.ts](https://github.com/m0saic-project/m0saic-packages/blob/main/packages/platform/src/ffexpr/ffmpeg.ts),
|
|
7
|
+
`buildOverlayAlphaFilter` + `buildCameraFilters` in `@m0saic/core`.
|
|
8
|
+
Verified against BtbN ffmpeg `N-124278-gcc3ca17127-20260430` (the pinned GPL build).
|
|
9
|
+
|
|
10
|
+
> Companion mental model: [`../skills/more-atoms-not-bigger-atoms.md`](../skills/more-atoms-not-bigger-atoms.md).
|
|
11
|
+
> This doc is the measured case study; that one is the general principle.
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## The symptom (and why it lies)
|
|
16
|
+
|
|
17
|
+
FFmpeg's expression parser (`av_expr_parse`, `libavutil/eval.c`) has a **fixed
|
|
18
|
+
recursion budget (~100)**. When an emitted expression exceeds it, filtergraph
|
|
19
|
+
init fails with:
|
|
20
|
+
|
|
21
|
+
- **`Error initializing filters / Error : Cannot allocate memory` (−12)** — a
|
|
22
|
+
**phantom OOM**: deterministic, ~60 ms, completely independent of free RAM.
|
|
23
|
+
This is the additive-chain shape.
|
|
24
|
+
- or **`Missing ')' or too many args` → `Invalid argument` (−22) /
|
|
25
|
+
`Error reinitializing filters`** — the deeply-nested shape (e.g. a `crop`
|
|
26
|
+
focus expr).
|
|
27
|
+
|
|
28
|
+
The word "memory" is a lie. It is NOT commit pressure, graph size, filter
|
|
29
|
+
count, or a platform difference — all of which this failure repeatedly (and
|
|
30
|
+
expensively) misled debugging toward. It is one expression too deep.
|
|
31
|
+
|
|
32
|
+
## The cliff (measured, term-count-driven — NOT chars)
|
|
33
|
+
|
|
34
|
+
- A **flat `a+b+c+…` chain fails at exactly 99 terms** (98 parses, 99 fails).
|
|
35
|
+
Character length is irrelevant: a 3.8 K-char / 98-term expr parses; a
|
|
36
|
+
2.3 K-char / 99-term expr fails.
|
|
37
|
+
- **Genuine nesting** (nested `if(cond,…,else)` chains) fails at ~100 levels.
|
|
38
|
+
- Because ffmpeg spends one recursion level **per additive term at the same
|
|
39
|
+
nesting level**, both shapes share the same ~100 budget.
|
|
40
|
+
|
|
41
|
+
## The two shapes → the two fixes
|
|
42
|
+
|
|
43
|
+
1. **Flat additive chains** (`enable` window sums, the `overlay.enable`→`geq`
|
|
44
|
+
alpha fold, camera zoom/focus). Fixable by REGROUPING the same terms into a
|
|
45
|
+
**balanced binary tree** — `((a+b)+(c+d))…` — which keeps parse depth
|
|
46
|
+
O(log N) and is safe past 1024 terms. This is `rebalanceAdditiveChains`.
|
|
47
|
+
2. **Deep nesting** (one nested `if()` level per keyframe). Rebalancing CANNOT
|
|
48
|
+
help — the builder must **emit a flat shape instead**. dsl-tutorial's camera
|
|
49
|
+
`focusExpr` was rewritten from nested `if(lt(t,…),…, else)` (depth 109 at
|
|
50
|
+
10×10) to a **flat sum of disjoint window-gated smoothstep segments**
|
|
51
|
+
(`lt` head + `gte·lt` segments + `gte` tail).
|
|
52
|
+
|
|
53
|
+
## Authoring rule
|
|
54
|
+
|
|
55
|
+
**No flat additive chain longer than ~32 terms, and no piecewise-over-time
|
|
56
|
+
expression built as a nested if-else chain, should ever reach a filtergraph.**
|
|
57
|
+
Build piecewise-in-time values as **flat gated sums**; long chains are
|
|
58
|
+
auto-rebalanced at the engine funnels below.
|
|
59
|
+
|
|
60
|
+
## Engine enforcement (shipped 2026-07-02 — route new work through these)
|
|
61
|
+
|
|
62
|
+
`rebalanceAdditiveChains` (`@m0saic/platform` `ffexpr/balance.ts`, threshold
|
|
63
|
+
`DEFAULT_MAX_FLAT_ADDITIVE_TERMS = 32`) is applied automatically at every
|
|
64
|
+
expression funnel:
|
|
65
|
+
|
|
66
|
+
- `quoteEnableArg` — ALL `:enable='…'` sites (overlay, drawtext,
|
|
67
|
+
`compileEnableArg`).
|
|
68
|
+
- `buildOverlayAlphaFilter` — the `overlay.enable` → `geq` alpha fold.
|
|
69
|
+
- `buildCameraFilters` — zoom/focus chains.
|
|
70
|
+
|
|
71
|
+
Chains **≤ 32 terms pass through byte-identical** (goldens unaffected); only
|
|
72
|
+
longer chains are regrouped. Rebalancing fixes PARSE; it does not change eval
|
|
73
|
+
cost (see below).
|
|
74
|
+
|
|
75
|
+
**Known bypasses to watch:** hand-rolled lavfi strings that build `enable=`
|
|
76
|
+
outside the funnels — dsl-string caret-track `drawbox`, dsl-canvas `drawbox`
|
|
77
|
+
(both currently ≤3-term chains, safe) — and any template that still builds a
|
|
78
|
+
nested-`if` expression (e.g. a line-chart animation) which will hit the cliff
|
|
79
|
+
if its dataset grows. Keep per-filter chains small, or call
|
|
80
|
+
`rebalanceAdditiveChains` before joining.
|
|
81
|
+
|
|
82
|
+
## The "graph-SIZE OOM" was this cliff wearing a trench coat (debunked)
|
|
83
|
+
|
|
84
|
+
A separate ≈130 K-chars / 9 000-filters "graph-size OOM" mode was once believed
|
|
85
|
+
to exist, and a `splitByGraphBudget` chunker was calibrated against it. **It does
|
|
86
|
+
not reproduce.** Direct verification:
|
|
87
|
+
|
|
88
|
+
- The exact 142 K canvas part it was calibrated on renders standalone at full
|
|
89
|
+
duration; so do a **12 000-chained-drawbox / 1.19 MB single graph** and a
|
|
90
|
+
9 000-filter graph.
|
|
91
|
+
- Every recorded failure sidecar in `smokebatch/` (smoke9–29) is a command
|
|
92
|
+
whose EXPRESSIONS cross the parse cliff (flat ≥99 or nesting ≥109), failing in
|
|
93
|
+
<80 ms with −12/−22. **No sidecar ever names a size-heavy command.**
|
|
94
|
+
|
|
95
|
+
Graph chars merely *correlated* with expression bulk in that era's graphs. The
|
|
96
|
+
graph-size chunker was reverted. **Do not re-add a filtergraph-size split
|
|
97
|
+
chasing a "size OOM" — it is the parse cliff.** (The input-count split,
|
|
98
|
+
`maxInputsPerCommand`, is a separate and real argv/process-arg constraint and
|
|
99
|
+
stays.)
|
|
100
|
+
|
|
101
|
+
## Note: the macOS "grind" is a different wall
|
|
102
|
+
|
|
103
|
+
The same dense graphs that fail fast on Windows (parse cliff) instead ran for
|
|
104
|
+
20+ min on macOS. That was NOT the parse cliff (Mac's ffmpeg parses flat chains
|
|
105
|
+
fine) — it was **per-pixel `geq` eval cost** (O(terms) per pixel per plane per
|
|
106
|
+
frame). Rebalancing does not reduce it; density caps like `PULSE_TERM_CAP` and
|
|
107
|
+
representational fixes (SVG masks, pre-rendered intermediates) do. That's a
|
|
108
|
+
perf ceiling, not a crash — see the "more atoms" skill, atom 2.
|
|
109
|
+
|
|
110
|
+
There is also a *third*, mechanically distinct macOS wall on
|
|
111
|
+
conversion-/input-dense graphs (many `scale`/`format` nodes + many still
|
|
112
|
+
inputs, e.g. dsl-tutorial grids): **swscale thread-cap exhaustion**, where
|
|
113
|
+
per-conversion thread pools blow past `kern.num_taskthreads` and frames freeze
|
|
114
|
+
at t=0 or grind at ~1 fps. That one is neither parse depth nor `geq` eval cost —
|
|
115
|
+
that wall is engine-internal: `.ai/moat/runtime/macos-swscale-thread-cap.md`
|
|
116
|
+
(absent in the shipped copy; symptom summary: dense still-image canvases freeze
|
|
117
|
+
at t=0 / grind ~1 fps on macOS — the engine's threadGuard mitigates it).
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# "Push complexity down" — reduce to 1, then bake the constant
|
|
2
|
+
|
|
3
|
+
Two moves, applied in order. Move 1 is structural (isolate complexity); move 2
|
|
4
|
+
is temporal (stop recomputing a constant). They compose: nest, prove the knobs,
|
|
5
|
+
then bake. The user's vocabulary for this is literally **"reduce to 1"**, **"push
|
|
6
|
+
complexity down"**, and **"bake that result to a flat video file."**
|
|
7
|
+
|
|
8
|
+
## Move 1 — Reduce to 1 (nested source)
|
|
9
|
+
|
|
10
|
+
When a subtree is too complex or too precise, render it as its own **nested
|
|
11
|
+
source** (child mosaic document). In the parent it collapses to a single `F` —
|
|
12
|
+
one cell, one source ref. High-level layout stays simple; the complexity is
|
|
13
|
+
isolated one level down. It still renders every frame; you've only separated
|
|
14
|
+
concerns by precision tier (e.g. a px-baked, quantization-sensitive scatter vs.
|
|
15
|
+
resolution-independent chrome).
|
|
16
|
+
|
|
17
|
+
Mechanism: wrap the subtree as a child doc (`children[...]` + a
|
|
18
|
+
`{type:"mosaic", ref}` source) so the engine renders it into its own
|
|
19
|
+
framebuffer and the parent sees one `F`. Same primitive as nested-mosaic
|
|
20
|
+
clipping.
|
|
21
|
+
|
|
22
|
+
## Move 2 — Bake the constant (flat asset)
|
|
23
|
+
|
|
24
|
+
When a subtree is **identical on every render** — a fixed background, a
|
|
25
|
+
decorative field, a logo sting with no data dependency — don't render it at
|
|
26
|
+
runtime at all. Pre-render it **once** and reference the result as a flat asset.
|
|
27
|
+
|
|
28
|
+
How:
|
|
29
|
+
1. Author a one-off **`internal: true`** bake template whose only job is to
|
|
30
|
+
render that subtree full-canvas, opaque, at each target aspect (e.g.
|
|
31
|
+
`hero/ffmpeg-pulse/scatter-bake/v1`).
|
|
32
|
+
2. `m0saic make` it once per aspect → small `.mp4` / `.png` into the pack's
|
|
33
|
+
`_shared/assets/` (mirror to `dist` via `build:templates`).
|
|
34
|
+
3. In the real template, replace the nested subtree with one
|
|
35
|
+
`{type:"media", mediaType:"video"|"image", assetId, placement:{fit:"cover"}}`
|
|
36
|
+
source, picking the asset by aspect (`scatter-${variantKey}.mp4`).
|
|
37
|
+
4. Update the unit test: assert the `assets.<name>` ref + the `mediaType`, and
|
|
38
|
+
DROP the now-stale "child carries N tiles / animates" assertions.
|
|
39
|
+
|
|
40
|
+
Determinism still holds: a baked asset is a committed fixture, byte-identical
|
|
41
|
+
every run. Animation is fine to bake — the reveal/motion lives in the video.
|
|
42
|
+
|
|
43
|
+
## When to bake (the trigger)
|
|
44
|
+
|
|
45
|
+
> "When you notice DSL count is high **and** render is long, and the result is
|
|
46
|
+
> intended to be the same every time — bake it to a flat file."
|
|
47
|
+
|
|
48
|
+
All three must hold:
|
|
49
|
+
- **High DSL / long render** — the subtree dominates `dsl-complexity.md` metrics
|
|
50
|
+
or wall-clock.
|
|
51
|
+
- **Constant across renders** — no prop/data dependency; same pixels every time.
|
|
52
|
+
(If it varies with data, you cannot bake it — keep it live or reduce to 1.)
|
|
53
|
+
- **Knobs are locked** — bake LAST, after the look is signed off. Baking freezes
|
|
54
|
+
the subtree; re-tuning means re-baking.
|
|
55
|
+
|
|
56
|
+
## When NOT to bake
|
|
57
|
+
|
|
58
|
+
- The subtree depends on props/data (varies per render) → it's not constant.
|
|
59
|
+
- The look isn't locked yet → premature; you'll re-bake every iteration.
|
|
60
|
+
- The subtree is cheap → baking adds an asset + a build step for no win.
|
|
61
|
+
|
|
62
|
+
## Measured payoff (FFmpeg-pulse title beat, 2026-06-23)
|
|
63
|
+
|
|
64
|
+
The title beat's signature background is a ~225-rect scatter with an L→R reveal —
|
|
65
|
+
the same on every render. Baking it to a flat per-aspect `.mp4`:
|
|
66
|
+
|
|
67
|
+
| Metric | Live scatter (inlined) | Baked scatter (1 video src) | Change |
|
|
68
|
+
|---|---|---|---|
|
|
69
|
+
| Wall-clock render (1920×1080, 9 s) | ~5 min | 66 s | ~4.5× faster, ~78% less |
|
|
70
|
+
| Flattened `.mosaic` size | ~163 K chars | 3,838 chars | ~98% smaller |
|
|
71
|
+
| Flattened source count | 238 | 14 | ~94% fewer |
|
|
72
|
+
| Baked asset size | — | ~98–155 KB / aspect | one-time, reused |
|
|
73
|
+
|
|
74
|
+
One-time bake cost ~3 m 21 s/aspect, amortized over every future render (it pays
|
|
75
|
+
for itself after ~1 render). The residual 66 s is the **chrome-overlay floor** —
|
|
76
|
+
the variable, prop-driven work that legitimately stays live. The bake removed the
|
|
77
|
+
*constant* cost and left only the *variable* cost; that's the whole point.
|
|
78
|
+
|
|
79
|
+
## Trade-offs
|
|
80
|
+
|
|
81
|
+
- **Inline:** one flat doc, simplest pipeline; fat parent m0, mixed precision
|
|
82
|
+
tiers, depth accrues toward the ~25-layer overlay mask ceiling (see
|
|
83
|
+
`feasibility-precision-quantization.md` and the overlay-depth limit), full cost
|
|
84
|
+
every render.
|
|
85
|
+
- **Reduced to 1 (nested):** clean parent, isolated complexity, dodges the mask
|
|
86
|
+
ceiling; still full render cost (the subtree still renders).
|
|
87
|
+
- **Baked (flat asset):** near-zero per-render cost for that subtree, tiny
|
|
88
|
+
parent; but adds a committed asset + a one-off bake template + a build step, and
|
|
89
|
+
the subtree is frozen (re-tune ⇒ re-bake).
|
|
90
|
+
|
|
91
|
+
## Adjacent
|
|
92
|
+
|
|
93
|
+
`dsl-complexity.md` (metrics → *when*), `feasibility-precision-quantization.md`
|
|
94
|
+
(precision tiers + the mask ceiling), nested-mosaic clipping (the move-1
|
|
95
|
+
mechanism). The shipped reference for a constant-subtree bake is
|
|
96
|
+
https://github.com/m0saic-project/m0saic-packages/blob/main/packages/templates/src/m0saic/hero/ffmpeg-pulse/scatter-bake/v1.
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# skills/ — DSL & engine mental models
|
|
2
|
+
|
|
3
|
+
Operational skills layered on the handbook. **The handbook is canonical** — a doc
|
|
4
|
+
here that restates grammar is a checklist form of the same rules, and on any
|
|
5
|
+
conflict [`../handbook/`](../handbook/README.md) wins.
|
|
6
|
+
|
|
7
|
+
## Grammar & construction (emitting valid m0)
|
|
8
|
+
|
|
9
|
+
- [`structural-construction.md`](structural-construction.md) — the build-time
|
|
10
|
+
rule checklist (count-exactness, overlay discipline, trailing-passthrough).
|
|
11
|
+
- [`m0saic-string-generation.md`](m0saic-string-generation.md) — programmatic
|
|
12
|
+
generation: builders first, unified transforms, validate, repair loop.
|
|
13
|
+
- [`dsl-stdlib-method-catalog.md`](dsl-stdlib-method-catalog.md) — pointer to
|
|
14
|
+
the full stdlib catalog (colocated with the package).
|
|
15
|
+
|
|
16
|
+
## Geometry & semantics (what the parser does)
|
|
17
|
+
|
|
18
|
+
- [`axis-and-geometry.md`](axis-and-geometry.md) — split axes, `splitEven`
|
|
19
|
+
remainders, rect propagation.
|
|
20
|
+
- [`passthrough-semantics.md`](passthrough-semantics.md) — `0` donation runs,
|
|
21
|
+
claimants, carry scoping.
|
|
22
|
+
- [`overlay-semantics.md`](overlay-semantics.md) — **the overlay anchor**:
|
|
23
|
+
attachment, bodies, deferred paint order.
|
|
24
|
+
- [`zero-overlay-analysis.md`](zero-overlay-analysis.md) — the `0{}` phantom
|
|
25
|
+
operator (rarely needed; read overlay-semantics first).
|
|
26
|
+
|
|
27
|
+
## Identity & parsing
|
|
28
|
+
|
|
29
|
+
- [`identity.md`](identity.md) — StableKey, the five axes, selection policies.
|
|
30
|
+
- [`parse-apis.md`](parse-apis.md) — which parse API to use + the
|
|
31
|
+
frames→sources bridge.
|
|
32
|
+
|
|
33
|
+
## Authoring surfaces
|
|
34
|
+
|
|
35
|
+
- [`labels-and-masks.md`](labels-and-masks.md) — labels/masks across
|
|
36
|
+
dictionary → generator → editor → disk.
|
|
37
|
+
- [`text-in-templates.md`](text-in-templates.md) — text sources, rasterizer
|
|
38
|
+
choice, sizing traps.
|
|
39
|
+
- [`more-atoms-not-bigger-atoms.md`](more-atoms-not-bigger-atoms.md) — the
|
|
40
|
+
ffmpeg scaling contract (capacity grows with atom COUNT).
|