/**
 * `wrnexus update [dir] [--version=x.y.z | --latest] [--dry-run]`
 *
 * Upgrade an app (or every app in a workspace) to a WrNexus release:
 *   1. Resolve and bump every installed `@wrnexus/*` dependency independently.
 *   2. `bun install`.
 *   3. Refresh framework-owned reference files (public/llms.txt) and apply any
 *      versioned, idempotent migrations that newer releases introduce.
 *   4. Record the applied version in package.json (`"wrnexus": { version }`).
 *
 * Migration version resolution: `--version=x.y.z` > `--latest` (queries npm) >
 * the running CLI's own version (the default — pair with `bunx @wrnexus/cli@latest
 * update` to jump to the newest release with no network guesswork).
 *
 * Migrations are CONSERVATIVE: source rewrites are syntax-aware, idempotent, and
 * protected by a complete pre-update backup. User-owned files are never replaced wholesale. Add new ones to `MIGRATIONS`
 * as the framework evolves — that is how "new things" reach existing apps.
 */

import {
  cpSync,
  existsSync,
  mkdirSync,
  readFileSync,
  readdirSync,
  rmSync,
  statSync,
  writeFileSync,
} from "node:fs";
import { execFile, spawnSync } from "node:child_process";
import { promisify } from "node:util";
import { basename, dirname, join, relative, resolve } from "node:path";
import { pathToFileURL } from "node:url";
import { formatWrn, parse } from "@wrnexus/syntax";
import { AI_GUIDE, CLAUDE_MD } from "./ai-guide.ts";
import { inspectProject, type DoctorCheck } from "./doctor.ts";
import { generateApplicationTypes } from "./types.ts";

/** The version of the CLI currently running (its own package.json). */
function cliVersion(): string {
  try {
    return JSON.parse(readFileSync(join(import.meta.dir, "..", "package.json"), "utf8")).version;
  } catch {
    return "0.0.0";
  }
}

/** Query the registry for the latest published `@wrnexus/cli` version. */
function latestPublished(): string | null {
  try {
    const npm = process.platform === "win32" ? "npm.cmd" : "npm";
    const out = spawnSync(npm, ["view", "@wrnexus/cli", "version"], { encoding: "utf8" });
    const v = (out.stdout ?? "").trim();
    return /^\d+\.\d+\.\d+/.test(v) ? v : null;
  } catch {
    return null;
  }
}

const execFileAsync = promisify(execFile);

/** Resolve every installed WRNexus dependency independently and in parallel. */
export async function latestPackageVersions(names: string[]): Promise<Map<string, string>> {
  const npm = process.platform === "win32" ? "npm.cmd" : "npm";
  const unique = [...new Set(names.filter((name) => name.startsWith("@wrnexus/")))];
  const entries = await Promise.all(
    unique.map(async (name) => {
      try {
        const { stdout } = await execFileAsync(npm, ["view", name, "version", "--json"], {
          encoding: "utf8",
        });
        const parsed = JSON.parse(stdout.trim()) as string | string[];
        const version = Array.isArray(parsed) ? parsed.at(-1) : parsed;
        return typeof version === "string" && /^\d+\.\d+\.\d+/.test(version)
          ? ([name, version] as const)
          : null;
      } catch {
        return null;
      }
    }),
  );
  return new Map(entries.filter((entry): entry is readonly [string, string] => entry !== null));
}

/** Numeric compare of `x.y.z` (pre-release/build tags ignored). */
function cmp(a: string, b: string): number {
  const pa = a.split("-")[0]!.split(".").map(Number);
  const pb = b.split("-")[0]!.split(".").map(Number);
  for (let i = 0; i < 3; i++) {
    const d = (pa[i] || 0) - (pb[i] || 0);
    if (d) return d > 0 ? 1 : -1;
  }
  return 0;
}

interface MigrationCtx {
  appRoot: string;
  from: string;
  to: string;
  dryRun: boolean;
  explicitImports: boolean;
  report: MigrationReport;
  log: (msg: string) => void;
}

export interface MigrationReport {
  changedAutomatically: string[];
  needsReview: string[];
  unresolvedImports: string[];
  ambiguousFunctions: string[];
  legacyOutputPayloads: string[];
  parseFailures: string[];
}

interface Migration {
  /** Framework version that introduced this change. Runs when `from < version <= to`. */
  version: string;
  id: string;
  description: string;
  apply: (ctx: MigrationCtx) => void;
}

const VSCODE_EXTENSIONS = `{
  "recommendations": ["wrnexus.wrnexus", "esbenp.prettier-vscode", "dbaeumer.vscode-eslint"]
}
`;

const LEGACY_VSCODE_EXTENSIONS =
  JSON.stringify(
    {
      recommendations: ["wrnexus.wrnexus", "esbenp.prettier-vscode", "dbaeumer.vscode-eslint"],
    },
    null,
    2,
  ) + "\n";

function walkProjectFiles(dir: string, extension: string): string[] {
  if (!existsSync(dir)) return [];
  const out: string[] = [];
  for (const entry of readdirSync(dir)) {
    if (["node_modules", "dist", ".git", ".wrnexus"].includes(entry)) continue;
    const path = join(dir, entry);
    const stat = statSync(path);
    if (stat.isDirectory()) out.push(...walkProjectFiles(path, extension));
    else if (stat.isFile() && path.endsWith(extension)) out.push(path);
  }
  return out;
}

