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-levelparametersmerged with operation-level, operation level winning onname: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
/dashboardKPI page is always emitted and renders its recent-events row viacell_str, but thecell_str/fmt_cellhelper was only emitted for apps with aggregates/formats/charts — any such app's wasm build failed withcannot 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 withcannot 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
reqafterreq.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 /petstake?") and get spec-grounded answers from the docs it ships — the same surfaces (aide page,/api/aide/chat,aide_chatMCP,/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
sourcesdeclaration already existed — K1), no new runtime mechanism (build-time knowledge copy, source resolution, and the vendored engine are reused), no new CLI flag (--knowledgesemantics 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),
--knowledgegating, and the plan round-trip (load + build, no errors) — 9/9 green. - Smoke: import a fixture spec,
mosaic check+mosaic build --checkpass 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.