docs: four implementation plans for the cleanup, apis block, migration, and editor work

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-19 23:05:31 +05:30
co-authored by Claude Opus 5
parent 68cc75d0b0
commit 224af8fd96
4 changed files with 2602 additions and 0 deletions
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,400 @@
# Editor Tooling Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** The language server and VS Code extension understand `apis { }`, complete `api.<name>()`, flag the removed constructs, and stop offering syntax the compiler rejects.
**Architecture:** Four surfaces change independently — the TextMate grammar, the keyword and snippet completions, the server's call completion and hover, and diagnostics for removed constructs. A final task answers by observation whether generated type errors surface inside `.wrn`, which has never been verified.
**Tech Stack:** Bun, TypeScript, `bun:test`, `node --test`, LSP, TextMate grammars.
**Spec:** `docs/superpowers/specs/2026-08-19-editor-tooling-design.md`
## Global Constraints
- `client` is one word with several jobs: `client state { }`, `runtime = "client"`, and the `client function` modifier all survive. **Only the `client { }` / `ssr { }` data-block patterns are removed.** Blanket removal would un-highlight constructs that still exist.
- Offering a construct the compiler rejects is worse than offering nothing.
- **Nothing may claim inline diagnostics work until someone has seen them work.**
- The editor bundles embed the compiler and language server — rebuild with `bun run --cwd editors/vscode build`, or the `check:editor-*` gates fail on a stale bundle.
- `bun run format` before every commit; the gate is `bun run check:production`.
- Do NOT use `node -e`, shell heredocs, or `sed` to write code into files.
---
### Task 1: Grammar
**Files:**
- Modify: `editors/vscode/syntaxes/wrn.tmLanguage.json`
- Test: `editors/vscode/test/grammar-apis.test.js`
**Interfaces:**
- Produces: `apis` highlights as a block keyword; entries highlight as declarations.
- [ ] **Step 1: Write the failing test**
Create `editors/vscode/test/grammar-apis.test.js`:
```js
"use strict";
const assert = require("node:assert");
const { test } = require("node:test");
const { readFileSync } = require("node:fs");
const { join } = require("node:path");
const grammar = readFileSync(join(__dirname, "../syntaxes/wrn.tmLanguage.json"), "utf8");
test("the grammar knows the apis block", () => {
assert.ok(grammar.includes("apis"), "apis should appear as a block keyword");
});
test("client keeps its highlighting where it is still valid", () => {
// client state {}, runtime = "client", and client function all survive.
// Only the client {} data block was removed.
assert.ok(grammar.includes("client"), "client must still be matched");
assert.ok(grammar.includes("shared"), "the shared function modifier must still be matched");
});
```
- [ ] **Step 2: Run the test to verify it fails**
Run: `node --test editors/vscode/test/grammar-apis.test.js`
Expected: FAIL on the first assertion — `apis` is absent.
- [ ] **Step 3: Add `apis`, remove only the data-block patterns**
Add `apis` to the block-keyword pattern alongside `functions`. Then find the patterns matching `ssr`/`client` as **data blocks** and remove only those. Leave every rule that matches `client` in `client state`, in `runtime` values, and as a function modifier.
Add a pattern for an entry — `<name> <METHOD> <path>` — so a declaration reads as a declaration.
- [ ] **Step 4: Run the test and check by eye**
Run: `node --test editors/vscode/test/grammar-apis.test.js`
Expected: PASS. Then open a `.wrn` file using `apis { }`, `client state { }`, `functions { shared function }`, and `runtime = "client"` in VS Code and confirm each still colours correctly. Record what you saw.
- [ ] **Step 5: Commit**
```bash
bun run format
git add editors/vscode
git commit -m "feat(editor): highlight the apis block"
```
---
### Task 2: Keyword and snippet completion
**Files:**
- Modify: `packages/language-server/src/index.ts` (`WRN_KEYWORDS`, ~line 31)
- Modify: `editors/vscode/src/completion.js` (block snippets)
- Test: `packages/language-server/test/apis-completion.test.ts`
**Interfaces:**
- Consumes: nothing.
- Produces: `apis` is a known keyword; `ssr` / `client` data-block snippets are gone.
- [ ] **Step 1: Write the failing test**
Create `packages/language-server/test/apis-completion.test.ts`:
```ts
import { expect, test } from "bun:test";
import { WRN_KEYWORDS } from "../src/index.ts";
test("apis is a known page-level keyword", () => {
expect(WRN_KEYWORDS).toContain("apis");
});
```
- [ ] **Step 2: Run the test to verify it fails**
Run: `bun test packages/language-server/test/apis-completion.test.ts`
Expected: FAIL — `apis` is missing.
- [ ] **Step 3: Add the keyword and the snippets**
Add `"apis"` to `WRN_KEYWORDS`. In `editors/vscode/src/completion.js`, add a container snippet and an entry snippet including the `request` / `response` / `error` sections, and remove any `ssr {` / `client {` data-block snippet.
- [ ] **Step 4: Run the tests**
Run: `bun test packages/language-server && bun run --cwd editors/vscode test`
Expected: PASS.
- [ ] **Step 5: Commit**
```bash
bun run format
git add packages/language-server editors/vscode
git commit -m "feat(editor): complete the apis block and drop the removed snippets"
```
---
### Task 3: `api.` call completion and hover
**Files:**
- Modify: `packages/language-server/src/server.ts` (completion and hover handlers)
- Test: `packages/language-server/test/api-call-completion.test.ts`
**Interfaces:**
- Consumes: `ast.dataApis` entries, which carry `name`, `method`, `path`, and `sections`.
- Produces: completion items for `api.` and hover detail for a block name.
This is where the syntax pays off in the editor: the set of legal calls is knowable, so the editor should know it.
- [ ] **Step 1: Write the failing test**
Create `packages/language-server/test/api-call-completion.test.ts`:
```ts
import { expect, test } from "bun:test";
import { apiCallCompletions, apiCallHover } from "../src/server.ts";
const SOURCE = `page Search {
apis {
searchUsers POST /api/users {
request { body { name?: string } }
response { return data.users }
}
listTeams GET /api/teams {
response { return data.teams }
}
}
functions {
client async function go(): Promise<void> {
await api.
}
}
view { <main>x</main> }
}
`;
test("api. offers every declared block with method and path", () => {
const items = apiCallCompletions(SOURCE);
const labels = items.map((item) => item.label);
expect(labels).toContain("searchUsers");
expect(labels).toContain("listTeams");
const search = items.find((item) => item.label === "searchUsers")!;
expect(search.detail).toContain("POST");
expect(search.detail).toContain("/api/users");
});
test("hovering a block name reports its method, path and request fields", () => {
const hover = apiCallHover(SOURCE, "searchUsers");
expect(hover).toContain("POST");
expect(hover).toContain("/api/users");
expect(hover).toContain("name");
});
test("a page with no apis block offers nothing", () => {
expect(apiCallCompletions(`page P { view { <main>x</main> } }`)).toEqual([]);
});
```
- [ ] **Step 2: Run the test to verify it fails**
Run: `bun test packages/language-server/test/api-call-completion.test.ts`
Expected: FAIL — the functions do not exist.
- [ ] **Step 3: Implement and export both functions**
Add `apiCallCompletions(source: string)` and `apiCallHover(source: string, name: string)` to `packages/language-server/src/server.ts`, parsing with `@wrnexus/syntax` and reading `ast.dataApis`. Wire `apiCallCompletions` into the `textDocument/completion` handler for positions immediately after `api.`, and `apiCallHover` into `textDocument/hover`.
The parser must tolerate the half-typed `await api.` in the fixture. If it throws, fall back to returning `[]` rather than failing the request — completion fires while the document does not parse, which is the normal case.
- [ ] **Step 4: Run the tests**
Run: `bun test packages/language-server`
Expected: PASS.
- [ ] **Step 5: Commit**
```bash
bun run format
git add packages/language-server
git commit -m "feat(language-server): complete and describe api block calls"
```
---
### Task 4: The `api=` attribute in markup
**Files:**
- Modify: `packages/language-server/src/html-service.ts`
- Test: `packages/language-server/test/api-attribute.test.ts`
**Interfaces:**
- Consumes: `apiCallCompletions` from Task 3.
- Produces: `api="…"` is not flagged as unknown, and completion inside the quotes offers block names.
- [ ] **Step 1: Write the failing test**
Create `packages/language-server/test/api-attribute.test.ts` asserting that (a) an `api="searchUsers"` attribute produces no unknown-attribute diagnostic, and (b) completion inside the quotes offers `searchUsers`. Reuse the fixture shape from Task 3.
- [ ] **Step 2: Run the test to verify it fails**
Run: `bun test packages/language-server/test/api-attribute.test.ts`
Expected: FAIL.
- [ ] **Step 3: Teach the HTML service about the attribute**
Treat `api` as a known attribute on any element, and route completion inside its quotes to `apiCallCompletions`. The value is a call expression, not text — it must not be spell-checked or reformatted as prose.
- [ ] **Step 4: Run the tests and commit**
```bash
bun test packages/language-server
bun run format
git add packages/language-server
git commit -m "feat(language-server): understand the api binding attribute"
```
---
### Task 5: Diagnostics for the removed constructs
**Files:**
- Modify: `packages/language-server/src/diagnostics` entry point (wherever `.wrn` diagnostics are produced)
- Test: `packages/language-server/test/removed-construct-diagnostics.test.ts`
**Interfaces:**
- Produces: an `ssr { api … }` or `client { api … }` block yields a diagnostic naming `apis { }`, positioned on the block keyword.
The compiler already rejects these. The editor should say so while typing, and say what to do instead.
- [ ] **Step 1: Write the failing test**
Create `packages/language-server/test/removed-construct-diagnostics.test.ts`:
```ts
import { expect, test } from "bun:test";
import { diagnoseWrn } from "../src/index.ts";
test("an ssr data block is flagged and names the replacement", () => {
const diagnostics = diagnoseWrn(`page P {
ssr { api x GET /api/x { response { return data } } }
view { <main>x</main> }
}
`);
expect(diagnostics.length).toBeGreaterThan(0);
expect(diagnostics[0]!.message).toContain("apis");
});
test("client state is not flagged", () => {
const diagnostics = diagnoseWrn(`page P {
client state { count = 0 }
view { <main>x</main> }
}
`);
expect(diagnostics.filter((item) => item.severity === 1)).toEqual([]);
});
```
Use whichever diagnostic entry point the language server exports; keep the assertions identical.
- [ ] **Step 2: Run the test to verify it fails**
Run: `bun test packages/language-server/test/removed-construct-diagnostics.test.ts`
Expected: FAIL — either no diagnostic, or one that does not name `apis`.
- [ ] **Step 3: Surface the parse error as a diagnostic**
The parser already throws a message naming `apis { }` for these blocks. Ensure that message reaches the diagnostic with a position on the offending keyword rather than at offset zero.
- [ ] **Step 4: Run the tests and commit**
```bash
bun test packages/language-server
bun run format
git add packages/language-server
git commit -m "feat(language-server): flag the removed data blocks"
```
---
### Task 6: Answer the inline-diagnostics question, then the full gate
**Files:**
- Modify: whatever the observation in Step 2 shows is needed, or none
**Interfaces:**
- Consumes: Tasks 1-5.
The `apis` plan generates type assertions that make `tsc` fail when a block declares a field its endpoint rejects. **Whether that failure appears inside the `.wrn` file has never been confirmed** — it was inferred from reading source. This task settles it by looking.
- [ ] **Step 1: Rebuild the bundles**
```bash
bun run --cwd editors/vscode build
```
- [ ] **Step 2: Observe, and write down what you see**
In `examples/basic-app`, add a field to an `apis { }` entry that its endpoint does not accept, and run `bun run --cwd examples/basic-app wrnexus generate types`. Open the page in VS Code and record exactly where the error appears: on the block, only in `app/types/wrnexus.generated.api-checks.ts`, or nowhere.
Write the answer into the task report. **Do not skip this step and reason about it instead** — that is what left the question open the first time.
- [ ] **Step 3: Act on what you observed**
If the error already surfaces usefully on the block, document it and stop.
If it appears only in the generated file, map the diagnostic back: the generator knows which page and block produced each assertion, so record that mapping when emitting and use it to relocate the diagnostic.
If that mapping proves larger than this task can hold, **stop and report it as follow-up work** rather than half-building it. Say so plainly in the report.
- [ ] **Step 4: Remove the temporary field**
Revert the deliberate error and confirm `bun run typecheck` passes with zero net diff in `examples/basic-app`.
- [ ] **Step 5: Full gate**
```bash
bun run format
bun test
bun run typecheck
bun run --cwd editors/vscode build
bun run --cwd editors/vscode test
bun run check:production
```
Expected: exit 0 throughout, including `check:editor-compiler`, `check:editor-language-server`, and `check:editor-extension`.
- [ ] **Step 6: Manual pass, recorded**
Open the migrated `examples/basic-app` in VS Code and confirm: `apis { }` highlights, `api.` completes with the page's block names, hovering a name shows its method and path, and an `ssr { api … }` block is flagged. Record what you saw in the report — including anything that did not work.
- [ ] **Step 7: Commit**
```bash
git add -A
git commit -m "feat(editor): complete tooling support for the apis block"
```
---
## Notes for the executor
- **`client` is not one thing.** Removing every `client` rule from the grammar would break `client state`, `runtime = "client"`, and `client function`. Only the data-block patterns go.
- **The parser must tolerate half-typed input.** Completion fires while the document does not parse; a thrown error must become an empty completion list, not a failed request.
- **Step 2 of Task 6 is an observation, not a deduction.** Open the editor and look.
- **If a test would still pass with the code it guards deleted, it is not a test.**
@@ -0,0 +1,537 @@
# Legacy and Config Cleanup Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Delete the compatibility-flag surface, the `"legacy"` function runtime, dead migrations, and deprecated APIs before the framework's first public release.
**Architecture:** Almost all of this is deletion. Seven config keys are never read by any code, so removing them changes nothing. The one behaviour-sensitive item is the `"legacy"` function runtime, which is mapped to `"shared"` — an equivalent substitution, because an unmarked function is already emitted into both bundles.
**Tech Stack:** Bun, TypeScript, `bun:test`.
**Spec:** `docs/superpowers/specs/2026-08-19-legacy-and-config-cleanup-design.md`
## Global Constraints
- This plan removes configuration. It adds none.
- The legacy `api` block forms (bare-body `with ($data)`, and `api` inside `ssr {}` / `client {}`) are **out of scope** — they are replaced by the next plan, not deleted here.
- A removed config key must be **rejected loudly**, not silently ignored. Someone with a stale config must be told, not left believing a flag still applies.
- `bun run format` before every commit; the repo gate is `bun run check:production`.
- The editor bundles embed the compiler — rebuild with `bun run --cwd editors/vscode build` after any `packages/syntax` or `packages/compiler` change, or `check:editor-compiler` fails on a stale bundle.
- Do NOT use `node -e`, shell heredocs, or `sed` to write code into files; escaping mangles them silently. Use file editing tools.
---
### Task 1: Replace the `"legacy"` function runtime with `"shared"`
**Files:**
- Modify: `packages/syntax/src/v060.ts` (the `FunctionRuntime` type; the default at ~line 209)
- Modify: `packages/compiler/src/client-codegen.ts` (membership tests at ~lines 173 and 340)
- Modify: `packages/compiler/src/server-codegen.ts` (the `["legacy", "server", "shared"]` list)
- Modify: `packages/compiler/src/codegen.ts` (`targetFunctions`, ~line 1310)
- Test: `packages/compiler/test/legacy-runtime-removal.test.ts`
**Interfaces:**
- Produces: `FunctionRuntime` becomes `"client" | "server" | "shared"`. Later tasks and plans rely on `"legacy"` no longer existing.
**Why this is equivalent, not a behaviour change:** an unmarked `function foo()` currently parses as `"legacy"`, and both codegens include `"legacy"` in their membership tests — `["legacy", "client", "shared"]` for the browser and `["legacy", "server", "shared"]` for the server. So an unmarked function is already emitted into _both_ bundles, exactly like `shared`. `legacyDefaultRuntime` looks like it should modulate this but is never read.
- [ ] **Step 1: Write the failing test**
Create `packages/compiler/test/legacy-runtime-removal.test.ts`:
```ts
import { expect, test } from "bun:test";
import { parse } from "@wrnexus/syntax";
import { generateTargets } from "../src/targets.ts";
const SOURCE = `page Probe {
functions {
function unmarkedHelper() {
return "both";
}
client function clientOnly() {
return "browser";
}
server function serverOnly() {
return "server";
}
}
view { <main>x</main> }
}
`;
test("an unmarked function is emitted into both the browser and server modules", () => {
// This is the property the "legacy" runtime provided. Removing the variant
// must not change it.
const targets = generateTargets(parse(SOURCE));
expect(targets.browser).toContain("unmarkedHelper");
expect(targets.server).toContain("unmarkedHelper");
});
test("marked functions still go only where they belong", () => {
const targets = generateTargets(parse(SOURCE));
expect(targets.browser).toContain("clientOnly");
expect(targets.browser).not.toContain("serverOnly");
expect(targets.server).toContain("serverOnly");
expect(targets.server).not.toContain("clientOnly");
});
test("no emitted target mentions the removed legacy runtime", () => {
const targets = generateTargets(parse(SOURCE));
expect(targets.browser).not.toContain('"legacy"');
expect(targets.server).not.toContain('"legacy"');
});
```
- [ ] **Step 2: Run the test and record the baseline**
Run: `bun test packages/compiler/test/legacy-runtime-removal.test.ts`
Expected: the first two tests PASS (they describe current behaviour and must keep passing), the third may already pass. This test file is a **regression guard written before the change**, so a green run here is correct — record the output.
- [ ] **Step 3: Remove the `"legacy"` variant from the type and parser**
In `packages/syntax/src/v060.ts`:
```ts
export type FunctionRuntime = "client" | "server" | "shared";
```
And at the parse site (~line 209), change the default:
```ts
let runtime: FunctionRuntime = "shared";
```
- [ ] **Step 4: Drop `"legacy"` from every membership test**
In `packages/compiler/src/client-codegen.ts`, both occurrences:
```ts
["client", "shared"].includes(fn.runtime),
```
In `packages/compiler/src/server-codegen.ts`:
```ts
const names = ast.runtimeFunctions
.filter((fn) => ["server", "shared"].includes(fn.runtime))
.map((fn) => fn.name);
```
In `packages/compiler/src/codegen.ts`, `targetFunctions`:
```ts
const runtimes =
target === "browser" ? (["client", "shared"] as const) : (["server", "shared"] as const);
```
Search the repo for any remaining `"legacy"` in these packages and remove each — the string must not survive in `packages/syntax` or `packages/compiler`.
- [ ] **Step 5: Run the tests**
Run: `bun test packages/syntax packages/compiler`
Expected: PASS, including the three guards from Step 1. If the first two now fail, the substitution was not equivalent — stop and report rather than adjusting the test.
- [ ] **Step 6: Rebuild the editor bundles and commit**
```bash
bun run format
bun run --cwd editors/vscode build
git add packages/syntax packages/compiler editors/vscode/src
git commit -m "refactor: replace the legacy function runtime with shared"
```
---
### Task 2: Delete the compatibility surface
**Files:**
- Delete: `packages/styles/src/compatibility.ts`
- Delete: `packages/cli/src/compatibility-command.ts`
- Delete: `packages/cli/test/compatibility.test.ts`
- Modify: `packages/styles/src/config.ts` (`FunctionsConfig` ~233, `CompatibilityConfig` ~242, `AppConfig extends CompatibilityPolicy` ~249, the `functions?:` and `compatibility?:` members, the `resolveCompatibility` validation ~619, and the `CompatibilityPolicy` import ~23)
- Modify: `packages/styles/src/index.ts` (the `./compatibility.ts` exports at ~lines 39-45)
- Modify: `packages/cli/src/index.ts` (dispatch at ~line 295, help text at ~line 75)
- Modify: `packages/cli/src/create.ts` (~lines 263, 278-283)
- Modify: `packages/cli/src/update.ts` (the config insertion string at ~line 389)
- Modify: `packages/styles/test/config.test.ts` (assertions on the removed keys)
- Modify: `examples/basic-app/wrnexus.config.ts`
- Test: `packages/styles/test/removed-config-keys.test.ts`
**Interfaces:**
- Consumes: nothing from Task 1.
- Produces: `AppConfig` no longer extends `CompatibilityPolicy` and has no `compatibility` or `functions` members. `@wrnexus/styles` no longer exports `resolveCompatibility`, `isCompatibilityDate`, `CURRENT_COMPATIBILITY_DATE`, `CURRENT_FRAMEWORK_BEHAVIOUR`, `CompatibilityPolicy`, or `CompatibilityReport`.
**These seven keys are never read.** `legacyEmit`, `legacyEventProps`, `legacyComponentDiscovery`, `stringLayouts`, and `legacyDefaultRuntime` appear only in the type declaration, `create.ts`, and `update.ts`. `compatibilityDate` and `frameworkBehaviour` feed only a printed report and one validation. Removing them changes no behaviour.
- [ ] **Step 1: Write the failing test**
Create `packages/styles/test/removed-config-keys.test.ts`:
```ts
import { expect, test } from "bun:test";
import { validateConfig } from "../src/config.ts";
// A stale config must fail loudly. Silently ignoring a removed key leaves
// someone believing a flag still applies.
const REMOVED = [
{ key: "compatibilityDate", config: { compatibilityDate: "2026-08-02" } },
{ key: "frameworkBehaviour", config: { frameworkBehaviour: 1 } },
{ key: "functions", config: { functions: { legacyDefaultRuntime: "current" } } },
{ key: "compatibility", config: { compatibility: { legacyEmit: false } } },
];
for (const { key, config } of REMOVED) {
test(`a config still setting "${key}" is rejected with a message naming it`, () => {
const issues = validateConfig(config as never);
const match = issues.find((issue) => issue.path === key || issue.path.startsWith(`${key}.`));
expect(match).toBeDefined();
expect(match!.severity).toBe("error");
expect(match!.message.toLowerCase()).toContain("removed");
});
}
test("a config without those keys is accepted", () => {
const issues = validateConfig({} as never);
expect(issues.filter((issue) => issue.severity === "error")).toEqual([]);
});
```
If `validateConfig` is not the exported name in `packages/styles/src/config.ts`, use whichever function that module exports for validation and keep the assertions identical.
- [ ] **Step 2: Run the test to verify it fails**
Run: `bun test packages/styles/test/removed-config-keys.test.ts`
Expected: FAIL — the keys are currently accepted, so no issue is produced.
- [ ] **Step 3: Delete the compatibility module and its command**
```bash
git rm packages/styles/src/compatibility.ts packages/cli/src/compatibility-command.ts packages/cli/test/compatibility.test.ts
```
In `packages/styles/src/index.ts`, remove the whole `./compatibility.ts` export block (both the value exports and the `export type` line).
In `packages/cli/src/index.ts`, remove the `case "compatibility":` dispatch and the `wrnexus compatibility …` line from the help text.
- [ ] **Step 4: Remove the config members and add the rejections**
In `packages/styles/src/config.ts`: delete the `CompatibilityPolicy` import, the `FunctionsConfig` and `CompatibilityConfig` interfaces, the `functions?:` and `compatibility?:` members of `AppConfig`, `extends CompatibilityPolicy` on `AppConfig`, and the `resolveCompatibility` validation block.
Then add the rejections so a stale config fails loudly:
```ts
const REMOVED_CONFIG_KEYS = [
"compatibilityDate",
"frameworkBehaviour",
"functions",
"compatibility",
] as const;
for (const key of REMOVED_CONFIG_KEYS) {
if ((config as Record<string, unknown>)[key] !== undefined) {
issues.push({
path: key,
severity: "error",
message: "was removed; delete it from the configuration",
});
}
}
```
Place this beside the other validation pushes, using whatever local variable that function accumulates issues in.
- [ ] **Step 5: Stop scaffolding and inserting the keys**
In `packages/cli/src/create.ts`, delete the `compatibilityDate`, `frameworkBehaviour`, and `functions: { legacyDefaultRuntime: … }` lines from the generated config.
In `packages/cli/src/update.ts` (~line 389), remove `functions: { legacyDefaultRuntime: "current" },` and the whole `compatibility: { … },` fragment from the insertion string.
- [ ] **Step 6: Trim the example app config**
In `examples/basic-app/wrnexus.config.ts`, delete `compatibilityDate`, `frameworkBehaviour`, `functions`, and `compatibility`.
- [ ] **Step 7: Update the existing config tests**
`packages/styles/test/config.test.ts` asserts on the removed keys. Remove those assertions. Do not weaken any assertion that is still meaningful — if a test only existed to cover compatibility, delete the whole test.
- [ ] **Step 8: Run the tests**
Run: `bun test packages/styles packages/cli`
Expected: PASS, including the new rejection tests.
- [ ] **Step 9: Commit**
```bash
bun run format
git add -A packages/styles packages/cli examples/basic-app
git commit -m "refactor: delete the compatibility config surface"
```
---
### Task 3: Drop migrations below 0.8.0
**Files:**
- Modify: `packages/cli/src/update.ts` (all `Migration` entries with `version` below `"0.8.0"`)
- Test: `packages/cli/test/update-migration-floor.test.ts`
**Interfaces:**
- Consumes: nothing.
- Produces: the migration list starts at `0.8.0`.
`update.ts` holds 111 migrations reaching back to `0.2.8`. The framework is pre-public and the only projects run `0.8.x`, so everything below the floor is unreachable.
- [ ] **Step 1: Write the failing test**
Create `packages/cli/test/update-migration-floor.test.ts`:
```ts
import { expect, test } from "bun:test";
import { readFileSync } from "node:fs";
import { join } from "node:path";
test("no migration targets a version below 0.8.0", () => {
const source = readFileSync(join(import.meta.dir, "../src/update.ts"), "utf8");
const versions = [...source.matchAll(/version:\s*"([0-9.]+)"/g)].map((match) => match[1]!);
expect(versions.length).toBeGreaterThan(0);
const belowFloor = versions.filter((version) => {
const [major, minor] = version.split(".").map(Number);
return major! === 0 && minor! < 8;
});
expect(belowFloor).toEqual([]);
});
```
- [ ] **Step 2: Run the test to verify it fails**
Run: `bun test packages/cli/test/update-migration-floor.test.ts`
Expected: FAIL, listing the `0.2.x``0.7.x` versions.
- [ ] **Step 3: Delete the migrations below the floor**
Remove every `Migration` object whose `version` is below `"0.8.0"`, along with any helper function that becomes unused as a result. Keep every `0.8.x` entry.
After deleting, search for now-unreferenced helpers in the file and remove them too — an unused private helper is dead code, and the linter will flag it.
- [ ] **Step 4: Run the tests**
Run: `bun test packages/cli`
Expected: PASS. Existing update tests that exercised removed migrations should be deleted with them; do not keep a test that asserts nothing.
- [ ] **Step 5: Verify `update` still runs end to end**
```bash
bun run --cwd examples/basic-app wrnexus update --dry-run
```
Expected: completes without error and reports no pending migrations for an app already at the current version. Paste the output into the commit body if it is short.
- [ ] **Step 6: Commit**
```bash
bun run format
git add packages/cli
git commit -m "chore: drop update migrations below 0.8.0"
```
---
### Task 4: Remove the deprecated compiler re-export shims
**Files:**
- Modify: `packages/compiler/src/codegen.ts` (~line 20, the `./parser.ts` import)
- Modify: `packages/compiler/src/native-codegen.ts` (~line 1, the `./parser.ts` import)
- Delete: `packages/compiler/src/parser.ts`, `packages/compiler/src/tokenizer.ts`, `packages/compiler/src/types.ts`
**Interfaces:**
- Consumes: nothing.
- Produces: nothing new; imports move to `@wrnexus/syntax`.
**Order matters.** These three files are two-line re-exports marked deprecated, but `codegen.ts` and `native-codegen.ts` still import from them. Deleting the files first breaks the build.
- [ ] **Step 1: Repoint the imports**
In `packages/compiler/src/codegen.ts`, change:
```ts
import { VOID_ELEMENTS, type Attr, type DataMode, type PageAst, type ViewNode } from "./parser.ts";
```
to import the same names from `@wrnexus/syntax`. If the file already imports from `@wrnexus/syntax`, merge them into that one import rather than adding a second.
In `packages/compiler/src/native-codegen.ts`, change:
```ts
import type { Attr, PageAst, ViewNode } from "./parser.ts";
```
the same way.
- [ ] **Step 2: Verify nothing else imports the shims**
```bash
grep -rn "from \"./parser.ts\"\|from \"./tokenizer.ts\"\|from \"./types.ts\"" packages/compiler/src/
```
Expected: no output. If anything remains, repoint it before continuing.
- [ ] **Step 3: Delete the shims**
```bash
git rm packages/compiler/src/parser.ts packages/compiler/src/tokenizer.ts packages/compiler/src/types.ts
```
- [ ] **Step 4: Run the tests**
Run: `bun test packages/compiler && bun run typecheck`
Expected: PASS.
- [ ] **Step 5: Rebuild the editor bundles and commit**
```bash
bun run format
bun run --cwd editors/vscode build
git add -A packages/compiler editors/vscode/src
git commit -m "refactor: drop the deprecated compiler re-export shims"
```
---
### Task 5: Remove the deprecated `@wrnexus/auth` options
**Files:**
- Modify: `packages/auth/src/http/index.ts` (~lines 56-59)
- Modify: `packages/auth/src/plugin.ts` (~lines 55-58)
- Modify: `packages/auth/src/types.ts` (~line 423)
- Modify: `packages/auth/src/engine.ts` (~lines 163-165 and 179-181)
- Modify: `packages/auth/test/http.test.ts`, `packages/auth/test/plugin.test.ts`, `packages/auth/test/engine.test.ts`
**Interfaces:**
- Consumes: nothing.
- Produces: nothing new. Options are removed, not renamed.
**These are our own superseded options, not an out-of-date dependency.** The current form is already what `examples/auth-showcase/app/lib/auth.ts` uses — it passes `onSignedIn` / `onSignedOut` to `createAuthEngine`, which is correct and must not change. The deprecated members are the same names on _different_ option objects.
- [ ] **Step 1: Confirm the blast radius before deleting**
```bash
grep -rn "onSuccessfullSignUp" packages/ examples/ services/ | grep -v dist/
grep -rn "onSignedIn\|onSignedOut" packages/ examples/ --include=*.ts | grep -v "packages/auth/src" | grep -v dist/
```
Expected: `onSuccessfullSignUp` has zero references. The `onSignedIn` / `onSignedOut` hits are `packages/auth/test/http.test.ts`, `packages/auth/test/plugin.test.ts`, and `examples/auth-showcase/app/lib/auth.ts`. **The example is the correct `createAuthEngine` form and must be left alone.** Record what you found; if the results differ from this, stop and report before deleting anything.
- [ ] **Step 2: Remove the option declarations**
Delete `onSignedIn` and `onSignedOut` (and their `@deprecated` comments) from the options interface in `packages/auth/src/http/index.ts` and from `packages/auth/src/plugin.ts`. Delete `onSuccessfullSignUp` from `packages/auth/src/types.ts`. Delete the `rpId` and `origin` members from both verification signatures in `packages/auth/src/engine.ts`.
Then remove the code that reads them. The `rpId` / `origin` values are already ignored — verification uses the values bound to the issued challenge — so removing them changes no behaviour.
- [ ] **Step 3: Update the tests that exercised the deprecated paths**
`packages/auth/test/http.test.ts` and `plugin.test.ts` pass the deprecated options. Rewrite each to use the `createAuthEngine` form where the test is still meaningful, and delete the test where its only purpose was to cover the deprecated alias.
`packages/auth/test/engine.test.ts` passes `rpId` / `origin` to verification. Remove those arguments; the assertions on the verification result should be unchanged, which is the evidence that the options were inert.
- [ ] **Step 4: Run the tests**
Run: `bun test packages/auth && bun run typecheck`
Expected: PASS.
- [ ] **Step 5: Confirm no `@deprecated` markers remain in auth**
```bash
grep -rn "@deprecated" packages/auth/src/
```
Expected: no output.
- [ ] **Step 6: Commit**
```bash
bun run format
git add -A packages/auth
git commit -m "refactor: remove the deprecated auth options"
```
---
### Task 6: Full gate
**Files:**
- Modify: whatever the gate reports as stale (generated types, public API baseline, editor bundles)
**Interfaces:**
- Consumes: Tasks 1-5.
- Produces: a green `check:production`.
- [ ] **Step 1: Rebuild the editor bundles**
```bash
bun run --cwd editors/vscode build
```
The compiler and language server are embedded there and both changed.
- [ ] **Step 2: Run the full gate**
```bash
bun run format
bun test
bun run typecheck
bun run check:production
```
- [ ] **Step 3: Regenerate anything the gate reports as stale**
`check:public-api` fails when exports change — and this plan removed several from `@wrnexus/styles`. Run `bun run generate:public-api`, then **read the diff and confirm it is removals only**. An unexpected addition means something was exported by accident.
`check:generated-types` may need `bun run --cwd examples/basic-app wrnexus generate types`.
- [ ] **Step 4: Re-run the gate until green**
```bash
bun run check:production
```
Expected: exit 0.
- [ ] **Step 5: Commit**
```bash
git add -A
git commit -m "chore: regenerate baselines after the legacy cleanup"
```
---
## Notes for the executor
- **The seven config keys are dead.** If you find code that actually reads one, stop and report — the spec's central claim would be wrong and the plan needs revisiting.
- **Task 1 is the only behaviour-sensitive change.** Its first two tests describe current behaviour and must pass both before and after. If they fail after, the substitution was not equivalent; report rather than editing the test.
- **The auth example is already correct.** `examples/auth-showcase` uses `createAuthEngine({ onSignedIn })`, which is the current API, not the deprecated one.
- **If a test would still pass with the code it guards deleted, it is not a test.** Delete the implementation, watch it fail, restore it.
@@ -0,0 +1,479 @@
# `wrnexus update` Migration Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** One `wrnexus update` carries an existing project from today's syntax to the syntax left by the cleanup and `apis { }` plans — or refuses precisely, naming the file and the reason.
**Architecture:** These are new `Migration` entries in the existing `update.ts` framework, which already has dry-run support and a report that separates automatic changes from ones needing review. `.wrn` rewriting parses with `@wrnexus/syntax` and re-emits through `formatWrn`, both already imported there.
**Tech Stack:** Bun, TypeScript, `bun:test`, `@wrnexus/syntax`.
**Spec:** `docs/superpowers/specs/2026-08-19-update-migration-design.md`
## Global Constraints
- **A file is transformed correctly, or it is left untouched and reported.** There is no third outcome — never a partial rewrite.
- Every migration is **idempotent**: running it twice changes nothing the second time.
- **Dry-run reports exactly what a real run would change**, and writes nothing.
- 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.
- Migrations attach to the release that ships the breaking change, above the `0.8.0` floor.
- `bun run format` before every commit; the gate is `bun run check:production`.
- Do NOT use `node -e`, shell heredocs, or `sed` to write code into files.
---
### Task 1: Remove the dead config keys
**Files:**
- Modify: `packages/cli/src/update.ts` (add a `Migration`)
- Test: `packages/cli/test/migrate-config-keys.test.ts`
**Interfaces:**
- Produces: a migration with `id: "remove-dead-config-keys"`.
- [ ] **Step 1: Write the failing test**
Create `packages/cli/test/migrate-config-keys.test.ts`:
```ts
import { afterEach, expect, test } from "bun:test";
import { mkdirSync, mkdtempSync, readFileSync, writeFileSync } from "node:fs";
import { rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { runMigrations } from "../src/update.ts";
const roots: string[] = [];
afterEach(() => {
for (const root of roots.splice(0)) rmSync(root, { recursive: true, force: true });
});
const CONFIG = `export default {
compatibilityDate: "2026-08-02",
frameworkBehaviour: 1,
functions: { legacyDefaultRuntime: "current" },
compatibility: { legacyEmit: false, stringLayouts: false },
observability: { sampleRate: 1 },
};
`;
function project(): string {
const root = mkdtempSync(join(tmpdir(), "wrnexus-migrate-"));
roots.push(root);
mkdirSync(join(root, "app"), { recursive: true });
writeFileSync(join(root, "wrnexus.config.ts"), CONFIG);
return root;
}
test("the removed keys are deleted and the rest is kept", async () => {
const root = project();
await runMigrations({ appRoot: root, from: "0.8.0", to: "0.9.0", dryRun: false });
const config = readFileSync(join(root, "wrnexus.config.ts"), "utf8");
expect(config).not.toContain("compatibilityDate");
expect(config).not.toContain("frameworkBehaviour");
expect(config).not.toContain("legacyDefaultRuntime");
expect(config).not.toContain("legacyEmit");
expect(config).toContain("observability");
});
test("running it twice changes nothing the second time", async () => {
const root = project();
await runMigrations({ appRoot: root, from: "0.8.0", to: "0.9.0", dryRun: false });
const once = readFileSync(join(root, "wrnexus.config.ts"), "utf8");
await runMigrations({ appRoot: root, from: "0.8.0", to: "0.9.0", dryRun: false });
expect(readFileSync(join(root, "wrnexus.config.ts"), "utf8")).toBe(once);
});
test("a dry run writes nothing", async () => {
const root = project();
await runMigrations({ appRoot: root, from: "0.8.0", to: "0.9.0", dryRun: true });
expect(readFileSync(join(root, "wrnexus.config.ts"), "utf8")).toBe(CONFIG);
});
```
Use whatever entry point `update.ts` exports for running migrations; if the name differs from `runMigrations`, adapt the calls and keep the assertions identical.
- [ ] **Step 2: Run the test to verify it fails**
Run: `bun test packages/cli/test/migrate-config-keys.test.ts`
Expected: FAIL — the keys survive.
- [ ] **Step 3: Add the migration**
Append to the migration list in `packages/cli/src/update.ts`:
```ts
{
version: "0.9.0",
id: "remove-dead-config-keys",
description: "Delete compatibilityDate, frameworkBehaviour, functions, and compatibility",
apply(ctx) {
const file = join(ctx.appRoot, "wrnexus.config.ts");
if (!existsSync(file)) return;
const before = readFileSync(file, "utf8");
// Each key is a whole property line or block; removing the line leaves
// valid TypeScript because these are always object members.
const after = before
.replace(/^\s*compatibilityDate:.*\n/m, "")
.replace(/^\s*frameworkBehaviour:.*\n/m, "")
.replace(/^\s*functions:\s*\{[^}]*\},?\s*\n/m, "")
.replace(/^\s*compatibility:\s*\{[^}]*\},?\s*\n/m, "");
if (after === before) return;
ctx.report.changedAutomatically.push(`${file}: removed dead compatibility keys`);
if (!ctx.dryRun) writeFileSync(file, after, "utf8");
},
},
```
- [ ] **Step 4: Run the tests**
Run: `bun test packages/cli`
Expected: PASS.
- [ ] **Step 5: Commit**
```bash
bun run format
git add packages/cli
git commit -m "feat(cli): migrate away the dead config keys"
```
---
### Task 2: Move `ssr { api … }` / `client { api … }` into `apis { }`
**Files:**
- Create: `packages/cli/src/migrations/apis-block.ts`
- Modify: `packages/cli/src/update.ts` (register the migration)
- Test: `packages/cli/test/migrate-apis-block.test.ts`
**Interfaces:**
- Produces: `migrateApisBlock(source: string): { source: string; changed: boolean } | { skip: string }` — a pure function over `.wrn` text, so it is testable without a filesystem. `skip` carries the human-readable reason.
Sectioned bodies carry across unchanged, because the payload is already bound to `data`.
- [ ] **Step 1: Write the failing test**
Create `packages/cli/test/migrate-apis-block.test.ts`:
```ts
import { expect, test } from "bun:test";
import { migrateApisBlock } from "../src/migrations/apis-block.ts";
const SOURCE = `page Search {
client {
api searchUsers POST /api/users {
request { body { name?: string } }
response { return data.users }
error { return [] }
}
}
view { <main>x</main> }
}
`;
test("a client api entry moves into an apis block", () => {
const result = migrateApisBlock(SOURCE) as { source: string; changed: boolean };
expect(result.changed).toBe(true);
expect(result.source).toContain("apis {");
expect(result.source).toContain("searchUsers POST /api/users");
expect(result.source).not.toContain("client {\n api");
});
test("the sections survive unchanged", () => {
const result = migrateApisBlock(SOURCE) as { source: string };
expect(result.source).toContain("return data.users");
expect(result.source).toContain("return []");
});
test("running it on migrated source changes nothing", () => {
const once = (migrateApisBlock(SOURCE) as { source: string }).source;
const twice = migrateApisBlock(once) as { source: string; changed: boolean };
expect(twice.changed).toBe(false);
expect(twice.source).toBe(once);
});
test("a name declared in both modes is skipped with a reason", () => {
const clash = `page P {
ssr { api dup GET /api/a { response { return data } } }
client { api dup GET /api/a { response { return data } } }
view { <main>x</main> }
}
`;
const result = migrateApisBlock(clash) as { skip: string };
expect(result.skip).toContain("dup");
});
```
- [ ] **Step 2: Run the test to verify it fails**
Run: `bun test packages/cli/test/migrate-apis-block.test.ts`
Expected: FAIL — the module does not exist.
- [ ] **Step 3: Implement the transform**
Create `packages/cli/src/migrations/apis-block.ts`. Parse with `parse` from `@wrnexus/syntax` to find the entries and validate the file, collect every `api` entry from `ssr` / `client` blocks, detect duplicate names across modes and return `{ skip }` when found, then emit one `apis { }` block and delete the now-empty mode blocks. Re-emit through `formatWrn`.
Detect already-migrated input by checking whether the source has an `apis` block and no mode data blocks; return `{ source, changed: false }`.
- [ ] **Step 4: Register it**
Add a `Migration` with `id: "move-api-blocks"` that walks `app/**/*.wrn`, calls `migrateApisBlock`, and routes the outcome: a change goes to `changedAutomatically`, a `skip` goes to `needsReview` with the file and reason, and a `parse` failure goes to `parseFailures` with the file left untouched.
- [ ] **Step 5: Run the tests**
Run: `bun test packages/cli`
Expected: PASS.
- [ ] **Step 6: Commit**
```bash
bun run format
git add packages/cli
git commit -m "feat(cli): migrate api entries into the apis block"
```
---
### Task 3: Move mode-scoped helpers into `functions { shared … }`
**Files:**
- Create: `packages/cli/src/migrations/mode-functions.ts`
- Modify: `packages/cli/src/update.ts`
- Test: `packages/cli/test/migrate-mode-functions.test.ts`
**Interfaces:**
- Produces: `migrateModeFunctions(source: string): { source: string; changed: boolean } | { skip: string }`.
- [ ] **Step 1: Write the failing test**
Create `packages/cli/test/migrate-mode-functions.test.ts`:
```ts
import { expect, test } from "bun:test";
import { migrateModeFunctions } from "../src/migrations/mode-functions.ts";
const SOURCE = `page Hello {
ssr {
functions {
function userNames(users) {
return users.map((user) => user.name).join(", ")
}
}
}
view { <main>x</main> }
}
`;
test("a mode helper becomes a shared function", () => {
const result = migrateModeFunctions(SOURCE) as { source: string; changed: boolean };
expect(result.changed).toBe(true);
expect(result.source).toContain("shared function userNames");
expect(result.source).not.toContain("ssr {");
});
test("running it again changes nothing", () => {
const once = (migrateModeFunctions(SOURCE) as { source: string }).source;
const twice = migrateModeFunctions(once) as { changed: boolean; source: string };
expect(twice.changed).toBe(false);
expect(twice.source).toBe(once);
});
test("a name that already exists at page level is skipped with a reason", () => {
const clash = `page P {
functions { shared function userNames() { return "" } }
ssr { functions { function userNames(users) { return "" } } }
view { <main>x</main> }
}
`;
const result = migrateModeFunctions(clash) as { skip: string };
expect(result.skip).toContain("userNames");
});
```
- [ ] **Step 2: Run the test to verify it fails**
Run: `bun test packages/cli/test/migrate-mode-functions.test.ts`
Expected: FAIL — the module does not exist.
- [ ] **Step 3: Implement and register**
Create the module following Task 2's shape: relocate each mode-scoped function into the page-level `functions { }` with the `shared` modifier, skipping the file with a reason when a name already exists there. Register a `Migration` with `id: "move-mode-functions"` that routes outcomes to the same three report buckets.
- [ ] **Step 4: Run the tests**
Run: `bun test packages/cli`
Expected: PASS.
- [ ] **Step 5: Commit**
```bash
bun run format
git add packages/cli
git commit -m "feat(cli): migrate mode-scoped helpers to shared functions"
```
---
### Task 4: Detect legacy bare-body blocks and report them — do not rewrite
**Files:**
- Create: `packages/cli/src/migrations/legacy-api-body.ts`
- Modify: `packages/cli/src/update.ts`
- Test: `packages/cli/test/migrate-legacy-api-body.test.ts`
**Interfaces:**
- Produces: `detectLegacyApiBodies(source: string): { name: string; freeIdentifiers: string[] }[]`.
**This transform is deliberately manual, and the test pins that.** A legacy bare body is evaluated inside `with ($data ?? {})`, so it references payload fields as bare identifiers. Converting `return userNames(users)` needs `data.users` — but **nothing in the source distinguishes `users` (payload) from `userNames` (page helper)**. The response shape belongs to the route, which may not be typed. A migration that guessed would emit code that compiles and is wrong.
- [ ] **Step 1: Write the failing test**
Create `packages/cli/test/migrate-legacy-api-body.test.ts`:
```ts
import { expect, test } from "bun:test";
import { detectLegacyApiBodies } from "../src/migrations/legacy-api-body.ts";
const SOURCE = `page Hello {
ssr {
api ssrUsers GET /api/users/ssr {
return userNames(users)
}
}
view { <main>x</main> }
}
`;
test("a legacy bare body is detected with its free identifiers", () => {
const found = detectLegacyApiBodies(SOURCE);
expect(found).toHaveLength(1);
expect(found[0]!.name).toBe("ssrUsers");
expect(found[0]!.freeIdentifiers).toContain("users");
expect(found[0]!.freeIdentifiers).toContain("userNames");
});
test("a sectioned block is not reported", () => {
const sectioned = `page P {
apis { x GET /api/x { response { return data.users } } }
view { <main>x</main> }
}
`;
expect(detectLegacyApiBodies(sectioned)).toEqual([]);
});
```
- [ ] **Step 2: Run the test to verify it fails**
Run: `bun test packages/cli/test/migrate-legacy-api-body.test.ts`
Expected: FAIL — the module does not exist.
- [ ] **Step 3: Implement detection only**
Create the module. Find `api` entries whose `sections` is absent (the bare-body form), and collect the free identifiers in the body — identifiers that are not declared locally and are not JavaScript globals. Return them. **Write no transform.**
- [ ] **Step 4: Register a report-only migration**
Add a `Migration` with `id: "report-legacy-api-bodies"` that pushes one `needsReview` entry per block, naming the file, the block, and the identifiers, and leaves the file byte-identical. Have `wrnexus update` print a short line explaining why this one is manual: the payload fields cannot be told apart from page helpers without knowing the route's response shape.
- [ ] **Step 5: Write the byte-identical test**
Add a test that runs the full migration over a fixture project containing a legacy bare body and asserts the file's contents are unchanged afterwards, and that the report names the block. **This is the most important test in the plan** — it pins that the migration does not attempt the rewrite.
- [ ] **Step 6: Run the tests**
Run: `bun test packages/cli`
Expected: PASS.
- [ ] **Step 7: Commit**
```bash
bun run format
git add packages/cli
git commit -m "feat(cli): report legacy api bodies for manual migration"
```
---
### Task 5: Exit code, output order, and the end-to-end run
**Files:**
- Modify: `packages/cli/src/update.ts` (the command's output and exit code)
- Test: `packages/cli/test/update-exit-code.test.ts`
**Interfaces:**
- Consumes: Tasks 1-4.
- [ ] **Step 1: Write the failing test**
Create `packages/cli/test/update-exit-code.test.ts` asserting that a project with a legacy bare body produces a non-zero exit, and a fully-migratable project produces zero. Use the same temp-project pattern as Task 1.
- [ ] **Step 2: Run the test to verify it fails**
Run: `bun test packages/cli/test/update-exit-code.test.ts`
Expected: FAIL — the command currently exits zero regardless.
- [ ] **Step 3: Implement the output and exit code**
Print, in order: what changed, what needs review and why, what failed to parse. Exit non-zero when `needsReview` or `parseFailures` is non-empty.
- [ ] **Step 4: Migrate the example app with the command alone**
```bash
bun run --cwd examples/basic-app wrnexus update
```
Expected: the `.wrn` pages are migrated by the tool, not by hand. **If the framework's own example cannot be migrated by the tool, the tool is not finished** — report that rather than editing the example manually.
- [ ] **Step 5: Verify the migrated example**
```bash
bun run --cwd examples/basic-app build
bun test
bun run typecheck
bun run check:production
```
- [ ] **Step 6: Commit**
```bash
bun run format
git add -A
git commit -m "feat(cli): fail the update when a project needs manual review"
```
---
## Notes for the executor
- **Never half-rewrite a file.** Parse first; on failure, record and move on. If any part of a file's transform cannot complete, skip the whole file and report it.
- **Idempotency is not optional.** Every transform detects already-migrated input.
- **Task 4 writes no transform.** If you find yourself building one, stop — the spec explains why a correct automatic answer does not exist.