# Change the model: swap the canon without rewriting the app ## At a glance - **Goal:** move the application to a new canon version — or a new canon — by changing only the files that name the model, and approve the move with checks that fail loudly on incompatibility. - **You need:** the full application from chapters 1 through 7. This chapter changes it; the suite from [Start](/build/start/) guards every step. - **Run:** ```bash node --import tsx src/cli.ts check-contract ``` The contract check opens the pinned spec offline, verifies every predicate the app sends or asks, and recomputes the hand-checked example case. Exit code 0 approves the spec; 1 names the failure. - **Files:** a compatible update touches one line; a same-app model swap touches the model-naming layer: ```text src/to-case.ts MODEL_SPEC string (every move), predicate names, units, policy, ADAPTER_VERSION bump src/query.ts question names and argument shapes src/missing.ts ASKABLE allowlist for the new predicates form hints/types labels, widgets, generated declarations ``` ## Three kinds of model change Three different moves hide under "change the model", and only the first two keep the rest of the app: 1. A compatible update (new canon build, same questions and facts). Only the spec string and the installed package change. 2. A new model for the same application (a different periods canon with renamed predicates). The adapter, queries, and askable list change; the schema, reader, capture format, and server stay. 3. A new domain (from periods to duties, positions, anything else). New input fields and new result rendering are legitimately needed — that is a different application, not a design failure. No second published canon exists for this app today, so this chapter demonstrates the move as the check sequence, not as a live migration: the contract check that approves a spec, and the controlled refusal of one that cannot serve the app. ## What moves and what stays The files listed under **Files** above name the model. Everything else stays untouched by moves 1 and 2: ```text src/input-schema.ts the form shape does not follow the canon src/read-result.ts the four AppResult kinds are engine-wide src/capture.ts CAPTURE_FORMAT deadline-app.capture/1 is the app's own src/server.ts routes and the example API do not move ``` That split is deliberate: the adapter, the queries, and the askable list name the model; the schema, the reader, the capture format, and the server do not. Move 3 — a new domain — justifiably changes the second group too. ## Follow the migration checklist The move follows a checklist, in order: 1. Pin the new spec and install the new canon package. Nothing else may change in this step. 2. Ask the manifest what the new model offers: `questions('en')` returns its inventory (`package`, `questions`, `facts`, `symbols`, `rules`, and more) — read what the new build declares. 3. Check each predicate the application sends or asks with `passport().inModel(name)`. A question the new model does not carry is an integration error — the call raises `FactError` (`unknown relation`), it does not quietly answer `NEITHER`. Find that out here, not from a confused user: an undeclared predicate, an underived claim, and a missing user fact are three different states with three different handlers. 4. Update `to-case.ts`: predicate names, quantity units, the deadline policy URN, and bump `ADAPTER_VERSION` (currently `1.0.0`) — the capture records which adapter wrote it, and a silent adapter change would corrupt that. 5. Update `query.ts` for renamed questions and the `ASKABLE` list in `missing.ts` for the new suppliable names. 6. Refresh form hints and generated types from the new manifest, so the editor completes the new names and rejects the old ones. ## Run the contract check The contract check automates steps 2 and 3 plus the hand-counted example expectation: ```ts // src/check-contract.ts // Every predicate name this app depends on: facts it sends plus the // question it asks. export const APP_PREDICATES = ['frist_ereignis', 'frist_dauer_tage', 'frist_ende']; ``` ```bash node --import tsx src/cli.ts check-contract ``` ```text spec: de.bgb.fristen@0.1.0 (opened de.bgb.fristen@0.1.0) pass open — opened de.bgb.fristen@0.1.0 offline pass inModel(frist_ereignis) — frist_ereignis is executable in de.bgb.fristen@0.1.0 pass inModel(frist_dauer_tage) — frist_dauer_tage is executable in de.bgb.fristen@0.1.0 pass inModel(frist_ende) — frist_ende is executable in de.bgb.fristen@0.1.0 pass example-case — example case computes 2026-03-20 ``` An unknown spec fails closed with a named reason and exit code 1 — it never throws an unhandled error: ```bash node --import tsx src/cli.ts check-contract --spec de.bgb.fristen@9.9.9 ``` ```text spec: de.bgb.fristen@9.9.9 (not opened) FAIL open — cannot open de.bgb.fristen@9.9.9 offline: package de.bgb.fristen@9.9.9 is not in a canon package or cache (offline) ``` ## Approve the move with three checks The three checks that approve the move, in order: ```bash # 1. the spec serves this app (exit 1 on any failure) node --import tsx src/cli.ts check-contract --spec # 2. the compiler accepts the new names npm run typecheck # 3. the suite passes with the new expectations npm test ``` `inModel` failing means the predicate is not executable — stop and pick names the model carries. The type check failing means a renamed predicate or a mistyped application object slipped through — the generated declarations caught the name before any run. But the compiler does not check arity or per-predicate argument types: the published signature takes `args: FactArg[]`, a plain array, so a missing, extra, or mistyped argument still compiles. That conformance is the SDK's runtime job (`FactError` on unknown relations and wrong arity), and the suite's job is the rest — if it fails on values, the new model computes differently, so read the diff as a change in the law's formalization, update the expectation deliberately, and record why. When all three pass, the move is approved: ```text check-contract .................. all pass, exit 0 npm run typecheck ............... clean npm test ........................ all passing ADAPTER_VERSION ................. bumped when the mapping changes, recorded in captures ``` Three levels, three owners: the compiler checks names and shapes the declarations express; the SDK at runtime checks the request against the relation's declaration; the scenario tests check the computed answers. Predicate-indexed tuple types would move the arity check left, but the SDK does not generate them — do not read that guarantee into `FactArg[]`. ## Limits and errors - `FactError` with a `nearest` list after the swap means a predicate was renamed in the new model. The fix lands in `to-case.ts` (facts) or `query.ts` (questions) — never in the reader or the view. - `NEITHER` everywhere after the swap usually means the questions are asked of a model that declares but no longer derives them. `inModel` confirms or excludes that in one call — and if a predicate is not declared at all, expect `FactError`, not `NEITHER`. - Captures keep their `model` spec; old captures replay against the old canon, not the new one. Replaying an old capture against a different open model is refused with the spec mismatch — versions are never mixed silently. ## Check your understanding Run the contract check against the pinned spec and an unknown one: ```bash node --import tsx src/cli.ts check-contract node --import tsx src/cli.ts check-contract --spec de.bgb.fristen@9.9.9; echo "exit: $?" ``` ```text pass open — opened de.bgb.fristen@0.1.0 offline pass example-case — example case computes 2026-03-20 FAIL open — cannot open de.bgb.fristen@9.9.9 offline: package de.bgb.fristen@9.9.9 is not in a canon package or cache (offline) exit: 1 ``` Predict before you open each answer.
The new canon renames `frist_dauer_tage`, and you forget to update `to-case.ts`. What does the app see? `check-contract` fails on `inModel(frist_dauer_tage)`, and a call that sends the old name raises `FactError` with a `nearest` list. It does not quietly answer `NEITHER`: an undeclared predicate is an integration error. The fix lands in `to-case.ts`.
A fact is sent with one argument too many. Does `npm run typecheck` catch it? No. The published signature takes `args: FactArg[]`, a plain array, so a wrong arity still compiles. The SDK catches it at runtime with `FactError`, and the scenario tests check the computed answers.
When do you bump `ADAPTER_VERSION`? When the form-to-facts mapping changes — predicate names, units, or the policy in `to-case.ts`. Every capture records the adapter that wrote it, and a silent mapping change would corrupt that record.
## Next - [Troubleshooting](/build/troubleshooting/) - [Connect an AI assistant](/guide/mcp/) to explore the new model first - [Loading and pinning](/build/sdk/loading-and-pinning/) and [compatibility](/build/sdk/compatibility/) before swapping the spec