# Carrying projects to the new syntax with `wrnexus update` — Design **Date:** 2026-08-19 **Status:** Approved for implementation **Scope:** Migrations that take an existing project from today's syntax to the syntax left by the cleanup and `apis { }` specs. **Depends on:** `2026-08-19-legacy-and-config-cleanup-design.md` and `2026-08-19-apis-block-design.md`. Both define the target this migrates to, so both must land first. ## Goal After the two preceding specs, every existing project is written in a syntax the framework no longer accepts. One `wrnexus update` should carry a project across — config keys removed, `api` blocks moved into `apis { }`, mode-scoped helpers relocated — and, where it cannot do that safely, say so precisely instead of guessing. ## What already exists This is an extension of working machinery, not a new subsystem: - `Migration { version, id, description, apply(ctx) }`, run when `from < version <= to`. - `MigrationCtx` carries `appRoot`, `from`, `to`, **`dryRun`**, a report, and a logger. - `MigrationReport` already separates `changedAutomatically`, `needsReview`, `parseFailures`, `ambiguousFunctions`, and `unresolvedImports`. - `update.ts` already imports `parse` and `formatWrn` from `@wrnexus/syntax`, so parsing a `.wrn` file, transforming it, and re-emitting formatted source is an established pattern here. The report's shape matters: it was built around the idea that some changes are safe to make and others must be handed back to a human. That distinction is the backbone of this spec. ### Non-goals - Migrating projects below `0.8.0`. The cleanup spec removes those migrations; no such project exists. - Rewriting application logic. Only the constructs these specs changed. ## The safety contract **A file is transformed correctly, or it is left untouched and reported.** There is no third outcome. Concretely: 1. Parse the file. A parse failure records the path in `parseFailures` and moves on — the file is never partially rewritten. 2. Transform, then re-emit through `formatWrn`. 3. If any part of a file's transform cannot be completed, **the whole file is skipped** and recorded in `needsReview` with the reason and the construct involved. Running the migration twice must be a no-op: every transform detects already-migrated input and does nothing. Dry-run must report exactly what a real run would change. ## The migrations ### 1. Remove the dead config keys Delete `compatibility`, `functions`, `compatibilityDate`, and `frameworkBehaviour` from `wrnexus.config.ts`. Mechanical, fully automatic, and safe because none of them was ever read. ### 2. `ssr { api … }` / `client { api … }` → `apis { … }` Move each `api` entry into a page-level `apis { }` block, dropping the mode. Sectioned bodies — those already using `request` / `response` / `error` — carry across unchanged, because the payload is already bound to `data`. Fully automatic. If a page has entries in both an `ssr` and a `client` block sharing a name, that is a duplicate under the new rules and the **file is skipped and reported**, since choosing which one survives is a decision about intent. ### 3. `ssr { functions { … } }` → `functions { shared function … }` Relocate mode-scoped helpers to the page-level `functions { }` block with the `shared` modifier. Automatic. If the page already has a function of the same name, the file is skipped and reported. ### 4. Legacy bare-body `api` blocks — **needs review, not automatic** This is the one transform that cannot be done safely, and the spec is explicit about it rather than attempting a best effort. A legacy bare body is evaluated inside `with ($data ?? {})`, so it references payload fields as bare identifiers: ```wrn api ssrUsers GET /api/users/ssr { return userNames(users) } ``` The sectioned form binds the payload to `data`, so this must become `data.users`. But **which free identifiers are payload fields is not knowable from the source.** In the example, `users` comes from the response and `userNames` is a page helper — and nothing in the file distinguishes them. The response shape belongs to the route, and the route may not even be typed. A migration that guessed would produce code that compiles and is wrong: `data.userNames(...)` or an untouched `users` that silently resolves to `undefined`. That is precisely the silent-wrong-answer failure this project keeps paying for. So: legacy bare-body blocks are **detected, reported in `needsReview` with the file, the block name, and the free identifiers found**, and left untouched. The report tells the author exactly what to decide. `wrnexus update` prints a short explanation of why this one is manual. ### 5. Deprecated `@wrnexus/auth` options Only if the cleanup spec's optional auth section is included. Rename call sites of the superseded options. Automatic where the rename is unambiguous; reported otherwise. ## Version All of these attach to the release that ships the breaking change. After the cleanup spec the migration floor is `0.8.0`, so the list is short and every entry is reachable. ## Output At the end of a run the command prints, in this order: what it changed, what needs review and why, and what failed to parse. A run with anything in `needsReview` or `parseFailures` exits non-zero, so a scripted upgrade cannot appear to succeed while leaving a project half-migrated. ## Testing Each migration gets a fixture project and three assertions: the transform produces the expected source, running it a second time changes nothing, and a dry run reports the same set without writing. - **Config removal** — keys gone, rest of the config untouched. - **`api` relocation** — a page with both `ssr` and `client` api blocks lands in one `apis { }`; entries keep their names, methods, paths, and sections. - **Name collision across modes** — the file is skipped and reported, not silently merged. - **Mode functions** — relocated with the `shared` modifier; a name collision skips and reports. - **Legacy bare body** — reported in `needsReview` with the block name and free identifiers, and the file is byte-identical afterwards. This is the most important test in the spec: it pins that the migration does _not_ attempt the rewrite. - **Parse failure** — a malformed `.wrn` is recorded in `parseFailures` and left untouched. - **Exit code** — non-zero when anything needs review. - **End to end** — `examples/basic-app` migrated by the command alone, then built and tested. If the framework's own example cannot be migrated by the tool, the tool is not finished. ## What this does not promise Automated source rewriting cannot be promised as "perfect". What is promised is bounded: every file is either correctly transformed or untouched and named in the report, with the reason. Nothing is half-rewritten, and nothing is guessed. The legacy bare-body case is deliberately manual because a correct automatic answer does not exist.