Mosaic one model, many lenses

ADR 0042 — import openapi: spec docs as knowledge sources

  • Status: accepted
  • Date: 2026-10-05
  • Slice: P42 (builder-roadmap: DSL-first, codegen-first, UI-first)

Context

mosaic import openapi <spec> <id> (P29, ADR 0029) turns an OpenAPI document into a proxy tessera: one rest.endpoints item and one mcp.tools tool per operation, all delegating to the spec's servers[0].url. The generated app declares an upstream-kb vector db that — with --knowledge — captures successful upstream responses at runtime. But the KB never contained the spec itself: the docs describing what each operation does (parameters, request bodies, responses) lived only in the source document, so the app's existing ask surfaces (aide at /aide + /api/aide/chat, aide_chat MCP tool, /search, the knowledge REST surface) could not answer questions about the imported API. And the generated tessera.yaml was only plan-checked in users' heads: nothing in the repo proved the emitted output parses and plans cleanly.

Decision

1. Spec docs → knowledge/docs/api/

The importer now emits, alongside tessera.yaml, one markdown doc per operation plus an index, under knowledge/docs/api/:

  • index.md — the API title, version, description, server, and a table of every operation (method, path, summary) linking to its doc;
  • <endpoint-id>.md — the operation: method + path, description, tags, a parameters table (name, in, required, type, description — path-level parameters merged with operation-level, operation level winning on name:in), the request body per content type, and a responses table (status, description, body schema).

Docs are deterministic (BTree-ordered iteration of the parsed spec, no timestamps) and derived purely from the document — $ref schemas render as their component name, inline objects as object (a: string, b: …), arrays as array<T>. The generated upstream-kb then declares:

vector_dbs:
  - name: upstream-kb
    sources:
      - path: knowledge/docs/api
        kind: docs
    docs: []

The build already copies <tessera>/knowledge/ into the output (copy_knowledge_dir), the runtime resolves relative source paths against the output root (MOAIC_KNOWLEDGE_BASE → CWD → exe root), and the knowledge engine is vendored for every app with a non-empty vector_dbs — so the spec docs are ingested at boot and retrievable with no new mechanism. --knowledge keeps its P29 meaning (runtime response capture) and is orthogonal: the spec docs are emitted either way.

2. Testable core + round-trip guarantee

cmd_import_openapi was split into a pure openapi_import(doc, tessera, cache, cache_secs, knowledge) -> OpenapiImport { tessera_yaml, docs, endpoints } (the CLI does only fetch/read/parse/write) plus deterministic helpers (op_docs, op_schema_type, op_schema_body, md_cell). Unit tests cover the emitted tessera (source declaration, endpoint ids), the per-operation docs (parameter merging, request body, response table, markdown escaping), the --knowledge gate, and — the key guarantee — a round-trip test: the generated tessera.yaml loads through load_tessera_yaml_str and builds through the plan layer with zero errors, so the import output is a valid tessera by test.

3. Example + codegen fixes it exposed

A committed example (examples/openapi-proxy: the pets.json fixture + its import output + conformance golden, wired into the CI web-builds list) pins the import shape as test-of-record. Building that first zero-aggregate proxy app compiled code paths that had never been compiled, exposing three latent codegen bugs, all fixed:

  • web lens: the /dashboard KPI page is always emitted and renders its recent-events row via cell_str, but the cell_str/fmt_cell helper was only emitted for apps with aggregates/formats/charts — any such app's wasm build failed with cannot find function cell_str. The helper is now always emitted.
  • app lens: the proxy knowledge-capture stamps doc ids with now_ms(), which is defined only in the workflow run-history block — a proxy app without workflows failed with cannot find function now_ms. The helper is now also emitted when no workflow exists but a proxy endpoint declares knowledge capture.
  • app lens: the proxy forwarder read headers from req after req.into_body() moved it (E0382) — the header set is now snapshotted before the body is consumed.

Consequences

  • The imported proxy app can answer about the imported API: ask the aide ("what parameters does GET /pets take?") and get spec-grounded answers from the docs it ships — the same surfaces (aide page, /api/aide/chat, aide_chat MCP, /search, knowledge REST) every vector-db app already has.
  • The import is now self-verifying: the round-trip test fails if the emitter and the plan layer ever disagree, and the example golden pins the rendered proxy app (app + knowledge engine + web lens) in CI.
  • No new DSL surface (the sources declaration already existed — K1), no new runtime mechanism (build-time knowledge copy, source resolution, and the vendored engine are reused), no new CLI flag (--knowledge semantics unchanged).
  • The three codegen fixes are behavior-preserving for every existing app: the only rendered-output change is in apps that had zero aggregates (none before this example), so no existing golden moved.

Verification

  • New unit tests (mosaic-cli): source declaration in the emitted tessera, per-operation doc contents (incl. path-level parameter override + pipe escaping), --knowledge gating, and the plan round-trip (load + build, no errors) — 9/9 green.
  • Smoke: import a fixture spec, mosaic check + mosaic build --check pass on the output (125 files render); the rendered proxy app builds both lenses (native app + wasm web, hydration: csr).
  • Gates: workspace tests (18 suites), clippy -D warnings, fmt, conformance re-pinned (only the new example), full-stack build + e2e.