From 7a1b4e5b3301d2a26c667ac8ce6ca58ca841a64d Mon Sep 17 00:00:00 2001 From: Ajay Ghanwat Date: Thu, 20 Aug 2026 12:24:19 +0530 Subject: [PATCH] fix(cli): guard the one migration write path that skips parse validation mode-functions writes without a parse check when a mode wrapper survives holding api entries, since that intermediate state is unparseable until move-api-blocks runs later in the same pass. Brace balance is the invariant a bad splice offset would break, so check that instead; nothing downstream could tell a corrupted wrapper from an untouched one. Also records that Task 5's example-app migration ran against an already- migrated target and so did not prove end-to-end behaviour. Co-Authored-By: Claude Opus 5 --- .../plans/2026-08-19-update-migration.md | 17 ++++++++++ packages/cli/src/migrations/mode-functions.ts | 32 ++++++++++++++++++- 2 files changed, 48 insertions(+), 1 deletion(-) diff --git a/docs/superpowers/plans/2026-08-19-update-migration.md b/docs/superpowers/plans/2026-08-19-update-migration.md index 30a46819..27b8109b 100644 --- a/docs/superpowers/plans/2026-08-19-update-migration.md +++ b/docs/superpowers/plans/2026-08-19-update-migration.md @@ -477,3 +477,20 @@ git commit -m "feat(cli): fail the update when a project needs manual review" - **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. + +--- + +## Post-execution note (2026-08-20) + +**Step 4 of Task 5 did not prove what it was written to prove.** By the time it ran, +`examples/basic-app` had already been moved to the current syntax by hand in commit +`890d6106`, so `wrnexus update` migrated a no-op target: 0 changed, 0 needing review, +0 parse failures. The framework's own example therefore does NOT demonstrate the tool +against real legacy syntax. + +The guarantee instead rests on fixture-based tests that drive the real `updateApp` / +`runUpdate` entry points over projects containing genuine `ssr {}` / `client {}` source +-- including the mixed-content page that only converges because `move-mode-functions` +is registered before `move-api-blocks`. That is adequate coverage, but it is a weaker +kind of evidence than the plan intended, and it is recorded here rather than quietly +counted as a pass. diff --git a/packages/cli/src/migrations/mode-functions.ts b/packages/cli/src/migrations/mode-functions.ts index 9135916e..54ad5995 100644 --- a/packages/cli/src/migrations/mode-functions.ts +++ b/packages/cli/src/migrations/mode-functions.ts @@ -32,6 +32,24 @@ function findMatching(source: string, open: number, openChar = "{", closeChar = return -1; } +/** Net `{` minus `}` outside string literals -- a splice that loses a brace changes it. */ +function braceBalance(source: string): number { + let balance = 0; + let quote = ""; + for (let index = 0; index < source.length; index++) { + const char = source[index]!; + if (quote) { + if (char === "\\") index++; + else if (char === quote) quote = ""; + continue; + } + if (char === '"' || char === "'" || char === "`") quote = char; + else if (char === "{") balance++; + else if (char === "}") balance--; + } + return balance; +} + interface ModeBlock { mode: "ssr" | "client"; /** Start of the `ssr`/`client` keyword. */ @@ -261,7 +279,19 @@ export function migrateModeFunctions( if (after === source) return { source, changed: false }; - if (!leavesModeWrapper) { + if (leavesModeWrapper) { + // The only path that writes without a parse check. A surviving `ssr {`/`client {` + // wrapper still holding `api` entries is unparseable by design -- `move-api-blocks` + // finishes the job later in the same run -- so `parse` cannot validate it here. + // Brace balance is the one invariant still checkable, and it is what a bad splice + // offset would break. Downstream nothing else would catch it: the parser rejects a + // corrupted wrapper and an untouched one identically. + if (braceBalance(after) !== braceBalance(source)) { + throw new Error( + "mode-functions migration unbalanced the source braces; refusing to write the file", + ); + } + } else { try { parse(after); } catch (error) {