ADR 0041 — CLI: schema dsl, diff, explain
- Status: accepted
- Date: 2026-10-05
- Slice: P41 (builder-roadmap: DSL-first, codegen-first, UI-first)
Context
The tessera model is authored in tessera.yaml, a large typed surface
(128 Yaml* structs). Three everyday tasks had no first-class tooling:
- Authoring aid — editors could not validate or autocomplete
tessera.yaml:mosaic schemaonly covered the three legacy spec formats (tessera/mosaic/deployviaschemars), not the YAML surface that is now the only authoring format (ADR 0008). - Change review — when a model changes (a field added to an
aggregate, a projection renamed, a dashboard dropped), the only way to
see what changed at model level was to
diffthe rendered output — noisy (generated code) and slow. A plan-level, semantic diff was missing. - Error triage —
mosaic checkreportserror[<code>]: …; the code is the stable identity of the diagnostic, but there was no way to look up what a code means and how to fix it without reading the plan-layer source.
Decision
1. mosaic schema dsl
schema gains a fourth what value, dsl: the JSON Schema for the
tessera YAML surface. It is generated with schemars from the same
Yaml* structs the loader deserializes, so the schema cannot drift from
the parser:
- every
Yaml*struct derivesJsonSchema(added alongsideDebug,Deserialize); - fields typed
serde_yaml::Value/serde_yaml::Mapping(free-form escape hatches) are schematized asserde_json::Value; Localizable(an untagged string-or-map that also flattens) gets a hand-written impl:type: ["string", "object"];YamlDeploy.proxy_bufferinguses a small untaggedYamlOnOff(string | boolean) because a strict YAML 1.1 parser reads bareoffasfalse; the plan layer normalizes both forms toon|off(unchanged fail-closed validation).
The entry point is mosaic_core::tessera_yaml::dsl_json_schema().
2. mosaic diff <from> <to>
Two workspace roots (dirs containing tessera.yaml) are loaded and
built to plan level, then compared. The comparison unit is a digest:
Digest maps entity → facet → detail string, where the entity is
<kind>:<name> (e.g. aggregate:Order, projection:Orders,
workflow:CheckOut, dashboard:ops) and the facet is the meaningful
attribute (state.<field>, command.<name>.fields,
projection.<name>.on.<i>, …). tessera_diff::digest(&ws) covers the
whole plan surface (app incl. ui/env/realms, aggregates, workflows,
endpoints, cli, mcp, mcp_servers, dashboards, schedules, triggers,
notifications, flags, rulesets, policies, admins, scenes, vectordbs,
pages/docs, identity, peers, mirrors, aspects, gateway routes, schemas,
tests, model registry, command groups, requirements, reactors,
templates, queries, resources, domains).
diff(&old, &new) renders a deterministic report:
+ workflow:Refund.nodes.refund_call: …
~ aggregate:Order.state.priority: - -> Prim("String")
- schedule:nightly-report: <facets>
+ added entity / facet, - removed, ~ modified. Exit 0 when
identical, 1 when different (like diff); parse or plan errors on
either side abort with the diagnostics.
3. mosaic explain <code>
diag_docs carries a curated catalog — code → (what it means, how to fix it) — for every diagnostic code emitted by the plan layer
(~124 codes: dashboard-unknown-projection,
projection-unknown-event, cli-unknown-command-source, …).
explain prints the entry, or says so when the code is unknown. A test
scans the mosaic-core sources for every emitted code and asserts each is
cataloged, so the catalog cannot silently lag the plan layer.
Consequences
tessera.yamlgets editor validation + autocomplete out of the box (point the YAML language server atmosaic schema dsl); the schema is derived from the deserializer, so it stays true by construction.- Model review is one command: snapshot the old model dir (e.g. from git
or a backup) and
mosaic diff old/ new/shows exactly which entities and facets changed — without rendering. - Diagnostics become self-documenting:
mosaic explain <code>next tomosaic checkcloses the "what does this error mean" loop without source diving. - All three are CLI-only: no render change (conformance stayed green,
no re-pin), no new server surface, no DSL surface beyond the
YamlOnOffnormalization. - The diff digest is lossy by design: it compares declared model meaning, not rendered bytes. A change that only reorders generated code (never a real model change) shows nothing — which is the point.
Verification
schema dslemits a valid draft-07 JSON Schema (~70 KB); all four exampletessera.yamlfiles validate against it (Pythonjsonschema). The validation caught a real drift:proxy_buffering: offwas typedOption<String>but is a YAML 1.1 boolean — now accepted in both forms.diffon an unmodified workspace printsno differences(exit 0); adding one aggregate state field reports~ aggregate:Order.state.priority(exit 1).explainprints the curated entry for a known code and a clear fallback for an unknown one; the catalog-completeness test fails if any emitted code is uncataloged.- Gates: workspace tests (new core tests for diff/explain/YamlOnOff), clippy, fmt, conformance green (no re-pin), full-stack build + e2e.