function formatInlineProps(source: string): string {
  return source.replace(
    /(^[ \t]*)props\s*\{([^{}\n]*)\}/gm,
    (whole, indent: string, body: string) => {
      const declaration =
        /([A-Za-z_$][\w$]*)(\s*:\s*[^=]+?)?\s*=\s*("(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|\[\]|\{\}|true|false|null|undefined|-?\d+(?:\.\d+)?)/gy;
      const values: string[] = [];
      let offset = 0;
      while (offset < body.length) {
        while (/\s/.test(body[offset] ?? "")) offset++;
        if (offset >= body.length) break;
        declaration.lastIndex = offset;
        const match = declaration.exec(body);
        if (!match || match.index !== offset) return whole;
        values.push(`${match[1]}${match[2] ?? ""} = ${match[3]}`);
        offset = declaration.lastIndex;
      }
      if (values.length < 2) return whole;
      return `${indent}props {\n${values.map((value) => `${indent}  ${value}`).join("\n")}\n${indent}}`;
    },
  );
}

function quoteLegacyDynamicAttributes(source: string): string {
  let output = "";
  let index = 0;
  while (index < source.length) {
    const start = source.indexOf("<", index);
    if (start < 0) return output + source.slice(index);
    output += source.slice(index, start);
    if (source.startsWith("<!--", start)) {
      const end = source.indexOf("-->", start + 4);
      if (end < 0) return output + source.slice(start);
      output += source.slice(start, end + 3);
      index = end + 3;
      continue;
    }
    let end = start + 1;
    let quote = "";
    let braceDepth = 0;
    for (; end < source.length; end++) {
      const char = source[end]!;
      if (quote) {
        if (char === "\\") end++;
        else if (char === quote) quote = "";
        continue;
      }
      if (char === '"' || char === "'") quote = char;
      else if (char === "{") braceDepth++;
      else if (char === "}") braceDepth = Math.max(0, braceDepth - 1);
      else if (char === ">" && braceDepth === 0) break;
    }
    if (end >= source.length) return output + source.slice(start);
    let tag = source.slice(start, end + 1);
    if (!/^<\/?[A-Za-z]/.test(tag) || /^<\//.test(tag)) {
      output += tag;
      index = end + 1;
      continue;
    }
    tag = tag.replace(
      /(\s[@:#A-Za-z_$][\w$:.-]*)\s*=\s*\{([^{}']+)\}/g,
      (_all, name: string, expr: string) => `${name}='{${expr.trim()}}'`,
    );
    output += tag;
    index = end + 1;
  }
  return output;
}

export function migrateWrnSource(source: string): string {
  return formatInlineProps(quoteLegacyDynamicAttributes(source))
    .replace(/[ \t]+$/gm, "")
    .replace(/\r\n?/g, "\n")
    .replace(/\n{3,}$/g, "\n\n")
    .replace(/\s*$/, "\n");
}

export function formatCurrentWrnSource(source: string): string {
  return formatWrn(migrateWrnSource(source), {
    insertSpaces: true,
    tabSize: 2,
    printWidth: 100,
    multilineAttributes: true,
  });
}

function findMatching(source: string, open: number, openChar = "{", closeChar = "}"): number {
  let depth = 0;
  let quote = "";
  for (let index = open; 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 === openChar) depth++;
    else if (char === closeChar && --depth === 0) return index;
  }
  return -1;
}

function convertLegacyEmit(source: string, report: MigrationReport, file: string): string {
  let output = "";
  let cursor = 0;
  while (true) {
    const start = source.indexOf("$emit(", cursor);
    if (start < 0) return output + source.slice(cursor);
    output += source.slice(cursor, start);
    const end = findMatching(source, start + 5, "(", ")");
    if (end < 0) {
      report.needsReview.push(`${file}: unterminated $emit call`);
      return output + source.slice(start);
    }
    const args = source.slice(start + 6, end);
    const match = args.match(/^\s*(["'])([A-Za-z_$][\w$:-]*)\1\s*(?:,\s*([\s\S]*))?$/);
    if (!match) {
      report.needsReview.push(`${file}: dynamic $emit requires manual review`);
      output += source.slice(start, end + 1);
    } else {
      const payload = (match[3] ?? "").trim();
      output += payload ? `output.${match[2]}(${payload})` : `output.${match[2]}()`;
    }
    cursor = end + 1;
  }
}

function collectLegacyEvents(source: string): { source: string; outputs: string[] } {
  const outputs: string[] = [];
  const next = source.replace(
    /(^|[\s;])@event\s+([A-Za-z_$][\w$]*)\s*=\s*function\b[ \t]*(?:\r?\n)?/gm,
    (_whole, prefix: string, name: string) => {
      outputs.push(name);
      return prefix;
    },
  );
  return { source: next, outputs };
}

function ensureOutputsBlock(
  source: string,
  names: string[],
  report: MigrationReport,
  file: string,
): string {
  if (!names.length) return source;
  const unique = [...new Set(names)];
  const blockMatch = /\boutputs\s*\{/.exec(source);
  if (blockMatch) {
    const open = source.indexOf("{", blockMatch.index);
    const close = findMatching(source, open);
    if (close < 0) return source;
    const body = source.slice(open + 1, close);
    const missing = unique.filter((name) => !new RegExp(`\\b${name}\\s*\\(`).test(body));
    if (!missing.length) return source;
    const indent = source.slice(source.lastIndexOf("\n", blockMatch.index) + 1, blockMatch.index);
    const additions = missing.map((name) => `${indent}  ${name}(payload: unknown)`).join("\n");
    for (const name of missing)
      report.legacyOutputPayloads.push(`${file}: ${name}(payload: unknown)`);
    return source.slice(0, close) + `\n${additions}\n${indent}` + source.slice(close);
  }
  const rootOpen = source.indexOf("{");
  if (rootOpen < 0) return source;
  const lineStart = source.lastIndexOf("\n", rootOpen) + 1;
  const nonWhitespace = source.slice(lineStart).search(/\S/);
  const rootIndent = nonWhitespace < 0 ? "" : source.slice(lineStart, lineStart + nonWhitespace);
  const indent = `${rootIndent}  `;
  const block = `\n${indent}outputs {\n${unique.map((name) => `${indent}  ${name}(payload: unknown)`).join("\n")}\n${indent}}\n`;
  for (const name of unique) report.legacyOutputPayloads.push(`${file}: ${name}(payload: unknown)`);
  return source.slice(0, rootOpen + 1) + block + source.slice(rootOpen + 1);
}

function classifyLegacyFunctions(source: string, report: MigrationReport, file: string): string {
  const functionsMatch = /\bfunctions\s*\{/.exec(source);
  if (!functionsMatch) return source;
  const open = source.indexOf("{", functionsMatch.index);
  const close = findMatching(source, open);
  if (close < 0) return source;
  const body = source.slice(open + 1, close);
  let output = "";
  let cursor = 0;
  const header =
    /(^|\n)([ \t]*)(?!client\s|server\s|shared\s)(async\s+)?function\s+([A-Za-z_$][\w$]*)\s*\([^)]*\)(?:\s*:\s*[^{\n]+)?\s*\{/g;
  for (let match; (match = header.exec(body));) {
    const bodyOpen = body.indexOf("{", match.index + match[0].length - 1);
    const bodyClose = findMatching(body, bodyOpen);
    if (bodyClose < 0) break;
    const functionBody = body.slice(bodyOpen + 1, bodyClose);
    const client =
      /\b(window|document|localStorage|sessionStorage|navigator|HTMLElement|MouseEvent|KeyboardEvent|output\.|refs\.)\b/.test(
        functionBody,
      );
    const server =
      /\b(ctx|Bun|process|database|db\.|serverService|authService|request|response)\b/.test(
        functionBody,
      );
    let runtime = "";
    if (client && !server) runtime = "client ";
    else if (server && !client) runtime = "server ";
    else if (!client && !server && !/\b(fetch|Date\.now|Math\.random)\b/.test(functionBody))
      runtime = "shared ";
    else report.ambiguousFunctions.push(`${file}: ${match[4]}`);
    output +=
      body.slice(cursor, match.index) +
      match[1] +
      match[2] +
      runtime +
      (match[3] ?? "") +
      `function ${match[4]}`;
    const consumedHeader =
      match[0].lastIndexOf(`function ${match[4]}`) + `function ${match[4]}`.length;
    output += match[0].slice(consumedHeader);
    cursor = match.index + match[0].length;
    header.lastIndex = bodyClose + 1;
  }
  output += body.slice(cursor);
  return source.slice(0, open + 1) + output + source.slice(close);
}

export function migrateV060WrnSource(
  source: string,
  report: MigrationReport,
  file = "<inline>",
): string {
  const legacy = collectLegacyEvents(source);
  let next = ensureOutputsBlock(legacy.source, legacy.outputs, report, file);
  next = convertLegacyEmit(next, report, file);
  next = next.replace(/\bevent\.detail\b/g, "payload");
  next = classifyLegacyFunctions(next, report, file);
  return migrateWrnSource(next);
}

function updateV060Config(ctx: MigrationCtx): void {
  const candidates = ["wrnexus.config.ts", "wrnexus.config.js", "wrnexus.config.mjs"];
  const file = candidates.map((name) => join(ctx.appRoot, name)).find(existsSync);
  if (!file) return;
  const current = readFileSync(file, "utf8");
  if (/\bimports\s*:/.test(current) && /\bcompatibility\s*:/.test(current)) return;
  const insertion = `\n  imports: { mode: "compatible", autoImport: true, aliases: { "@": "./app" } },\n  types: { strict: false, noImplicitAny: false, strictNullChecks: true, checkTemplates: true, checkComponentProps: true, generateDeclarations: true, globalTypes: "./app/types/global.d.ts" },\n  functions: { legacyDefaultRuntime: "current" },\n  stores: { strictMutations: true, persistence: true },\n  compatibility: { legacyEmit: true, legacyEventProps: true, legacyComponentDiscovery: true, stringLayouts: true },`;
  const index = current.lastIndexOf("}");
  if (index < 0) return;
  const next =
    current.slice(0, index).replace(/,?\s*$/, "") + "," + insertion + "\n" + current.slice(index);
  ctx.log(`~ ${file.slice(ctx.appRoot.length + 1)}: v0.6 compatibility configuration`);
  if (!ctx.dryRun) writeFileSync(file, next, "utf8");
}

interface V060SymbolIndex {
  components: Map<string, string[]>;
  layouts: Map<string, string[]>;
  ui: Set<string>;
}

function declarationName(file: string): string {
  const source = readFileSync(file, "utf8");
  return (
    /\b(?:component|layout)\s+([A-Za-z_$][\w$]*)\s*\{/.exec(source)?.[1] ?? basename(file, ".wrn")
  );
}

function addSymbol(index: Map<string, string[]>, name: string, file: string): void {
  const current = index.get(name) ?? [];
  current.push(file);
  index.set(name, current);
}

function loadUiComponentNames(appRoot: string): Set<string> {
  const candidates = [
    join(appRoot, "node_modules", "@wrnexus", "ui", "component-reference.json"),
    join(appRoot, "packages", "ui", "component-reference.json"),
  ];
  for (const candidate of candidates) {
    if (!existsSync(candidate)) continue;
    try {
      const parsed = JSON.parse(readFileSync(candidate, "utf8")) as {
        components?: Array<{ name?: string }>;
      };
      return new Set(
        (parsed.components ?? [])
          .map((entry) => entry.name)
          .filter((name): name is string => Boolean(name)),
      );
    } catch {
      // Continue to an empty package index. The migration reports unresolved tags.
    }
  }
  return new Set();
}

function buildV060SymbolIndex(appRoot: string): V060SymbolIndex {
  const components = new Map<string, string[]>();
  const layouts = new Map<string, string[]>();
  for (const file of walkProjectFiles(join(appRoot, "app", "components"), ".wrn")) {
    addSymbol(components, declarationName(file), file);
  }
  for (const file of walkProjectFiles(join(appRoot, "app", "layouts"), ".wrn")) {
    const name = declarationName(file);
    addSymbol(layouts, name, file);
    addSymbol(layouts, basename(file, ".wrn"), file);
  }
  return { components, layouts, ui: loadUiComponentNames(appRoot) };
}

function importedLocalNames(source: string): Set<string> {
  const names = new Set<string>();
  for (const statement of source.matchAll(
    /^import\s+(?:type\s+)?([\s\S]*?)\s+from\s+["'][^"']+["']\s*;?/gm,
  )) {
    const clause = statement[1]!.trim();
    if (!clause.startsWith("{") && !clause.startsWith("*")) names.add(clause.split(",")[0]!.trim());
    for (const named of clause.matchAll(
      /(?:\{|,)\s*(?:type\s+)?([A-Za-z_$][\w$]*)(?:\s+as\s+([A-Za-z_$][\w$]*))?/g,
    )) {
      names.add(named[2] ?? named[1]!);
    }
  }
  return names;
}

function importPathFor(appRoot: string, file: string): string {
  return `@/${relative(join(appRoot, "app"), file).replace(/\\/g, "/")}`;
}

function insertImports(source: string, statements: string[]): string {
  if (!statements.length) return source;
  const unique = [...new Set(statements)].filter((statement) => !source.includes(statement));
  if (!unique.length) return source;
  const imports = Array.from(
    source.matchAll(
      /^import\s+[\s\S]*?(?:;\s*|\n(?=import|\s*(?:page|component|layout|global\s+store|page\s+store)\b))/gm,
    ),
  );
  const insertion = imports.length ? imports.at(-1)!.index! + imports.at(-1)![0].length : 0;
  const prefix = insertion ? "\n" : "";
  return (
    source.slice(0, insertion) +
    prefix +
    unique.join("\n") +
    "\n\n" +
    source.slice(insertion).replace(/^\s*/, "")
  );
}

function addExplicitImportsToSource(
  source: string,
  file: string,
  appRoot: string,
  index: V060SymbolIndex,
  report: MigrationReport,
): string {
  const imported = importedLocalNames(source);
  const rootName =
    /\b(?:page|component|layout|global\s+store|page\s+store)\s+([A-Za-z_$][\w$]*)/.exec(
      source,
    )?.[1];
  const applicationImports: string[] = [];
  const uiImports = new Set<string>();
  const tags = new Set(
    Array.from(source.matchAll(/<([A-Z][A-Za-z0-9_$]*)\b/g), (match) => match[1]!),
  );
  for (const name of tags) {
    if (name === rootName || imported.has(name)) continue;
    const candidates = index.components.get(name) ?? [];
    if (candidates.length === 1) {
      applicationImports.push(
        `import ${name} from ${JSON.stringify(importPathFor(appRoot, candidates[0]!))}`,
      );
      imported.add(name);
    } else if (candidates.length > 1) {
      report.unresolvedImports.push(
        `${file}: component '${name}' has ${candidates.length} application matches`,
      );
    } else if (index.ui.has(name)) {
      uiImports.add(name);
      imported.add(name);
    } else {
      report.unresolvedImports.push(`${file}: component '${name}' could not be resolved`);
    }
  }
  if (uiImports.size) {
    const existingUi = /import\s*\{([\s\S]*?)\}\s*from\s*["']@wrnexus\/ui["']\s*;?/.exec(source);
    if (existingUi) {
      const names = new Set([
        ...Array.from(
          existingUi[1]!.matchAll(/(?:^|,)\s*([A-Za-z_$][\w$]*)/g),
          (match) => match[1]!,
        ),
        ...uiImports,
      ]);
      source = source.replace(
        existingUi[0],
        `import {\n${[...names]
          .sort()
          .map((name) => `  ${name},`)
          .join("\n")}\n} from "@wrnexus/ui"`,
      );
    } else {
      applicationImports.push(
        `import {\n${[...uiImports]
          .sort()
          .map((name) => `  ${name},`)
          .join("\n")}\n} from "@wrnexus/ui"`,
      );
    }
  }
  return insertImports(source, applicationImports);
}

function migrateImportedLayout(
  source: string,
  file: string,
  appRoot: string,
  index: V060SymbolIndex,
  report: MigrationReport,
): string {
  const match = /\blayout\s*=\s*["']([^"']+)["']/.exec(source);
  if (!match || match[1] === "none") return source;
  const candidates = index.layouts.get(match[1]) ?? [];
  const unique = [...new Set(candidates)];
  if (unique.length !== 1) {
    report.unresolvedImports.push(
      `${file}: layout '${match[1]}' ${unique.length ? "is ambiguous" : "was not found"}`,
    );
    return source;
  }
  const symbol = declarationName(unique[0]!);
  const statement = `import ${symbol} from ${JSON.stringify(importPathFor(appRoot, unique[0]!))}`;
  return insertImports(source.replace(match[0], `layout = ${symbol}`), [statement]);
}

function writeMigrationReport(ctx: MigrationCtx): void {
  const file = join(ctx.appRoot, ".wrnexus", "migrations", "0.6.0-report.json");
  ctx.log(`+ .wrnexus/migrations/0.6.0-report.json`);
  if (ctx.dryRun) return;
  mkdirSync(dirname(file), { recursive: true });
  writeFileSync(
    file,
    JSON.stringify(
      {
        version: "0.6.0",
        from: ctx.from,
        to: ctx.to,
        appliedAt: new Date().toISOString(),
        ...ctx.report,
      },
      null,
      2,
    ) + "\n",
    "utf8",
  );
}

/**
 * Versioned, idempotent upgrade steps. Each MUST be safe to re-run. Source
 * migrations must preserve semantics and are protected by update backups.
 */

function updateV070Config(ctx: MigrationCtx): void {
  const candidates = ["wrnexus.config.ts", "wrnexus.config.js", "wrnexus.config.mjs"];
  const file = candidates.map((name) => join(ctx.appRoot, name)).find(existsSync);
  if (!file) return;
  const current = readFileSync(file, "utf8");
  const additions: string[] = [];
  if (!/\bnavigation\s*:/.test(current)) {
    additions.push(`  navigation: {
    mode: "auto",
  }`);
  }
  if (!/\bsecurity\s*:/.test(current)) {
    additions.push(`  security: {
    headers: true,
    requestLimits: {
      maxUrlLength: 8192,
      maxHeaderCount: 100,
      maxHeaderBytes: 32768,
      maxQueryParameters: 100,
      maxBodyBytes: 10485760,
      timeoutMs: 30000,
      maxConcurrent: 1000,
      fetchMetadata: true,
    },
    contentSecurityPolicy: { enabled: true, useDefaults: true },
    trustedTypes: { enabled: true, requireForScript: true },
    hsts: { enabled: true, maxAge: 63072000, includeSubDomains: true, preload: true },
  }`);
  }
  if (!/\bperformance\s*:/.test(current)) {
    additions.push(`  performance: {
    enforcement: "warn",
    analyze: true,
    budgets: {
      routeJsBytes: 51200,
      routeCssBytes: 25600,
      htmlBytes: 204800,
      hydrationMs: 200,
      serverRenderMs: 500,
      lcpMs: 2500,
      inpMs: 200,
      cls: 0.1,
      ttfbMs: 800,
    },
  }`);
  }
  if (!/\bobservability\s*:/.test(current)) {
    additions.push(`  observability: {
    enabled: true,
    serverTiming: true,
    sampleRate: 0.1,
    exporter: "none",
    webVitals: true,
  }`);
  }
  if (!additions.length) return;
  const index = current.lastIndexOf("}");
  if (index < 0) return;
  const before = current.slice(0, index).replace(/,?\s*$/, "");
  const next = `${before},\n${additions.join(",\n")}\n${current.slice(index)}`;
  ctx.log(`~ ${file.slice(ctx.appRoot.length + 1)}: security/performance production defaults`);
  if (!ctx.dryRun) writeFileSync(file, next, "utf8");
}

const MIGRATIONS: Migration[] = [
  {
    version: "0.2.8",
    id: "gitignore-artifacts",
    description: "Ensure .gitignore covers framework build artifacts",
    apply(ctx) {
      const file = join(ctx.appRoot, ".gitignore");
      const want = ["node_modules/", "dist/", ".wrnexus/", "*.db"];
      const current = existsSync(file) ? readFileSync(file, "utf8") : "";
      const have = new Set(current.split(/\r?\n/).map((l) => l.trim()));
      const missing = want.filter((w) => !have.has(w));
      if (missing.length === 0) return;
      ctx.log(`+ .gitignore: ${missing.join(", ")}`);
      if (ctx.dryRun) return;
      const next = current.replace(/\s*$/, "") + "\n" + missing.join("\n") + "\n";
      writeFileSync(file, next.replace(/^\n+/, ""), "utf8");
    },
  },
  {
    version: "0.2.14",
    id: "complete-gitignore",
    description: "Add the complete WRNexusJS generated-project ignore set",
    apply(ctx) {
      const file = join(ctx.appRoot, ".gitignore");
      const want = [
        "node_modules/",
        "dist/",
        ".wrnexus/",
        "**/.wrnexus/",
        "coverage/",
        ".env",
        ".env.*",
        "!.env.example",
        "*.log",
        "*.db",
        "*.db-shm",
        "*.db-wal",
        "*.sqlite",
        "*.sqlite3",
        "mobile/android/",
        "mobile/ios/",
        "mobile/.expo/",
        ".idea/",
        ".vscode/",
        ".DS_Store",
        "Thumbs.db",
        "*.tsbuildinfo",
        ".eslintcache",
      ];
      const current = existsSync(file) ? readFileSync(file, "utf8") : "";
      const have = new Set(current.split(/\r?\n/).map((line) => line.trim()));
      const missing = want.filter((entry) => !have.has(entry));
      if (!missing.length) return;
      ctx.log(`+ .gitignore: ${missing.join(", ")}`);
      if (!ctx.dryRun) {
        const next = `${current.replace(/\s*$/, "")}\n${missing.join("\n")}\n`.replace(/^\n+/, "");
        writeFileSync(file, next, "utf8");
      }
    },
  },
  {
    version: "0.2.14",
    id: "production-scripts",
    description: "Add standard production build and start scripts",
    apply(ctx) {
      // Workspace roots are manifests, not runnable applications.
      if (!existsSync(join(ctx.appRoot, "app", "pages"))) return;
      const file = join(ctx.appRoot, "package.json");
      const pkg = JSON.parse(readFileSync(file, "utf8")) as Record<string, any>;
      const scripts = (pkg.scripts ??= {});
      const additions: Record<string, string> = {};
      if (!scripts.build) additions.build = "wrnexus build .";
      if (!scripts.start) additions.start = "bun dist/server.js";
      if (!scripts.production) additions.production = "bun run build && bun run start";
      if (!Object.keys(additions).length) return;
      Object.assign(scripts, additions);
      ctx.log(`+ package scripts: ${Object.keys(additions).join(", ")}`);
      if (!ctx.dryRun) writeFileSync(file, JSON.stringify(pkg, null, 2) + "\n", "utf8");
    },
  },
  {
    version: "0.2.18",
    id: "add-helpers-package",
    description: "Add @wrnexus/helpers to existing runnable applications",
    apply(ctx) {
      // Workspace roots do not consume application Context helpers directly.
      if (!existsSync(join(ctx.appRoot, "app", "pages"))) return;
      const file = join(ctx.appRoot, "package.json");
      const pkg = JSON.parse(readFileSync(file, "utf8")) as Record<string, any>;
      const dependencies = (pkg.dependencies ??= {} as Record<string, string>);
      if (dependencies["@wrnexus/helpers"]) return;
      dependencies["@wrnexus/helpers"] = `^${ctx.to}`;
      ctx.log(`+ @wrnexus/helpers ^${ctx.to}`);
      if (!ctx.dryRun) writeFileSync(file, JSON.stringify(pkg, null, 2) + "\n", "utf8");
    },
  },
  {
    version: "0.2.19",
    id: "vscode-formatting",
    description: "Add safe VS Code format-on-save defaults and extension recommendations",
    apply(ctx) {
      const vscode = join(ctx.appRoot, ".vscode");
      const settings = join(vscode, "settings.json");
      const extensions = join(vscode, "extensions.json");
      const prettierIgnore = join(ctx.appRoot, ".prettierignore");
      const gitignore = join(ctx.appRoot, ".gitignore");

      const files: Array<[string, string, string]> = [
        [
          settings,
          JSON.stringify(
            {
              "editor.defaultFormatter": "esbenp.prettier-vscode",
              "editor.formatOnSave": true,
              "editor.codeActionsOnSave": { "source.fixAll.eslint": "explicit" },
              "prettier.requireConfig": true,
              "[wrn]": {
                "editor.defaultFormatter": "wrnexus.wrnexus",
                "editor.formatOnSave": true,
              },
            },
            null,
            2,
          ) + "\n",
          ".vscode/settings.json",
        ],
        [extensions, VSCODE_EXTENSIONS, ".vscode/extensions.json"],
      ];

      for (const [file, contents, label] of files) {
        if (existsSync(file)) continue;
        ctx.log(`+ ${label}`);
        if (!ctx.dryRun) {
          mkdirSync(vscode, { recursive: true });
          writeFileSync(file, contents, "utf8");
        }
      }

      const prettier = existsSync(prettierIgnore) ? readFileSync(prettierIgnore, "utf8") : "";
      if (!new Set(prettier.split(/\r?\n/).map((line) => line.trim())).has("CLAUDE.md")) {
        ctx.log("+ .prettierignore: CLAUDE.md");
        if (!ctx.dryRun) {
          writeFileSync(prettierIgnore, `${prettier.replace(/\s*$/, "")}\nCLAUDE.md\n`, "utf8");
        }
      }

      if (existsSync(gitignore)) {
        const current = readFileSync(gitignore, "utf8");
        const lines = current.split(/\r?\n/);
        const vscodeIgnore = lines.findIndex((line) => line.trim() === ".vscode/");
        const needed = [".vscode/*", "!.vscode/settings.json", "!.vscode/extensions.json"];
        if (vscodeIgnore >= 0 || needed.some((entry) => !lines.includes(entry))) {
          ctx.log("~ .gitignore: track shared VS Code configuration");
          if (!ctx.dryRun) {
            const filtered = lines.filter((line) => line.trim() !== ".vscode/");
            const have = new Set(filtered.map((line) => line.trim()));
            for (const entry of needed) if (!have.has(entry)) filtered.push(entry);
            writeFileSync(gitignore, filtered.join("\n").replace(/\n*$/, "\n"), "utf8");
          }
        }
      }
    },
  },
  {
    version: "0.2.20",
    id: "normalize-vscode-extensions",
    description: "Normalize the framework-generated VS Code recommendations file",
    apply(ctx) {
      const file = join(ctx.appRoot, ".vscode", "extensions.json");
      if (!existsSync(file) || readFileSync(file, "utf8") !== LEGACY_VSCODE_EXTENSIONS) return;
      ctx.log("~ .vscode/extensions.json formatting normalized");
      if (!ctx.dryRun) writeFileSync(file, VSCODE_EXTENSIONS, "utf8");
    },
  },
  {
    version: "0.2.21",
    id: "in-process-hmr-runtime",
    description: "No project-file changes; the in-process HMR behavior is runtime-only",
    apply() {
      // Explicit no-op: updating @wrnexus/dev-server is sufficient.
    },
  },
  {
    version: "0.2.22",
    id: "nested-forward-auth-origin",
    description: "No project-file changes; nested forward-auth headers are fixed at runtime",
    apply() {
      // Explicit no-op: updating @wrnexus/dev-server and helpers is sufficient.
    },
  },
  {
    version: "0.2.23",
    id: "custom-required-validation-message",
    description: "No project-file changes; custom required messages are provided at runtime",
    apply() {
      // Explicit no-op: updating @wrnexus/validation is sufficient.
    },
  },
  {
    version: "0.2.24",
    id: "package-usage-documentation",
    description: "No project-file changes; package usage documentation is expanded",
    apply() {
      // Explicit no-op: this release updates package and portal documentation only.
    },
  },
  {
    version: "0.2.25",
    id: "reactive-state-attributes",
    description: "No project-file changes; state expressions in attributes are reactive at runtime",
    apply() {
      // Explicit no-op: updating @wrnexus/compiler and @wrnexus/csr is sufficient.
    },
  },
  {
    version: "0.2.26",
    id: "class-improvements-attributes",
    description:
      "Adds conditional class directives, route state parameters, formatter improvements, and HMR fixes.",
    apply() {
      // No project file migration is required for this framework release.
    },
  },
  {
    version: "0.2.27",
    id: "component-improvements-tag",
    description: "Adds Component renders system allowed 2 ways.",
    apply() {
      // No project file migration is required for this framework release.
    },
  },
  {
    version: "0.2.28",
    id: "layout-component-improvements",
    description: "Added new block layout for component and improved component rendering.",
    apply() {
      // No project file migration is required for this framework release.
    },
  },
  {
    version: "0.2.29",
    id: "workspace-config-improvements",
    description:
      "Adds environment-aware workspace domains, app URL helpers, component tag syntax, layouts, and editor improvements.",
    apply() {
      // No generated project files require automatic changes.
      // Existing applications remain compatible with this release.
    },
  },
  {
    version: "0.2.30",
    id: "workspace-config-auth-improvements",
    description:
      "Adds dynamic workspace origins, workspace forward-auth resolution, layout support, component tag syntax, and editor improvements.",
    apply() {
      // No generated application files require automatic migration.
    },
  },
  {
    version: "0.2.31",
    id: "lifecycle-improvements",
    description:
      "Adds component functions, lifecycle hooks, state watchers, optimized behavior cleanup, and CSR navigation lifecycle support.",
    apply() {
      // No generated application files require automatic migration.
    },
  },
  {
    version: "0.2.32",
    id: "fix-runtime-bug-0-2-32",
    description:
      "Fixes safe encoding and browser parsing of multiline component behavior metadata.",
    apply() {
      // No generated application files require automatic migration.
    },
  },
  {
    version: "0.2.33",
    id: "fix-runtime-bug-0-2-33",
    description:
      "Fixes safe encoding and browser parsing of multiline component behavior metadata.",
    apply() {
      // No generated application files require automatic migration.
    },
  },
  {
    version: "0.2.34",
    id: "reactive-bug",
    description:
      "Fixes component behavior encoding, lifecycle and watcher metadata parsing, CSR cleanup, and reactive form control property updates.",
    apply() {
      // No generated application files require automatic migration.
    },
  },
  {
    version: "0.2.35",
    id: "class-bug",
    description:
      "Fixes component behavior encoding, lifecycle and watcher metadata parsing, CSR cleanup, and reactive form control property updates.",
    apply() {
      // No generated application files require automatic migration.
    },
  },
  {
    version: "0.2.36",
    id: "attribute-bug",
    description:
      "Adds support for Tailwind arbitrary-value class directives and improves reactive class bindings.",
    apply() {
      // No generated application files require automatic migration.
    },
  },
  {
    version: "0.2.37",
    id: "loop-condition-fix",
    description:
      "Adds server-rendered component each/if blocks and fixes dynamic component rendering.",
    apply() {
      // No generated application files require automatic migration.
    },
  },
  {
    version: "0.2.38",
    id: "loop-condition-bug",
    description:
      "Adds server-rendered component each/if blocks and fixes dynamic component rendering.",
    apply() {
      // No generated application files require automatic migration.
    },
  },
  {
    version: "0.2.39",
    id: "server-function-bug",
    description:
      "Adds server-rendered component each/if blocks and fixes dynamic component rendering.",
    apply() {
      // No generated application files require automatic migration.
    },
  },
  {
    version: "0.2.40",
    id: "conditions-bug-0-2-40",
    description:
      "Adds server-rendered component each/if blocks and fixes dynamic component rendering.",
    apply() {
      // No generated application files require automatic migration.
    },
  },
  {
    version: "0.2.41",
    id: "conditions-bug-0-2-41",
    description:
      "Adds server-rendered component each/if blocks and fixes dynamic component rendering.",
    apply() {
      // No generated application files require automatic migration.
    },
  },
  {
    version: "0.2.42",
    id: "conditions-bug-0-2-42",
    description:
      "Adds server-rendered component each/if blocks and fixes dynamic component rendering.",
    apply() {
      // No generated application files require automatic migration.
    },
  },
  {
    version: "0.2.43",
    id: "conditions-bug-0-2-43",
    description:
      "Adds server-rendered component each/if blocks and fixes dynamic component rendering.",
    apply() {
      // No generated application files require automatic migration.
    },
  },
  {
    version: "0.2.44",
    id: "conditions-bug-0-2-44",
    description:
      "Adds server-rendered component each/if blocks and fixes dynamic component rendering.",
    apply() {
      // No generated application files require automatic migration.
    },
  },
  {
    version: "0.2.45",
    id: "conditions-bug-0-2-45",
    description:
      "Adds server-rendered component each/if blocks and fixes dynamic component rendering.",
    apply() {
      // No generated application files require automatic migration.
    },
  },
  {
    version: "0.2.46",
    id: "conditions-bug-0-2-46",
    description:
      "Adds server-rendered component each/if blocks and fixes dynamic component rendering.",
    apply() {
      // No generated application files require automatic migration.
    },
  },
  {
    version: "0.2.47",
    id: "conditions-bug-0-2-47",
    description:
      "Adds server-rendered component each/if blocks and fixes dynamic component rendering.",
    apply() {
      // No generated application files require automatic migration.
    },
  },
  {
    version: "0.2.48",
    id: "components-update-0-2-48",
    description: "Adds reactive hydration support for server-rendered each loop locals.",
    apply() {
      // No generated application files require automatic migration.
    },
  },
  {
    version: "0.2.49",
    id: "components-update-0-2-49",
    description: "Adds reactive hydration support for server-rendered each loop locals.",
    apply() {
      // No generated application files require automatic migration.
    },
  },
  {
    version: "0.2.50",
    id: "components-update-0-2-50",
    description: "Adds reactive hydration support for server-rendered each loop locals.",
    apply() {
      // No generated application files require automatic migration.
    },
  },
  {
    version: "0.2.51",
    id: "ui-package-repair",
    description: "Repairs bundled UI component syntax and app-local stylesheet processing.",
    apply() {
      // No generated application files require automatic migration.
    },
  },
  {
    version: "0.2.52",
    id: "shared-marketing-components",
    description: "Adds reusable FAQ, announcement, and back-to-top UI components.",
    apply() {
      // No generated application files require automatic migration.
    },
  },
  {
    version: "0.2.53",
    id: "faq-accordion-tailwind-safelist",
    description: "Ensures conditional FAQ accordion layout utilities are generated by Tailwind.",
    apply() {
      // No generated application files require automatic migration.
    },
  },
  {
    version: "0.2.54",
    id: "essential-ui-component-system",
    description: "Adds the PDF-defined minimum and essential shared UI component system.",
    apply() {
      // No generated application files require automatic migration.
    },
  },
  {
    version: "0.2.55",
    id: "expanded-ui-foundation-catalog",
    description:
      "Adds the expanded typography, layout, action, surface, card, and indicator catalog.",
    apply() {
      // No generated application files require automatic migration.
    },
  },
  {
    version: "0.2.56",
    id: "ui-motion-system",
    description:
      "Adds consistent, accessible motion and modern interactions across the shared UI catalog.",
    apply() {
      // No generated application files require automatic migration.
    },
  },
  {
    version: "0.2.57",
    id: "semantic-theme-palettes",
    description:
      "Adds eight semantic UI palettes and validated complete custom palette configuration.",
    apply() {
      // Existing theme configuration remains compatible; blue is the default palette.
    },
  },
  {
    version: "0.2.58",
    id: "complete-ui-component-catalog",
    description:
      "Completes the PDF-aligned UI catalog and ships generated props, slots, and events metadata.",
    apply() {
      // UI components remain auto-discovered; no application files require migration.
    },
  },
  {
    version: "0.2.59",
    id: "typed-wrn-declarations",
    description:
      "Adds explicit prop and state types, local type declarations, and typed component runtime validation.",
    apply() {
      // Existing inferred declarations remain compatible; typed syntax is opt-in.
    },
  },
  {
    version: "0.2.60",
    id: "page-owned-shell-backgrounds",
    description: "Lets pages own their backgrounds by keeping shared page shells transparent.",
    apply() {
      // The UI stylesheet update applies automatically.
    },
  },
  {
    version: "0.2.61",
    id: "spacing-neutral-component-boundaries",
    description:
      "Removes default outer component spacing and standardizes root class customization.",
    apply() {
      // The UI component and stylesheet updates apply automatically.
    },
  },
  {
    version: "0.2.62",
    id: "application-theme-contract-and-public-ui",
    description:
      "Exposes semantic theme helpers to application UI and promotes the public-site components into @wrnexus/ui.",
    apply() {
      // Package auto-discovery and the root-level theme aliases apply automatically.
    },
  },
  {
    version: "0.2.63",
    id: "polished-linked-catalog-cards",
    description:
      "Fixes visually hidden full-card links and improves channel, integration, and campaign card hierarchy and interaction.",
    apply() {
      // The shared UI stylesheet update applies automatically.
    },
  },
  {
    version: "0.2.64",
    id: "production-workspace-orchestrator",
    description:
      "Adds a first-class production command that builds, migrates, and serves every registered workspace app.",
    apply() {
      // Existing workspaces can invoke `wrnexus production` without generated-file changes.
    },
  },
  {
    version: "0.2.65",
    id: "private-production-app-listeners",
    description:
      "Restricts workspace-managed production app servers to loopback so external clients must use the gateway.",
    apply() {
      // The production gateway applies the private bind address automatically.
    },
  },
  {
    version: "0.2.66",
    id: "named-workspace-environments-and-reusable-public-chrome",
    description:
      "Adds top-level named workspace environment commands and data-driven public announcement, header, and footer components.",
    apply() {
      // Existing workspaces gain the CLI command aliases and optional UI props automatically.
    },
  },
  {
    version: "0.2.67",
    id: "configurable-workspace-environment-runtime",
    description:
      "Adds per-environment runtime, HMR, build, migration, profile, hostname, and port controls to workspace commands.",
    apply() {
      // Existing workspace configurations remain compatible; new controls are opt-in.
    },
  },
  {
    version: "0.2.68",
    id: "production-request-error-logging",
    description:
      "Logs production request exceptions, forward-auth verifier failures, proxy failures, and unexpected app exits with actionable context.",
    apply() {
      // Runtime diagnostics improve automatically after updating and redeploying.
    },
  },
  {
    version: "0.2.69",
    id: "centralized-production-error-diagnostics",
    description:
      "Relays bounded internal app error diagnostics to gateway logs and strips them from public responses.",
    apply() {
      // Runtime diagnostics improve automatically after updating and rebuilding.
    },
  },
  {
    version: "0.2.70",
    id: "direct-production-runtime-dependency",
    description:
      "Declares @wrnexus/dev-server directly in runnable apps so production bundles resolve the matching runtime release.",
    apply(ctx) {
      if (!existsSync(join(ctx.appRoot, "app", "pages"))) return;
      const file = join(ctx.appRoot, "package.json");
      const pkg = JSON.parse(readFileSync(file, "utf8")) as Record<string, any>;
      const dependencies = (pkg.dependencies ??= {} as Record<string, string>);
      if (dependencies["@wrnexus/dev-server"] === `^${ctx.to}`) return;
      dependencies["@wrnexus/dev-server"] = `^${ctx.to}`;
      ctx.log(`+ @wrnexus/dev-server ^${ctx.to}`);
      if (!ctx.dryRun) writeFileSync(file, JSON.stringify(pkg, null, 2) + "\n", "utf8");
    },
  },
  {
    version: "0.2.71",
    id: "public-forward-auth-redirect-origins",
    description:
      "Builds SSO redirects and returnTo URLs from configured public app origins instead of internal gateway listeners.",
    apply() {
      // Public URL handling improves automatically after updating and rebuilding.
    },
  },
  {
    version: "0.2.72",
    id: "fresh-linked-ui-compilation",
    description:
      "Refreshes the shared UI catalog and makes WRN compilation caches follow source content across package updates and linked dependencies.",
    apply() {
      // Component and cache behavior improves automatically after updating.
    },
  },
  {
    version: "0.2.73",
    id: "polished-auth-component-visuals",
    description:
      "Updates shared authentication actions and notices with reliable built-in icons, semantic color treatments, and polished sizing.",
    apply() {
      // Shared UI visuals improve automatically after updating and rebuilding.
    },
  },
  {
    version: "0.2.74",
    id: "wrn-imports-and-dynamic-public-shell",
    description:
      "Adds top-level WRN imports and data-driven public header and footer navigation, actions, links, and labels.",
    apply() {
      // Language and shared UI behavior improve automatically after updating.
    },
  },
  {
    version: "0.2.75",
    id: "dev-toolbar",
    description: "Dev toolbar for optimization and developer.",
    apply() {
      // Language and shared UI behavior improve automatically after updating.
    },
  },
  {
    version: "0.2.76",
    id: "dev-toolbar-fix",
    description: "Dev toolbar for optimization and developer.",
    apply() {
      // Language and shared UI behavior improve automatically after updating.
    },
  },
  {
    version: "0.2.77",
    id: "ui-components-fix",
    description: "Dev toolbar for optimization and developer.",
    apply() {
      // Language and shared UI behavior improve automatically after updating.
    },
  },
  {
    version: "0.2.78",
    id: "ui-components-fix-2",
    description: "Dev toolbar for optimization and developer.",
    apply() {
      // Language and shared UI behavior improve automatically after updating.
    },
  },
  {
    version: "0.2.79",
    id: "ui-components-fix-3",
    description: "Dev toolbar for optimization and developer.",
    apply() {
      // Language and shared UI behavior improve automatically after updating.
    },
  },
  {
    version: "0.3.0",
    id: "language-foundation-and-compatible-platform-upgrade",
    description:
      "Adds the shared syntax/AST package, stable diagnostics, plugins, partial hydration, advanced routing, build analysis, tracing, typed data APIs, and safe WRN source normalization.",
    apply(ctx) {
      const runnable = existsSync(join(ctx.appRoot, "app", "pages"));
      const packageFile = join(ctx.appRoot, "package.json");
      const pkg = JSON.parse(readFileSync(packageFile, "utf8")) as Record<string, any>;
      const scripts = (pkg.scripts ??= {});
      const dependencies = (pkg.dependencies ??= {});
      const addedScripts: string[] = [];
      const addedDependencies: string[] = [];
      if (runnable) {
        for (const [name, command] of Object.entries({
          doctor: "wrnexus doctor .",
          "config:explain": "wrnexus config . --explain",
          analyze: "wrnexus analyze .",
          "update:preview": "wrnexus update . --dry-run",
        })) {
          if (!scripts[name]) {
            scripts[name] = command;
            addedScripts.push(name);
          }
        }
        for (const name of ["@wrnexus/syntax", "@wrnexus/plugin"]) {
          if (!dependencies[name]) {
            dependencies[name] = `^${ctx.to}`;
            addedDependencies.push(name);
          }
        }
      }
      if (addedScripts.length) ctx.log(`+ package scripts: ${addedScripts.join(", ")}`);
      if (addedDependencies.length) ctx.log(`+ dependencies: ${addedDependencies.join(", ")}`);
      if (!ctx.dryRun && (addedScripts.length || addedDependencies.length)) {
        writeFileSync(packageFile, JSON.stringify(pkg, null, 2) + "\n", "utf8");
      }

      const changedFiles: string[] = [];
      for (const file of walkProjectFiles(join(ctx.appRoot, "app"), ".wrn")) {
        const before = readFileSync(file, "utf8");
        const after = migrateWrnSource(before);
        if (after === before) continue;
        changedFiles.push(file.slice(ctx.appRoot.length + 1).replace(/\\/g, "/"));
        ctx.log(`~ normalized ${changedFiles[changedFiles.length - 1]}`);
        if (!ctx.dryRun) writeFileSync(file, after, "utf8");
      }

      const reportFile = join(ctx.appRoot, ".wrnexus", "migrations", "0.3.0.json");
      ctx.log(`+ .wrnexus/migrations/0.3.0.json (${changedFiles.length} source files normalized)`);
      if (!ctx.dryRun) {
        mkdirSync(dirname(reportFile), { recursive: true });
        writeFileSync(
          reportFile,
          JSON.stringify(
            {
              version: "0.3.0",
              from: ctx.from,
              appliedAt: new Date().toISOString(),
              changedFiles,
              compatibility: {
                legacyDataFor: true,
                legacyEachBlocks: true,
                legacyComponentMounts: true,
                quotedDynamicAttributes: true,
              },
            },
            null,
            2,
          ) + "\n",
          "utf8",
        );
      }
    },
  },
  {
    version: "0.3.1",
    id: "pending-update-marker-verification-fix",
    description:
      "Allows warning-level doctor checks during pre-commit update verification so successfully verified migrations can commit their new version marker.",
    apply() {
      // CLI-only transaction fix. No generated application files require migration.
    },
  },
  {
    version: "0.3.2",
    id: "ssr-page-state-control-scope",
    description: "Makes page state variables available to server-rendered if and each expressions.",
    apply() {
      // Compiler-only fix. Existing WRN source files require no migration.
    },
  },
  {
    version: "0.3.3",
    id: "component-fix-0-3-3",
    description: "UI component fixes.",
    apply() {
      // Compiler-only fix. Existing WRN source files require no migration.
    },
  },
  {
    version: "0.3.4",
    id: "component-fix-0-3-4",
    description: "UI component fixes.",
    apply() {
      // Compiler-only fix. Existing WRN source files require no migration.
    },
  },
  {
    version: "0.3.5",
    id: "new-component-added-0-3-5",
    description: "UI component Added.",
    apply() {
      // Compiler-only fix. Existing WRN source files require no migration.
    },
  },
  {
    version: "0.3.6",
    id: "new-component-added-0-3-6",
    description: "UI component Added.",
    apply() {
      // Compiler-only fix. Existing WRN source files require no migration.
    },
  },
  {
    version: "0.4.0",
    id: "package-runtime-and-asset-platform",
    description:
      "Enables automatic package discovery, package components/routes/migrations, and page-scoped client runtime injection without manually copied JavaScript assets.",
    apply(ctx) {
      const packageFile = join(ctx.appRoot, "package.json");
      const pkg = JSON.parse(readFileSync(packageFile, "utf8")) as Record<string, any>;
      const scripts = (pkg.scripts ??= {});
      const additions: Record<string, string> = {
        "inspect:plugins": "wrnexus inspect plugins .",
        "inspect:runtimes": "wrnexus inspect runtimes .",
        "inspect:assets": "wrnexus inspect assets .",
        "inspect:routes": "wrnexus inspect routes .",
      };
      const addedScripts: string[] = [];
      for (const [name, command] of Object.entries(additions)) {
        if (!scripts[name]) {
          scripts[name] = command;
          addedScripts.push(name);
        }
      }
      if (addedScripts.length) ctx.log(`+ package scripts: ${addedScripts.join(", ")}`);
      if (!ctx.dryRun && addedScripts.length) {
        writeFileSync(packageFile, JSON.stringify(pkg, null, 2) + "\n", "utf8");
      }

      const changedFiles: string[] = [];
      const legacyCaptchaScript =
        /\s*<script\b[^>]*\bsrc\s*=\s*["']\/(?:__wrnexus\/captcha|assets\/wrnexus\/captcha)\.js(?:\?[^"']*)?["'][^>]*>(?:\s*<\/script>)?\s*/gi;
      for (const file of walkProjectFiles(join(ctx.appRoot, "app"), ".wrn")) {
        const before = readFileSync(file, "utf8");
        const after = before.replace(legacyCaptchaScript, "\n");
        if (after === before) continue;
        const relativeFile = file.slice(ctx.appRoot.length + 1).replace(/\\/g, "/");
        changedFiles.push(relativeFile);
        ctx.log(`~ removed legacy CAPTCHA script tag from ${relativeFile}`);
        if (!ctx.dryRun) writeFileSync(file, after, "utf8");
      }

      const movedAssets: string[] = [];
      for (const relativeFile of [
        "public/assets/wrnexus/captcha.js",
        "public/__wrnexus/captcha.js",
      ]) {
        const source = join(ctx.appRoot, relativeFile);
        if (!existsSync(source)) continue;
        const destination = join(ctx.appRoot, ".wrnexus", "legacy-assets", "0.4.0", relativeFile);
        movedAssets.push(relativeFile);
        ctx.log(`~ archived legacy ${relativeFile}`);
        if (!ctx.dryRun) {
          mkdirSync(dirname(destination), { recursive: true });
          cpSync(source, destination);
          rmSync(source, { force: true });
        }
      }

      const reportFile = join(ctx.appRoot, ".wrnexus", "migrations", "0.4.0.json");
      ctx.log(
        `+ .wrnexus/migrations/0.4.0.json (${changedFiles.length} source files, ${movedAssets.length} legacy assets)`,
      );
      if (!ctx.dryRun) {
        mkdirSync(dirname(reportFile), { recursive: true });
        writeFileSync(
          reportFile,
          JSON.stringify(
            {
              version: "0.4.0",
              from: ctx.from,
              appliedAt: new Date().toISOString(),
              changedFiles,
              movedAssets,
              packageRuntimeDiscovery: true,
              manualCaptchaRuntimeRequired: false,
            },
            null,
            2,
          ) + "\n",
          "utf8",
        );
      }
    },
  },
  {
    version: "0.5.0",
    id: "custom-ui-components-and-auth-platform",
    description:
      "Adds the authentication package, custom form controls, expanded UI component events, and reliable development hot reloading.",
    apply() {
      // Package, component, and development-runtime improvements apply automatically.
    },
  },
  {
    version: "0.5.1",
    id: "native-wrn-json-expressions",
    description:
      "Adds native array and object state initializers plus JSX-style expression props in WRN component markup.",
    apply() {
      // Syntax and compiler support are package-managed; existing applications remain compatible.
    },
  },
  {
    version: "0.5.2",
    id: "multiline-wrn-json-and-stable-gateway-runtime",
    description:
      "Supports formatted multiline array and object state initializers and keeps gateway children on the matching framework runtime.",
    apply() {
      // Syntax, compiler, and gateway fixes are package-managed; no application rewrite is required.
    },
  },
  {
    version: "0.5.3",
    id: "public-workspace-origins-and-schema-backed-auth-forms",
    description:
      "Keeps cross-app redirects on configured public origins and enables schema-backed auth forms with active navigation states.",
    apply() {
      // Gateway, helper, validation, and UI improvements are package-managed.
    },
  },
  {
    version: "0.5.10",
    id: "latest-release-after-upgrade",
    description: "Latest New Packages and release ready packages",
    apply() {
      // Gateway, helper, validation, and UI improvements are package-managed.
    },
  },
  {
    version: "0.5.11",
    id: "latest-release-after-upgrade",
    description: "Latest New Packages and release ready packages",
    apply() {
      // Gateway, helper, validation, and UI improvements are package-managed.
    },
  },
  {
    version: "0.5.11",
    id: "latest-release-after-upgrade",
    description: "Latest New Packages and release ready packages",
    apply() {
      // Gateway, helper, validation, and UI improvements are package-managed.
    },
  },
  {
    version: "0.5.12",
    id: "latest-release-after-upgrade",
    description: "Latest New Packages and release ready packages",
    apply() {
      // Gateway, helper, validation, and UI improvements are package-managed.
    },
  },
  {
    version: "0.5.13",
    id: "latest-release-after-upgrade",
    description: "Latest New Packages and release ready packages",
    apply() {
      // Gateway, helper, validation, and UI improvements are package-managed.
    },
  },
  {
    version: "0.5.13",
    id: "preserve-theme-accent-during-hmr",
    description: "Preserves the user's active theme and accent palette during HMR morph updates.",
    apply() {
      // Runtime-only framework fix. No project files require migration.
    },
  },
  {
    version: "0.5.14",
    id: "preserve-theme-accent-during-hmr",
    description: "Preserves the user's active theme and accent palette during HMR morph updates.",
    apply() {
      // Runtime-only framework fix. No project files require migration.
    },
  },
  {
    version: "0.5.15",
    id: "wrn-style-block-head-pipeline",
    description:
      "Promotes page, layout, and component style blocks into the document head after global CSS with CSP, HMR, and CSR navigation support.",
    apply() {
      // Compiler, SSR, dev-server, and CSR runtime changes apply automatically.
    },
  },
  {
    version: "0.6.0",
    id: "0.6.0-01-parser-and-language",
    description:
      "Enables the v0.6 parser, AST, typed state, imports, stores, outputs, and runtime function grammar.",
    apply() {},
  },
  {
    version: "0.6.0",
    id: "0.6.0-02-function-runtime",
    description:
      "Classifies unambiguous legacy functions as client, server, or shared while preserving ambiguous behavior.",
    apply(ctx) {
      for (const file of walkProjectFiles(join(ctx.appRoot, "app"), ".wrn")) {
        const before = readFileSync(file, "utf8");
        const relative = file.slice(ctx.appRoot.length + 1).replace(/\\/g, "/");
        const after = classifyLegacyFunctions(before, ctx.report, relative);
        if (after === before) continue;
        ctx.log(`~ ${relative}: runtime function modifiers`);
        ctx.report.changedAutomatically.push(relative);
        if (!ctx.dryRun) writeFileSync(file, after, "utf8");
      }
    },
  },
  {
    version: "0.6.0",
    id: "0.6.0-03-typed-outputs",
    description:
      "Converts legacy @event declarations to typed outputs with safe unknown payload fallbacks.",
    apply(ctx) {
      for (const file of walkProjectFiles(join(ctx.appRoot, "app"), ".wrn")) {
        const before = readFileSync(file, "utf8");
        const relative = file.slice(ctx.appRoot.length + 1).replace(/\\/g, "/");
        const legacy = collectLegacyEvents(before);
        const after = ensureOutputsBlock(legacy.source, legacy.outputs, ctx.report, relative);
        if (after === before) continue;
        ctx.log(`~ ${relative}: typed outputs`);
        ctx.report.changedAutomatically.push(relative);
        if (!ctx.dryRun) writeFileSync(file, after, "utf8");
      }
    },
  },
  {
    version: "0.6.0",
    id: "0.6.0-04-remove-emit",
    description:
      "Migrates static $emit calls and event.detail consumers to output.name(payload) and payload.",
    apply(ctx) {
      for (const file of walkProjectFiles(join(ctx.appRoot, "app"), ".wrn")) {
        const before = readFileSync(file, "utf8");
        const relative = file.slice(ctx.appRoot.length + 1).replace(/\\/g, "/");
        const after = convertLegacyEmit(before, ctx.report, relative).replace(
          /\bevent\.detail\b/g,
          "payload",
        );
        if (after === before) continue;
        ctx.log(`~ ${relative}: callable outputs`);
        ctx.report.changedAutomatically.push(relative);
        if (!ctx.dryRun) writeFileSync(file, after, "utf8");
      }
    },
  },
  {
    version: "0.6.0",
    id: "0.6.0-05-explicit-imports",
    description:
      "Enables compatible explicit imports and records unresolved symbols without guessing.",
    apply(ctx) {
      if (!ctx.explicitImports) return;
      const index = buildV060SymbolIndex(ctx.appRoot);
      for (const path of walkProjectFiles(join(ctx.appRoot, "app"), ".wrn")) {
        const relativeFile = path.slice(ctx.appRoot.length + 1).replace(/\\/g, "/");
        const before = readFileSync(path, "utf8");
        const after = addExplicitImportsToSource(
          before,
          relativeFile,
          ctx.appRoot,
          index,
          ctx.report,
        );
        if (after === before) continue;
        ctx.log(`~ ${relativeFile}: explicit imports`);
        if (!ctx.report.changedAutomatically.includes(relativeFile))
          ctx.report.changedAutomatically.push(relativeFile);
        if (!ctx.dryRun) writeFileSync(path, after, "utf8");
      }
    },
  },
  {
    version: "0.6.0",
    id: "0.6.0-06-layout-imports",
    description:
      "Converts safely resolvable string layouts to imported symbols while retaining compatibility.",
    apply(ctx) {
      if (!ctx.explicitImports) return;
      const index = buildV060SymbolIndex(ctx.appRoot);
      for (const path of walkProjectFiles(join(ctx.appRoot, "app"), ".wrn")) {
        const relativeFile = path.slice(ctx.appRoot.length + 1).replace(/\\/g, "/");
        const before = readFileSync(path, "utf8");
        const after = migrateImportedLayout(before, relativeFile, ctx.appRoot, index, ctx.report);
        if (after === before) continue;
        ctx.log(`~ ${relativeFile}: imported layout`);
        if (!ctx.report.changedAutomatically.includes(relativeFile))
          ctx.report.changedAutomatically.push(relativeFile);
        if (!ctx.dryRun) writeFileSync(path, after, "utf8");
      }
    },
  },
  {
    version: "0.6.0",
    id: "0.6.0-07-component-contracts",
    description: "Enables generated component, output, function, and declaration contracts.",
    apply() {},
  },
  {
    version: "0.6.0",
    id: "0.6.0-08-store-support",
    description: "Adds @wrnexus/store and @wrnexus/typecheck to runnable applications.",
    apply(ctx) {
      if (!existsSync(join(ctx.appRoot, "app", "pages"))) return;
      const file = join(ctx.appRoot, "package.json");
      const pkg = JSON.parse(readFileSync(file, "utf8")) as Record<string, any>;
      const dependencies = (pkg.dependencies ??= {});
      const added: string[] = [];
      for (const name of ["@wrnexus/store", "@wrnexus/typecheck"]) {
        if (dependencies[name] === `^${ctx.to}`) continue;
        dependencies[name] = `^${ctx.to}`;
        added.push(name);
      }
      if (added.length) ctx.log(`+ dependencies: ${added.join(", ")}`);
      if (!ctx.dryRun && added.length)
        writeFileSync(file, JSON.stringify(pkg, null, 2) + "\n", "utf8");
    },
  },
  {
    version: "0.6.0",
    id: "0.6.0-09-type-config",
    description:
      "Adds compatible imports, type checking, stores, and legacy behavior configuration.",
    apply(ctx) {
      // Workspace manifests are updated alongside each configured application,
      // but they are not themselves runnable apps. Keep app-owned declarations
      // under the individual app roots instead of creating `<workspace>/app`.
      if (!existsSync(join(ctx.appRoot, "app", "pages"))) return;
      updateV060Config(ctx);
      const types = join(ctx.appRoot, "app", "types");
      if (!existsSync(types)) {
        ctx.log("+ app/types/global.d.ts");
        if (!ctx.dryRun) {
          mkdirSync(types, { recursive: true });
          writeFileSync(
            join(types, "global.d.ts"),
            "// Application-wide ambient WRNexusJS types.\nexport {};\n",
            "utf8",
          );
        }
      }
    },
  },
  {
    version: "0.6.0",
    id: "0.6.0-10-showcase-and-ui",
    description: "Finalizes v0.6 source normalization and writes the detailed migration report.",
    apply(ctx) {
      for (const file of walkProjectFiles(join(ctx.appRoot, "app"), ".wrn")) {
        const before = readFileSync(file, "utf8");
        const relative = file.slice(ctx.appRoot.length + 1).replace(/\\/g, "/");
        const after = migrateWrnSource(before);
        if (after === before) continue;
        ctx.log(`~ ${relative}: final v0.6 normalization`);
        if (!ctx.report.changedAutomatically.includes(relative))
          ctx.report.changedAutomatically.push(relative);
        if (!ctx.dryRun) writeFileSync(file, after, "utf8");
      }
      writeMigrationReport(ctx);
    },
  },
  {
    version: "0.7.0",
    id: "0.7.0-01-security-performance-foundation",
    description:
      "Adds secure-by-default request limits, caching, image optimization, observability, benchmarks, and production hardening packages.",
    apply(ctx) {
      const file = join(ctx.appRoot, "package.json");
      if (existsSync(file)) {
        const pkg = JSON.parse(readFileSync(file, "utf8")) as Record<string, any>;
        const dependencies = (pkg.dependencies ??= {});
        const devDependencies = (pkg.devDependencies ??= {});
        const added: string[] = [];
        for (const name of [
          "@wrnexus/security",
          "@wrnexus/cache",
          "@wrnexus/image",
          "@wrnexus/observability",
        ]) {
          if (dependencies[name] === `^${ctx.to}`) continue;
          dependencies[name] = `^${ctx.to}`;
          added.push(name);
        }
        if (devDependencies["@wrnexus/benchmark"] !== `^${ctx.to}`) {
          devDependencies["@wrnexus/benchmark"] = `^${ctx.to}`;
          added.push("@wrnexus/benchmark (dev)");
        }
        if (added.length) {
          ctx.log(`+ security/performance dependencies: ${added.join(", ")}`);
          if (!ctx.dryRun) writeFileSync(file, JSON.stringify(pkg, null, 2) + "\n", "utf8");
        }
      }
      updateV070Config(ctx);
      const gitignore = join(ctx.appRoot, ".gitignore");
      const currentIgnore = existsSync(gitignore) ? readFileSync(gitignore, "utf8") : "";
      const ignoreEntries = [".env", ".env.*", "!.env.example", "!.env.*.example"];
      const knownIgnore = new Set(currentIgnore.split(/\r?\n/).map((line) => line.trim()));
      const missingIgnore = ignoreEntries.filter((entry) => !knownIgnore.has(entry));
      if (missingIgnore.length) {
        ctx.log(`+ .gitignore secure environment templates: ${missingIgnore.join(", ")}`);
        if (!ctx.dryRun) {
          writeFileSync(
            gitignore,
            `${currentIgnore.replace(/\s*$/, "")}\n${missingIgnore.join("\n")}\n`.replace(
              /^\n+/,
              "",
            ),
            "utf8",
          );
        }
      }
      const review =
        "Review WRNexusJS 0.7 security policy, CSP allowlists, tenant authorization, upload scanners, cache adapters, and production performance budgets.";
      if (!ctx.report.needsReview.includes(review)) ctx.report.needsReview.push(review);
    },
  },
  {
    version: "0.8.0",
    id: "0.8.0-01-package-kits",
    description:
      "Adds package-owned helper kits, reusable UI blocks, standalone realtime rooms, repaired i18n, image helpers, JWT utilities, and encrypted HTTP envelopes.",
    apply(ctx) {
      const file = join(ctx.appRoot, "package.json");
      if (!existsSync(file)) return;
      const pkg = JSON.parse(readFileSync(file, "utf8")) as Record<string, any>;
      const dependencies = (pkg.dependencies ??= {});
      const additions = ["@wrnexus/realtime", "@wrnexus/csr"];
      const added: string[] = [];
      for (const name of additions) {
        if (dependencies[name] === `^${ctx.to}`) continue;
        dependencies[name] = `^${ctx.to}`;
        added.push(name);
      }
      if (added.length) {
        ctx.log(`+ package-kit dependencies: ${added.join(", ")}`);
        if (!ctx.dryRun) writeFileSync(file, JSON.stringify(pkg, null, 2) + "\n", "utf8");
      }
      const review =
        "Review package-owned components and helpers, i18n locale layout, encrypted HTTP trust boundaries, and realtime room authorization before enabling them in production.";
      if (!ctx.report.needsReview.includes(review)) ctx.report.needsReview.push(review);
    },
  },
  {
    version: "0.8.0",
    id: "0.8.0-02-current-wrn-source",
    description:
      "Upgrades every application WRN source to current syntax, adds resolvable imports, normalizes formatting, and records unresolved work.",
    apply(ctx) {
      const appDirectory = join(ctx.appRoot, "app");
      if (!existsSync(appDirectory)) return;
      const index = buildV060SymbolIndex(ctx.appRoot);
      const changedFiles: string[] = [];

      for (const file of walkProjectFiles(appDirectory, ".wrn")) {
        const relativeFile = relative(ctx.appRoot, file).replace(/\\/g, "/");
        const before = readFileSync(file, "utf8");
        let after = migrateV060WrnSource(before, ctx.report, relativeFile);
        after = migrateImportedLayout(after, relativeFile, ctx.appRoot, index, ctx.report);
        after = addExplicitImportsToSource(after, relativeFile, ctx.appRoot, index, ctx.report);
        after = formatCurrentWrnSource(after);
        if (after === before) continue;

        try {
          parse(after);
        } catch (error) {
          const message = error instanceof Error ? error.message.split("\n", 1)[0] : String(error);
          ctx.report.parseFailures.push(`${relativeFile}: ${message}`);
          ctx.report.needsReview.push(
            `${relativeFile}: automatic modernization was skipped because the migrated source did not parse`,
          );
          ctx.log(`! ${relativeFile}: left unchanged because migrated source did not parse`);
          continue;
        }

        changedFiles.push(relativeFile);
        if (!ctx.report.changedAutomatically.includes(relativeFile)) {
          ctx.report.changedAutomatically.push(relativeFile);
        }
        ctx.log(`~ ${relativeFile}: current WRN syntax, imports, and formatting`);
        if (!ctx.dryRun) writeFileSync(file, after, "utf8");
      }

      const reportFile = join(
        ctx.appRoot,
        ".wrnexus",
        "migrations",
        "0.8.0-source-modernization.json",
      );
      ctx.log(
        `+ .wrnexus/migrations/0.8.0-source-modernization.json (${changedFiles.length} WRN files updated)`,
      );
      if (!ctx.dryRun) {
        mkdirSync(dirname(reportFile), { recursive: true });
        writeFileSync(
          reportFile,
          JSON.stringify(
            {
              version: "0.8.0",
              from: ctx.from,
              to: ctx.to,
              appliedAt: new Date().toISOString(),
              changedFiles,
              unresolvedImports: ctx.report.unresolvedImports,
              ambiguousFunctions: ctx.report.ambiguousFunctions,
              legacyOutputPayloads: ctx.report.legacyOutputPayloads,
              parseFailures: ctx.report.parseFailures,
              needsReview: ctx.report.needsReview,
            },
            null,
            2,
          ) + "\n",
          "utf8",
        );
      }
    },
  },
  {
    version: "0.8.1",
    id: "0.8.1-production-performance",
    description:
      "Applies production build, CSS delivery, service-worker lifecycle, asset caching, compression, and server request-path optimizations.",
    apply() {
      // Package-managed production optimizations require no application source rewrite.
    },
  },
  {
    version: "0.8.2",
    id: "0.8.2-navigation-and-vitals-stability",
    description:
      "Stops scroll-driven route prefetch storms, batches browser vitals, and supports telemetry behind trusted reverse proxies.",
    apply() {
      // Runtime stability fixes require no application source rewrite.
    },
  },
  {
    version: "0.8.3",
    id: "0.8.3-client-ssr-realtime-stability",
    description:
      "Fixes browser function bundling, SSR computed values, Async aliases, typed loop props, realtime identity isolation, loader invalidation, and strict package imports.",
    apply() {
      // Framework runtime and compiler fixes require no application source rewrite.
      // The compile-cache generation changes automatically invalidate old artifacts.
    },
  },
  {
    version: "0.8.4",
    id: "0.8.4-editor-language-support-stability",
    description:
      "Fixes WRNexus Language Server crashes, WRN formatting stability, false closing-tag diagnostics, typed event inference, dynamic component prop validation, and VS Code extension reliability.",
    apply() {
      // Compiler, diagnostics, formatter, language-server, and VS Code
      // extension fixes require no application source migration.
      //
      // Existing projects receive the framework package changes through the
      // dependency update. Developers must separately update/reinstall the
      // WRNexus Language Support VS Code extension.
    },
  },
  {
    version: "0.8.5",
    id: "0.8.5-datatable-toaster-overlay-dialogs",
    description:
      "Adds the DataTable and Toaster components, removes the Table scaffold, and gives modal dialogs focus management, a Tab trap and a scroll lock.",
    apply() {
      // Applications using <Table> must move to <DataTable>. The two are not
      // prop-compatible -- Table took `columns`/`rows` and rendered them as
      // plain text, while DataTable owns sorting, filtering, paging and
      // selection -- so this is a source change no codemod can make safely.
      //
      // The `@wrnexus/ui` main entry no longer re-exports the filesystem
      // helpers; import them from `@wrnexus/ui/registry` instead. Everything
      // else arrives through the dependency update.
    },
  },
  {
    version: "0.8.6",
    id: "0.8.6-navigation-and-layout-groups",
    description:
      "Rebuilds the navigation and layout component groups, moves them off Tailwind utilities onto wire-* classes, defines theme tokens that components referenced but nothing declared, and makes component outputs actually reach parent bindings.",
    apply() {
      // Source changes no codemod can make safely, so they are listed rather
      // than attempted.
      //
      // OUTPUTS NOW ARRIVE. Two faults kept declared outputs from reaching a
      // parent @binding, and both are fixed. Expect handlers that never ran
      // before to start running -- this is the intended repair, but it is a
      // behaviour change in code you may have written around.
      //
      //   1. Every camelCase output was undeliverable. HTML lowercases
      //      attribute names, so @sizeChange registered as "sizechange" while
      //      the component emitted "sizeChange" and the lookup missed. That
      //      covered all 17 camelCase outputs, including DataTable.pageChange
      //      and .rowClick, Map.markerClick, ChatBubble.messageClick and
      //      LayoutSplitter.sizeChange. The runtime now matches case
      //      insensitively.
      //
      //   2. Eighteen components dispatched hand-built CustomEvents instead of
      //      calling output.*, which never reaches a binding. Card, Footer,
      //      Breadcrumb, Accordion, alert, Badge, AnnouncementBar, AvatarGroup,
      //      ToggleCount and InputNumber now emit properly.
      //
      // If you worked around the old silence by listening for the raw DOM
      // event on the element, that listener still fires for cases where no
      // binding is registered, but the supported route is the @binding.
      //
      // Marquee, Map, Timeline, List and SearchBox now DECLARE the outputs
      // they were already firing: pause/resume, markerClick/select/zoom,
      // select, select and search/clear respectively.
      //
      // Tabs replaced its raw CustomEvents with declared outputs. Code
      // listening for the old change and select events on the element must
      // move to the @change and @select bindings.
      //
      // Sidebar renamed its classes to the BEM form used everywhere else:
      // wire-sidebar-shell, -items, -group, -toggle, -backdrop, -panel and
      // -layout became wire-sidebar__*. Nesting via children still works.
      //
      // The layout, page and section components moved from Tailwind utility
      // classes to wire-* classes with their own styles. Application CSS
      // selecting on the utility classes they used to render -- max-w-7xl,
      // gap-5, sm:grid-cols-2 and the rest -- no longer matches. Variants are
      // data attributes now, so target [data-variant] and friends instead.
      //
      // LayoutSplitter and CustomScrollbar previously declared outputs they
      // never emitted. LayoutSplitter now resizes and emits sizeChange rather
      // than resizeStart, resize and resizeEnd, and its props are size,
      // minSize, step and orientation rather than columns, gap and maxWidth.
      // CustomScrollbar dropped its scroll output; listen for the plain scroll
      // event on the element. Its props are axis, thickness, maxHeight and
      // radius.
      //
      // ui.css lost several application-pattern class families that nothing
      // referenced: wire-catalog-*, wire-page-shell, wire-product-card,
      // wire-legal-toc, wire-sdk-tabs, wire-cookie-* and wire-analytics-preview.
      // Anything hand-written against those needs its own styles.
      //
      // Themes gain tokens that were referenced but never defined, including
      // --wire-color-focus, --wire-color-surface-soft, --wire-color-on-danger
      // and the input-* family. A custom theme that declared these itself
      // keeps winning; one that did not will see focus rings and soft surfaces
      // start painting where they previously rendered as nothing.
    },
  },
  {
    version: "0.8.7",
    id: "0.8.7-ui-component-depth",
    description:
      "Removes superseded UI scaffolds and moves the remaining utility-styled components to self-contained wire-* BEM styles.",
    apply() {
      // AdvancedDatePicker -> DatePicker; AdvancedRangeSlider -> RangeSlider;
      // FileUpload -> FileInput + FileUploadProgress (or @wrnexus/uploader);
      // Toast and ToastNotifications -> Toaster.
      //
      // List, InputNumber, Marquee, TextLink, Map, SearchBox and Timeline no
      // longer render Tailwind utility class names. Application CSS selecting
      // those internal utilities must move to the component's wire-* BEM
      // classes or its data-size/data-variant attributes. List now emits
      // select for non-link items as well as navigation items.
    },
  },
  {
    version: "0.8.8",
    id: "0.8.8-editor-types-and-update-verification",
    description:
      "Adds typed page context support and regenerates lint-safe application declarations before update verification.",
    apply() {
      // No source rewrite is required. The 0.8.8 CLI regenerates application
      // declarations immediately before verification, repairing stale empty
      // interface contracts produced by earlier releases.
    },
  },
  {
    version: "0.8.9",
    id: "0.8.9-independent-package-updates",
    description:
      "Resolves and updates each installed WRNexus package to its independently published npm version.",
    apply() {
      // Dependency resolution is handled by the update command before install;
      // no application source rewrite is required.
    },
  },
];

/** Release tooling uses this to require an explicit migration entry per version. */
export function updateMigrationVersions(): string[] {
  return [...new Set(MIGRATIONS.map((migration) => migration.version))];
}

/** Bump each `@wrnexus/*` range to its independently resolved target. */
export function bumpDeps(
  pkg: Record<string, unknown>,
  target: string,
  packageVersions?: ReadonlyMap<string, string>,
): string[] {
  const changed: string[] = [];
  for (const field of ["dependencies", "devDependencies", "peerDependencies"]) {
    const deps = pkg[field] as Record<string, string> | undefined;
    if (!deps) continue;
    for (const name of Object.keys(deps)) {
      if (!name.startsWith("@wrnexus/")) continue;
      const resolved = packageVersions ? packageVersions.get(name) : target;
      if (!resolved) continue;
      const next = `^${resolved}`;
      if (deps[name] !== next) {
        changed.push(`${name} ${deps[name]} → ${next}`);
        deps[name] = next;
      }
    }
  }
  return changed;
}

/** The framework version an app was last updated to (or its installed CLI version). */
function appVersion(appRoot: string, pkg: Record<string, unknown>): string {
  const marker = (pkg.wrnexus as { version?: string } | undefined)?.version;
  if (marker) return marker;
  try {
    const p = join(appRoot, "node_modules", "@wrnexus", "cli", "package.json");
    if (existsSync(p)) return JSON.parse(readFileSync(p, "utf8")).version;
  } catch {
    /* ignore */
  }
  return "0.0.0";
}

/** Refresh pure framework-owned reference files. Never touches user-edited CLAUDE.md. */
function refreshFrameworkFiles(
  appRoot: string,
  dryRun: boolean,
  log: (message: string) => void,
): void {
  const publicDir = join(appRoot, "public");
  const llms = join(publicDir, "llms.txt");

  if (!existsSync(llms)) {
    log("+ public/llms.txt created");

    if (!dryRun) {
      mkdirSync(publicDir, {
        recursive: true,
      });

      writeFileSync(llms, AI_GUIDE, "utf8");
    }
  }

  const claude = join(appRoot, "CLAUDE.md");

  if (!existsSync(claude)) {
    log("+ CLAUDE.md created");

    if (!dryRun) {
      writeFileSync(claude, CLAUDE_MD, "utf8");
    }
  }
}

/** Update a single app dir (bump its package.json, refresh files, run migrations). */
interface UpdatedApp {
  root: string;
  packagePath: string;
  pkg: Record<string, unknown>;
}

function backupProjectFiles(appRoot: string, from: string, target: string): string {
  const stamp = new Date().toISOString().replace(/[:.]/g, "-");
  const backup = join(appRoot, ".wrnexus", "update-backups", `${stamp}-${from}-to-${target}`);
  mkdirSync(backup, { recursive: true });
  for (const name of [
    "package.json",
    "wrnexus.config.ts",
    "wrnexus.config.js",
    "wrnexus.config.mjs",
    "tsconfig.json",
    ".gitignore",
    ".prettierignore",
    ".vscode/settings.json",
    ".vscode/extensions.json",
    "CLAUDE.md",
    "public/llms.txt",
    "app",
  ]) {
    const source = join(appRoot, name);
    if (existsSync(source)) {
      const destination = join(backup, name);
      mkdirSync(dirname(destination), { recursive: true });
      cpSync(source, destination, { recursive: statSync(source).isDirectory() });
    }
  }
  return backup;
}

/** Update one app without marking success until install and verification pass. */
export function updateApp(
  appRoot: string,
  target: string,
  dryRun: boolean,
  options: {
    explicitImports?: boolean;
    report?: MigrationReport;
    packageVersions?: ReadonlyMap<string, string>;
  } = {},
): UpdatedApp | null {
  const pkgPath = join(appRoot, "package.json");
  if (!existsSync(pkgPath)) {
    console.log(`  ⚠ ${appRoot}: no package.json — skipped`);
    return null;
  }
  const pkg = JSON.parse(readFileSync(pkgPath, "utf8")) as Record<string, unknown>;
  const from = appVersion(appRoot, pkg);
  const log = (m: string) => console.log(`    ${m}`);
  const report =
    options.report ??
    ({
      changedAutomatically: [],
      needsReview: [],
      unresolvedImports: [],
      ambiguousFunctions: [],
      legacyOutputPayloads: [],
      parseFailures: [],
    } satisfies MigrationReport);
  console.log(`  ▸ ${pkg.name ?? appRoot}  (${from} → ${target})`);

  if (!dryRun) log(`backup: ${backupProjectFiles(appRoot, from, target)}`);

  const changes = bumpDeps(pkg, target, options.packageVersions);
  changes.forEach((c) => log(c));
  if (!changes.length) log("dependencies already current");

  refreshFrameworkFiles(appRoot, dryRun, log);

  for (const m of MIGRATIONS) {
    if (cmp(m.version, from) > 0 && cmp(m.version, target) <= 0) {
      m.apply({
        appRoot,
        from,
        to: target,
        dryRun,
        explicitImports: options.explicitImports ?? false,
        report,
        log,
      });
    }
  }

  // A migration may have conservatively edited package.json. Merge those
  // changes back before writing the dependency bump so neither side is lost.
  if (!dryRun) {
    Object.assign(pkg, JSON.parse(readFileSync(pkgPath, "utf8")) as Record<string, unknown>);
    bumpDeps(pkg, target, options.packageVersions);
  }
  if (!dryRun) writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + "\n", "utf8");
  return { root: appRoot, packagePath: pkgPath, pkg };
}

export function verificationCommands(scripts: Record<string, string>): string[][] {
  const commands: string[][] = [];

  // Update migrations can rewrite TypeScript, JSON, and framework configuration
  // files. Normalize those edits with the project's own formatter before running
  // the read-only format check. This prevents a successful migration from failing
  // only because wrnexus.config.ts or another touched file needs Prettier output.
  if (scripts.format) commands.push(["run", "format"]);
  if (scripts.check) commands.push(["run", "check"]);
  if (scripts.build) commands.push(["run", "build"]);

  return commands;
}

export function blockingVerificationChecks(checks: DoctorCheck[]): DoctorCheck[] {
  return checks.filter((check) => !check.ok && check.level !== "warning");
}

function verifyApp(app: UpdatedApp): boolean {
  // Generated declarations must reflect the newly installed CLI before the
  // application's lint/typecheck scripts inspect them. This also repairs stale
  // generated output left by an older framework version.
  if (existsSync(join(app.root, "app", "pages"))) generateApplicationTypes(app.root);
  const health = inspectProject(app.root);
  const failed = blockingVerificationChecks(health);

  if (failed.length) {
    for (const check of failed) {
      console.error(`  ✗ ${check.name}: ${check.detail}`);
    }

    return false;
  }

  const scripts = (app.pkg.scripts ?? {}) as Record<string, string>;
  for (const args of verificationCommands(scripts)) {
    const action = args[1] === "format" ? "Formatting" : "Verifying";
    console.log(`\n  ${action} ${app.pkg.name ?? app.root}: bun ${args.join(" ")}…`);
    const result = spawnSync(process.execPath, args, { cwd: app.root, stdio: "inherit" });
    if (result.status !== 0) return false;
  }
  return true;
}

/** Load a `wrnexus.workspace.ts`, returning its app dirs (or null if not a workspace). */
async function workspaceApps(root: string): Promise<string[] | null> {
  for (const f of ["wrnexus.workspace.ts", "wrnexus.workspace.js", "wrnexus.workspace.mjs"]) {
    const path = join(root, f);
    if (!existsSync(path)) continue;
    const mod = (await import(pathToFileURL(path).href)) as {
      default?: { apps?: { dir: string }[] };
    };
    return (mod.default?.apps ?? []).map((a) => resolve(root, a.dir));
  }
  return null;
}

export async function runUpdate(dir: string, args: string[]): Promise<void> {
  const root = resolve(dir);
  const dryRun = args.includes("--dry-run");
  const verify = !args.includes("--no-verify");
  const explicitImports = args.includes("--explicit-imports");
  const reportRequested = args.includes("--report");
  const versionArg = args.find((a) => a.startsWith("--version="))?.split("=")[1];
  const target =
    versionArg ?? (args.includes("--latest") ? latestPublished() : null) ?? cliVersion();

  // Always let the target CLI apply its own migrations. Without this handoff,
  // an older installed CLI could bump dependency versions but would not know
  // about syntax/config migrations shipped by the newer release.
  if (!args.includes("--delegated") && cmp(target, cliVersion()) > 0) {
    console.log(`\n  Fetching WRNexusJS CLI ${target} to run its project migrations…`);
    const forwarded = args.filter(
      (arg) => !arg.startsWith("--version=") && arg !== "--latest" && arg !== "--delegated",
    );
    const result = spawnSync(
      process.execPath,
      [
        "x",
        `@wrnexus/cli@${target}`,
        "update",
        dir,
        `--version=${target}`,
        "--delegated",
        ...forwarded,
      ],
      { cwd: root, stdio: "inherit" },
    );
    if (result.status !== 0) process.exitCode = result.status ?? 1;
    return;
  }

  // Workspace → update the root manifest + every app; else just this app.
  const apps = await workspaceApps(root);
  const targets = apps ? [root, ...apps] : [root];
  const dependencyNames = targets.flatMap((targetRoot) => {
    const file = join(targetRoot, "package.json");
    if (!existsSync(file)) return [];
    const manifest = JSON.parse(readFileSync(file, "utf8")) as Record<string, any>;
    return ["dependencies", "devDependencies", "peerDependencies"].flatMap((field) =>
      Object.keys(manifest[field] ?? {}).filter((name) => name.startsWith("@wrnexus/")),
    );
  });
  const packageVersions = versionArg
    ? new Map<string, string>()
    : await latestPackageVersions(dependencyNames);

  console.log(`\n  ⚡ wrnexus update → migrations ${target}${dryRun ? "  (dry run)" : ""}`);
  if (!versionArg) {
    console.log(
      `  Resolved ${packageVersions.size} installed WRNexus package version(s) from npm.\n`,
    );
  } else {
    console.log("");
  }

  const reports = new Map<string, MigrationReport>();
  const updated = targets
    .map((targetRoot) => {
      const report: MigrationReport = {
        changedAutomatically: [],
        needsReview: [],
        unresolvedImports: [],
        ambiguousFunctions: [],
        legacyOutputPayloads: [],
        parseFailures: [],
      };
      reports.set(targetRoot, report);
      return updateApp(targetRoot, target, dryRun, { explicitImports, report, packageVersions });
    })
    .filter(Boolean) as UpdatedApp[];

  if (reportRequested || dryRun) {
    for (const [targetRoot, report] of reports) {
      console.log(`
  Migration report: ${targetRoot}`);
      console.log(`    Changed automatically: ${new Set(report.changedAutomatically).size}`);
      console.log(`    Needs review: ${report.needsReview.length}`);
      console.log(`    Unresolved imports: ${report.unresolvedImports.length}`);
      console.log(`    Ambiguous function runtime: ${report.ambiguousFunctions.length}`);
      console.log(
        `    Legacy output payloads using unknown: ${report.legacyOutputPayloads.length}`,
      );
      console.log(`    Parse failures: ${report.parseFailures.length}`);
    }
  }

  if (dryRun) {
    console.log(`\n  Dry run — no files written. Re-run without --dry-run to apply.\n`);
    return;
  }

  // One install at the top (Bun workspaces hoist), using the bun that's running us.
  console.log(`\n  Installing…`);
  const res = spawnSync(process.execPath, ["install"], { cwd: root, stdio: "inherit" });
  if (res.status !== 0) {
    console.error(`\n  ⚠ bun install exited with ${res.status}. Fix the error and re-run.`);
    process.exit(res.status ?? 1);
  }

  if (verify) {
    const appsToVerify = apps ? updated.filter((app) => app.root !== root) : updated;
    for (const app of appsToVerify) {
      if (!verifyApp(app)) {
        console.error(
          `\n  ✗ Update files were applied, but verification failed. Fix the reported error and re-run wrnexus update.`,
        );
        process.exitCode = 1;
        return;
      }
    }
  }

  for (const app of updated) {
    app.pkg.wrnexus = { ...(app.pkg.wrnexus as object), version: target };
    writeFileSync(app.packagePath, JSON.stringify(app.pkg, null, 2) + "\n", "utf8");
  }

  console.log(`\n  ✓ Updated and verified ${updated.length} project manifest(s).`);
  console.log(`    Migration checkpoint: ${target}; dependencies use their own npm versions.`);
  console.log(`    Backups are under .wrnexus/update-backups/. Review and redeploy.\n`);
}
