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 <noreply@anthropic.com>
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user