@liustack/pptfast 0.8.0 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/cli.ts","../src/cli/commands.ts","../src/cli/config.ts","../src/cli/home.ts","../src/cli/deck-dir.ts","../src/cli/load-ir.ts","../src/cli/preview-html.ts","../src/cli/update.ts"],"sourcesContent":["#!/usr/bin/env node\nimport { Command } from \"commander\"\nimport { installNodePlatform } from \"./platform/node\"\nimport {\n runAssemble,\n runAudit,\n runDisassemble,\n runInit,\n runMigrate,\n runNarratives,\n runPreview,\n runRender,\n runSchema,\n runSpecValidate,\n runThemes,\n runValidate,\n} from \"./cli/commands\"\nimport { checkForUpdate, createSelfUpdater } from \"./cli/update\"\nimport { VERSION } from \"./version\"\n\ninstallNodePlatform()\n\nconst program = new Command()\nprogram\n .name(\"pptfast\")\n .description(\"Stable, editable PPTX generation for AI agents — semantic IR in, native DrawingML out\")\n .version(VERSION)\n\nfunction fail(e: unknown): never {\n console.error(e instanceof Error ? e.message : String(e))\n process.exit(1)\n}\n\nprogram\n .command(\"render\")\n .description(\"Render an IR JSON file, deck project directory, or bare deck name to a .pptx\")\n .argument(\"<target>\", \"IR JSON file, deck project directory, or bare name under ~/.pptfast/decks\")\n .requiredOption(\"-o, --output <file>\", \"output .pptx path\")\n .option(\"--theme <id>\", \"override the deck theme (see `pptfast themes`)\")\n .option(\"--style <path>\", \"style overrides JSON re-coloring the theme (see `pptfast schema --style`)\")\n .option(\"--draft\", \"allow unfilled placeholder pages (skip the draft gate)\")\n .action(async (target: string, opts: { output: string; theme?: string; style?: string; draft?: boolean }) => {\n try {\n console.log(\n await runRender(target, {\n output: opts.output,\n theme: opts.theme,\n stylePath: opts.style,\n draft: opts.draft,\n }),\n )\n } catch (e) {\n fail(e)\n }\n })\n\nprogram\n .command(\"validate\")\n .description(\"Validate an IR JSON file, deck project directory, or bare deck name against the schema\")\n .argument(\"<target>\", \"IR JSON file, deck project directory, or bare name under ~/.pptfast/decks\")\n .action(async (target: string) => {\n try {\n console.log(await runValidate(target))\n } catch (e) {\n fail(e)\n }\n })\n\nprogram\n .command(\"audit\")\n .description(\n \"Deterministic geometry audit (overflow, out-of-bounds, low-contrast, overlap, content-truncated, content-dropped), plus an optional --pixels contrast pass — exits 1 when it finds anything\",\n )\n .argument(\"<target>\", \"IR JSON file, deck project directory, or bare name under ~/.pptfast/decks\")\n .option(\"--json\", \"machine-readable output (the full AuditReport)\")\n .option(\"--pixels\", \"also run the optional pixel-contrast pass over image-backed text (requires sharp)\")\n .action(async (target: string, opts: { json?: boolean; pixels?: boolean }) => {\n try {\n const { output, hasFindings } = await runAudit(target, { json: opts.json, pixels: opts.pixels })\n console.log(output)\n if (hasFindings) process.exit(1)\n } catch (e) {\n fail(e)\n }\n })\n\nprogram\n .command(\"schema\")\n .description(\"Print the IR JSON Schema (feed this to a model before it writes IR)\")\n .option(\"--style\", \"print the style-override schema instead\")\n .option(\"--spec\", \"print the deck spec schema instead\")\n .option(\"--plan\", \"removed — use --spec instead\")\n .action((opts: { style?: boolean; spec?: boolean; plan?: boolean }) => {\n // vocabulary-v4 rename (spec §8.2): `--plan` renamed to `--spec`, no\n // long-lived alias — hard-fail pointing at the one new flag rather than\n // silently keep serving the plan schema under its old name.\n if (opts.plan) {\n fail(new Error(\"`pptfast schema --plan` has been renamed to `pptfast schema --spec` — run `pptfast schema --spec` instead\"))\n }\n console.log(runSchema(opts.spec ? \"spec\" : opts.style ? \"style\" : undefined))\n })\n\n// vocabulary-v4 rename (spec §8.2): `pptfast plan validate` renamed to\n// `pptfast spec validate`. The `plan` command group stays registered only so\n// `pptfast plan validate <file>` fails with a message pointing at the new\n// command, rather than commander's own generic \"unknown command\" error.\nconst plan = program.command(\"plan\").description(\"Removed — use `pptfast spec` instead\")\nplan\n .command(\"validate\")\n .description(\"Removed — use `pptfast spec validate` instead\")\n .argument(\"<file>\")\n .action(() => {\n fail(new Error(\"`pptfast plan validate` has been renamed to `pptfast spec validate` — run `pptfast spec validate <file>` instead\"))\n })\n\nconst spec = program.command(\"spec\").description(\"Deck spec commands (spec §6)\")\nspec\n .command(\"validate\")\n .description(\"Validate a deck spec JSON file against the schema and strategy-aware hard gates\")\n .argument(\"<spec.json>\")\n .action(async (specPath: string) => {\n try {\n console.log(await runSpecValidate(specPath))\n } catch (e) {\n fail(e)\n }\n })\n\nprogram\n .command(\"assemble\")\n .description(\"Assemble a deck project directory (deck.spec.json + pages/ + assets/) into an IR JSON file\")\n .argument(\"<dir|name>\", \"deck project directory, or bare name under ~/.pptfast/decks\")\n .option(\"-o, --output <file>\", \"output IR JSON path (default: <dir>/deck.json)\")\n .action(async (target: string, opts: { output?: string }) => {\n try {\n console.log(await runAssemble(target, { output: opts.output }))\n } catch (e) {\n fail(e)\n }\n })\n\nprogram\n .command(\"disassemble\")\n .description(\"Split an IR JSON file into a deck project directory (deck.spec.json + pages/)\")\n .argument(\"<ir.json>\", \"path to the IR file\")\n .requiredOption(\"-o, --output <dir>\", \"output deck project directory\")\n .action(async (irPath: string, opts: { output: string }) => {\n try {\n console.log(await runDisassemble(irPath, opts.output))\n } catch (e) {\n fail(e)\n }\n })\n\nprogram\n .command(\"migrate\")\n .description(\"Convert a v3 IR file to v4, or a deck.plan.json project directory to deck.spec.json — deterministic, no model\")\n .argument(\"<input>\", \"IR v3 JSON file, or a deck project directory containing deck.plan.json\")\n .requiredOption(\"-o, --output <output>\", \"output path — an IR JSON file for a v3 file input, a directory for a deck-project-directory input\")\n .action(async (input: string, opts: { output: string }) => {\n try {\n console.log(await runMigrate(input, opts.output))\n } catch (e) {\n fail(e)\n }\n })\n\nprogram\n .command(\"themes\")\n .description(\"List built-in themes\")\n .option(\"--json\", \"machine-readable output\")\n .action((opts: { json?: boolean }) => console.log(runThemes(Boolean(opts.json))))\n\nprogram\n .command(\"narratives\")\n .description(\"List named narrative presets (strategy/pacing/audience axes + theme recommendations)\")\n .option(\"--json\", \"machine-readable output\")\n .action((opts: { json?: boolean }) => console.log(runNarratives(Boolean(opts.json))))\n\n// vocabulary-v4 rename (spec §8.2): `pptfast scenarios` renamed to\n// `pptfast narratives`, no long-lived alias — hard-fail pointing at the new\n// command name.\nprogram\n .command(\"scenarios\")\n .description(\"Removed — use `pptfast narratives` instead\")\n .action(() => {\n fail(new Error(\"`pptfast scenarios` has been renamed to `pptfast narratives` — run `pptfast narratives` instead\"))\n })\n\nprogram\n .command(\"init\")\n .description(\"Scaffold a pptfast.config.json in the current directory\")\n .action(async () => {\n try {\n console.log(await runInit())\n } catch (e) {\n fail(e)\n }\n })\n\nprogram\n .command(\"preview\")\n .description(\"Render each slide to an SVG file for visual self-check\")\n .argument(\"<target>\", \"IR JSON file, deck project directory, or bare name under ~/.pptfast/decks\")\n .requiredOption(\"-o, --output <dir>\", \"output directory\")\n .option(\"--html\", \"also write a self-contained preview.html (all slides inlined — thumbnail strip, keyboard navigation) for human review\")\n .action(async (target: string, opts: { output: string; html?: boolean }) => {\n try {\n console.log(await runPreview(target, opts.output, { htmlOut: opts.html }))\n } catch (e) {\n fail(e)\n }\n })\n\nprogram\n .command(\"check-update\")\n .description(\"Check npm for a newer pptfast release\")\n .action(async () => {\n const info = await checkForUpdate({ currentVersion: VERSION })\n if (!info.checked) fail(new Error(`update check failed: ${info.error}`))\n console.log(\n info.updateAvailable\n ? `update available: ${info.currentVersion} → ${info.latestVersion} (run \\`pptfast self-update\\`)`\n : `pptfast ${info.currentVersion} is up to date`,\n )\n })\n\nprogram\n .command(\"self-update\")\n .description(\"Update the global pptfast install to the latest release\")\n .action(async () => {\n try {\n const result = await createSelfUpdater()({ currentVersion: VERSION })\n console.log(\n result.updated\n ? `updated: ${result.currentVersion} → ${result.latestVersion}`\n : `already at the latest version (${result.currentVersion})`,\n )\n } catch (e) {\n fail(e)\n }\n })\n\nprogram.parseAsync().catch(fail)\n","import { mkdir, rm, writeFile } from \"node:fs/promises\"\nimport { dirname, join, relative, resolve } from \"node:path\"\nimport {\n formatIssues,\n formatWarnings,\n generatePptx,\n irJsonSchema,\n listThemes,\n renderSlideSvg,\n styleJsonSchema,\n validateIr,\n type ValidationIssue,\n} from \"../api\"\nimport { PptfastError } from \"../errors\"\nimport { StyleOverrideSchema, type PptxIR, type StyleOverride } from \"../ir\"\nimport { PptxIRV3Schema } from \"../ir/legacy-v3\"\nimport { migrateIrV3ToV4 } from \"../ir/migrate\"\nimport { disassembleDeck, type PageContent } from \"../plan/assemble\"\nimport { formatInvalidSpecError, specJsonSchema, resolveSpecThemeId, validateSpec } from \"../plan\"\nimport { migrateDeckPlanToSpec } from \"../plan/migrate\"\nimport { AUDIENCE_VALUES, PACING_BUDGETS, STRATEGY_DEFINITIONS, NARRATIVE_PRESETS, resolveNarrative, type NarrativeProfile } from \"../narrative\"\nimport { auditDeck, type AuditFinding, type AuditReport } from \"../svg/audit/deck-audit\"\nimport { getInstalledThemeIds } from \"../themes/definitions\"\nimport { CONFIG_FILENAME, findConfig, findUserConfig } from \"./config\"\nimport {\n assertSafeFileSegment,\n isDeckDirectory,\n pathExists,\n readDeckDir,\n resolveDeckTarget,\n writeDeckAssets,\n PLAN_FILENAME,\n SPEC_FILENAME,\n} from \"./deck-dir\"\nimport { loadIrFile, resolveLocalAssets } from \"./load-ir\"\nimport { buildPreviewHtml } from \"./preview-html\"\n\n/** `findUserConfig()`'s own return shape, named here so it can be threaded as\n * a parameter (`loadDeckTarget`/`applyDeckConfig` below) instead of each\n * callee re-fetching it — see `applyDeckConfig`'s own doc comment for why. */\ntype UserConfigHit = Awaited<ReturnType<typeof findUserConfig>>\n\n/** `findConfig()`'s own return shape — the project-layer counterpart to\n * {@link UserConfigHit}, threaded the same way and for the same reason\n * (W5 task 6: `loadDeckTarget` now needs the project layer too, for\n * `decksDir` — see {@link resolveDecksDirSource}). */\ntype ProjectConfigHit = Awaited<ReturnType<typeof findConfig>>\n\nasync function loadStyleFile(path: string): Promise<StyleOverride> {\n const raw = await loadIrFile(path)\n const r = StyleOverrideSchema.safeParse(raw)\n if (!r.success) {\n const detail = r.error.issues\n .map((i) => `${i.path.join(\".\") || \"(root)\"}: ${i.message}`)\n .join(\"\\n\")\n throw new PptfastError(`invalid style file ${path}:\\n${detail}`)\n }\n return r.data\n}\n\n/** Names which of the four precedence layers the (invalid) resolved `theme`\n * value came from, for {@link applyDeckConfig}'s unknown-theme error — a\n * config-file layer names its own path, `--theme` names itself, and the\n * IR's own default has no path to name at all. */\nfunction describeThemeSource(\n opts: { theme?: string },\n projectHit: { path: string; config: { theme?: string } } | null,\n userHit: UserConfigHit,\n): string {\n if (opts.theme !== undefined) return \"--theme\"\n if (projectHit?.config.theme !== undefined) return projectHit.path\n if (userHit?.config.theme !== undefined) return userHit.path\n return \"the deck's own theme\"\n}\n\n/**\n * The `config` argument `resolveDeckTarget` (`./deck-dir.ts`) and its\n * `decksRoot` (`./home.ts`) expect: an object exposing `decksDir`, resolved\n * against whichever base that value's own layer implies. Project\n * `pptfast.config.json`'s own `decksDir` (spec §7's project-level escape\n * hatch, `ConfigSchema` in `./config.ts`, W5 task 6) wins over the user\n * config's (`UserConfigSchema`) when both are set — same project-beats-user\n * precedence as `theme`/`style` (see `applyDeckConfig` below) — but the two\n * layers resolve against different bases (project against the config file's\n * own directory, user against `pptfastHome()`, `decksRoot`'s one fixed\n * base), so a winning project value is resolved to an absolute path *here*,\n * before being handed down: `decksRoot`'s own\n * `resolve(pptfastHome(), config?.decksDir ?? \"decks\")` then returns that\n * absolute path unchanged (`path.resolve`'s own semantics for an absolute\n * later segment) — the same \"already-absolute short-circuits the base\"\n * behavior `decksRoot({ decksDir: \"/elsewhere/decks\" })` already exercises\n * for the user layer, reused rather than reimplemented. Falls through to\n * `userHit?.config` untouched when the project layer has no `decksDir` of\n * its own — including when there is no project config at all — so the user\n * layer (or, absent that too, `decksRoot`'s own built-in default) keeps\n * working exactly as before this function existed.\n */\nfunction resolveDecksDirSource(\n projectHit: ProjectConfigHit,\n userHit: UserConfigHit,\n): { decksDir?: string } | undefined {\n if (projectHit?.config.decksDir !== undefined) {\n return { decksDir: resolve(dirname(projectHit.path), projectHit.config.decksDir) }\n }\n return userHit?.config\n}\n\n/**\n * Resolve deck defaults onto the raw (pre-validation) IR.\n * Precedence (spec §7's four-layer chain, W5 task 5): CLI flag > project\n * `pptfast.config.json` (walked up from cwd) > user `~/.pptfast/config.json`\n * (`findUserConfig`, no cwd walk-up — a single fixed path, see `./config.ts`)\n * > whatever the artifact itself already carries (an authored IR's own\n * `theme`, or `PptxIRSchema`'s own \"consulting\" default when nothing\n * anywhere sets one — that bottom fallback is `irTheme.id`/`irTheme.style`\n * below, left `undefined` here for the schema to fill in). `--theme` only\n * swaps theme.id — IR-authored style survives.\n *\n * `opts.projectHit`/`opts.userHit` are the caller's own already-fetched\n * `findConfig(cwd)`/`findUserConfig()` results (`undefined` when the caller\n * has not fetched one — this function fetches whichever is missing itself,\n * so it stays usable standalone). Every real caller (`runRender`/\n * `runValidate`/`runPreview` below) fetches both exactly once — `loadDeckTarget`\n * needs the project layer too now, for `decksDir` (W5 task 6,\n * {@link resolveDecksDirSource}) — and passes them to both this function and\n * `loadDeckTarget`, so a command reads either config file at most once per\n * invocation instead of once per helper that happens to need it.\n *\n * The installed-theme check used to run at config *read* time\n * (`readConfigFile`, `./config.ts`) — eagerly, against every layer's value,\n * whether or not it would ever actually apply. It now runs here instead,\n * once, against `theme` (the value that actually wins the four-layer\n * chain): a stale/unknown theme sitting in a config layer that a `--theme`\n * flag (or a higher-precedence config layer) overrides anyway must not\n * hard-fail a command over a value nothing was ever going to use.\n */\nexport async function applyDeckConfig(\n raw: unknown,\n opts: { theme?: string; stylePath?: string; cwd: string; projectHit?: ProjectConfigHit; userHit?: UserConfigHit },\n): Promise<void> {\n if (typeof raw !== \"object\" || raw === null) return // schema error surfaces in validateIr\n const deck = raw as Record<string, unknown>\n const irTheme =\n typeof deck.theme === \"object\" && deck.theme !== null\n ? (deck.theme as Record<string, unknown>)\n : {}\n const [projectHit, userHit] = await Promise.all([\n opts.projectHit !== undefined ? Promise.resolve(opts.projectHit) : findConfig(opts.cwd),\n opts.userHit !== undefined ? Promise.resolve(opts.userHit) : findUserConfig(),\n ])\n const theme = opts.theme ?? projectHit?.config.theme ?? userHit?.config.theme ?? (irTheme.id as string | undefined)\n const style = opts.stylePath\n ? await loadStyleFile(opts.stylePath)\n : (projectHit?.config.style ?? userHit?.config.style ?? irTheme.style)\n if (theme !== undefined) {\n const installedThemeIds = getInstalledThemeIds()\n if (!installedThemeIds.includes(theme)) {\n throw new PptfastError(\n `unknown theme \"${theme}\" (from ${describeThemeSource(opts, projectHit, userHit)}) — available: ${installedThemeIds.join(\", \")} (see \\`pptfast themes\\`)`,\n )\n }\n }\n if (theme === undefined && style === undefined) return\n deck.theme = { ...irTheme, id: theme, ...(style !== undefined ? { style } : {}) }\n}\n\n/**\n * Shared \"turn a CLI target argument into a raw IR-shaped object plus its\n * asset base directory\" step for `runValidate`/`runRender`/`runPreview` (W5\n * task 5) — the one piece of logic those three commands would otherwise\n * triplicate. `arg` is resolved through `resolveDeckTarget` (path vs.\n * bare-name, spec §7) using the effective `decksDir` source — project config\n * when it sets one, else the user config's, else `resolveDeckTarget`'s own\n * built-in default (W5 task 6, {@link resolveDecksDirSource}) — then\n * branches on whether the resolved target is a deck project directory:\n *\n * - directory → `readDeckDir` (assemble in memory — spec + pages/ + assets/,\n * `./deck-dir.ts`), asset paths resolve against the deck directory itself.\n * - file → the pre-existing single-file path, byte-for-byte: `loadIrFile`\n * then the same `dirname(resolve(...))` asset base every caller already\n * used. When `arg` is an explicit path (has a separator, or exists\n * locally — true of every pre-W5 caller, since every existing test passes\n * a full path), `resolveDeckTarget` returns it completely unchanged with\n * no `fs` call at all, so this branch degenerates to exactly the old\n * inline code — single-file behavior stays byte-identical.\n *\n * `isDir` is threaded back so `runValidate` can gate its dir-only placeholder\n * note on it (single-file mode must never grow that note, even for a\n * hand-authored IR that happens to set `placeholder: true` itself).\n *\n * `projectHit`/`userHit` are the caller's own already-fetched\n * `findConfig(cwd)`/`findUserConfig()` results (see `applyDeckConfig`'s doc\n * comment above for why both are threaded rather than fetched here too).\n */\nasync function loadDeckTarget(\n arg: string,\n cwd: string,\n projectHit: ProjectConfigHit,\n userHit: UserConfigHit,\n): Promise<{ raw: unknown; baseDir: string; isDir: boolean }> {\n const target = await resolveDeckTarget(arg, resolveDecksDirSource(projectHit, userHit), cwd)\n if (await isDeckDirectory(target)) {\n const { ir, deckDir } = await readDeckDir(target)\n return { raw: ir, baseDir: deckDir, isDir: true }\n }\n const raw = await loadIrFile(target)\n return { raw, baseDir: dirname(resolve(target)), isDir: false }\n}\n\nexport interface RenderOptions {\n output: string\n theme?: string\n stylePath?: string\n cwd?: string\n /** Skip the unfilled-placeholder-pages gate (W5 task 1) — see `generatePptx` in `../api`. */\n draft?: boolean\n}\n\n/**\n * `irPath` accepts a single IR/spec JSON file, a deck project directory, or\n * a bare deck name under `~/.pptfast/decks` (W5 task 5, `loadDeckTarget`\n * above) — directory/bare-name input is assembled in memory first, then\n * follows the exact same validate → resolve-assets → generate pipeline a\n * single file always has. `--draft` threads through unchanged either way\n * (`generatePptx`'s own gate, W5 task 1) — a deck project's own placeholder\n * pages are exactly what that gate exists to catch.\n *\n * Appends the same field-alias {@link normalizedNote} `runValidate` below\n * prints (W5 whole-branch review finding 3 — the README already claimed\n * `render` did this; it never actually threaded `v.normalized` through\n * until now), plus {@link warningsNote} (borrow wave, Task 2) whenever the\n * pre-flight `validateIr` call below returned warn-severity findings —\n * `generatePptx`'s own internal re-validate (`../api.ts`) follows the exact\n * same error-only severity rule, so this pre-flight check and the actual\n * generation it gates in step can never disagree on what counts as blocking.\n */\nexport async function runRender(irPath: string, opts: RenderOptions): Promise<string> {\n const cwd = opts.cwd ?? process.cwd()\n const [projectHit, userHit] = await Promise.all([findConfig(cwd), findUserConfig()])\n const { raw, baseDir } = await loadDeckTarget(irPath, cwd, projectHit, userHit)\n await applyDeckConfig(raw, { theme: opts.theme, stylePath: opts.stylePath, cwd, projectHit, userHit })\n const v = validateIr(raw)\n if (!v.ok) throw new PptfastError(`invalid IR:\\n${formatIssues(v.errors)}`)\n await resolveLocalAssets(v.ir!, baseDir)\n const bytes = await generatePptx(v.ir!, { draft: opts.draft })\n await mkdir(dirname(resolve(opts.output)), { recursive: true })\n await writeFile(opts.output, bytes)\n const ok = `wrote ${opts.output} (${v.ir!.slides.length} slides, ${bytes.length} bytes)`\n const notes = [warningsNote(v.warnings), normalizedNote(v.normalized)].filter((n): n is string => n !== undefined)\n return notes.length > 0 ? `${ok}\\n${notes.join(\"\\n\")}` : ok\n}\n\n/**\n * `\"note: N field alias(es) normalized\\n path: alias → canonical\\n...\"` —\n * the note line every one of `validateIr`'s callers appends after its own\n * success line when `ValidateResult.normalized` (`../api.ts`) is non-empty,\n * i.e. `validateIr` deterministically rewrote at least one synonym field\n * name before parsing (W5 task 4 — kpi `title`→`label` and friends,\n * `../ir/field-aliases.ts`). Extracted so `runRender`/`runPreview` (W5\n * whole-branch review finding 3 — the README already claimed `validate`\n * *and* `render` both printed this note — `render` never actually did, and\n * `preview` is folded in here too for the same reason) can append the exact\n * same note `runValidate` below has always printed, instead of each\n * re-deriving the formatting a second and third time. `undefined` when\n * nothing was normalized, the same \"let the caller skip the line entirely\"\n * shape {@link placeholderNote} below already uses.\n */\nfunction normalizedNote(normalized: string[] | undefined): string | undefined {\n if (!normalized || normalized.length === 0) return undefined\n const n = normalized.length\n return `note: ${n} field alias${n === 1 ? \"\" : \"es\"} normalized\\n${normalized.map((line) => ` ${line}`).join(\"\\n\")}`\n}\n\n/**\n * `\"warning: page N — path: message\"` block, one line per\n * {@link ValidateResult.warnings} entry (`../api.ts`, borrow wave Task 2's\n * dual-threshold severity split) — printed by `runValidate`/`runRender`\n * alongside their own success line whenever `validateIr` returned at least\n * one warn-severity finding. `undefined` when there are none, same\n * \"let the caller skip the line entirely\" shape {@link normalizedNote}\n * above and {@link placeholderNote} below both use. Exit code is\n * unaffected either way — a warning never turns a `runValidate`/`runRender`\n * call into a thrown `PptfastError` (only `!v.ok`, i.e. an error-severity\n * finding, does that). This note is purely additive visibility.\n */\nfunction warningsNote(warnings: ValidationIssue[] | undefined): string | undefined {\n if (!warnings || warnings.length === 0) return undefined\n return formatWarnings(warnings)\n}\n\n/**\n * Dir-mode-only informational note (W5 task 5, `runValidate` below): unlike\n * `generatePptx`'s draft gate (a hard error) or the content-quality gate\n * (which skips a placeholder's content rules entirely, `ir-quality.ts`), a\n * placeholder page is schema-valid and produces no validation issue on its\n * own — without this, a deck project with pages still unfilled would\n * validate silently \"OK\" with no signal anything is left to do. `undefined`\n * when there are none, so the caller can skip the note line entirely rather\n * than test its own string for emptiness.\n */\nfunction placeholderNote(ir: PptxIR): string | undefined {\n const placeholders = ir.slides\n .map((slide, i) => ({ slide, page: i + 1 }))\n .filter(({ slide }) => slide.placeholder)\n if (placeholders.length === 0) return undefined\n const refs = placeholders\n .map(({ slide, page }) => (slide.id ? `${slide.id} (page ${page})` : `page ${page}`))\n .join(\", \")\n return `note: ${placeholders.length} unfilled placeholder page${placeholders.length === 1 ? \"\" : \"s\"}: ${refs}`\n}\n\n/**\n * `irPath` accepts a single IR/spec JSON file, a deck project directory, or\n * a bare deck name (same `loadDeckTarget` resolution `runRender` uses).\n * Directory/bare-name input additionally gets a {@link placeholderNote} —\n * gated on `isDir` specifically so single-file mode (including a\n * hand-authored IR that sets `placeholder: true` itself) never grows one,\n * keeping that path's output byte-identical to before this task.\n *\n * Returns human-readable report. Throws PptfastError when invalid (CLI exit 1).\n * When `validateIr` deterministically rewrote any synonym field names before\n * parsing (W5 task 4 — kpi `title`→`label` and friends, `ir/field-aliases.ts`),\n * appends them as a \"note\" line after the OK summary: visible so the caller\n * knows their input got silently massaged, but never a reason to fail — a\n * fixed alias never makes it into `v.errors`.\n *\n * Borrow wave, Task 2 (dual-threshold severity): also appends\n * {@link warningsNote} whenever `validateIr` returned warn-severity\n * findings — printed as `\"warning: ...\"` lines, exit code 0 either way\n * (only `!v.ok`, above, throws). A deck can print `OK` and still carry\n * warnings — that combination is the point of the split, not a bug.\n *\n * Borrow wave, Task 2 follow-up (review finding, medium): also runs\n * `resolveLocalAssets` on `v.ir!`, same as `runRender`/`runAudit`/\n * `runPreview` already do — `validateIr` itself only sniffs already-inlined\n * `data:` URIs (`checkAssetBytes`, `../api.ts`'s own doc comment on why a\n * local file path is a different, Node-only ingestion form), so without\n * this a deck-dir referencing a corrupt local `.png` printed `OK` here while\n * `render` correctly rejected the exact same input right after — an\n * inconsistency with SKILL.md's Phase 3 contract, which treats `validate`\n * as the authoritative pre-flight check. `resolveLocalAssets` mutating\n * `v.ir!.assets.images[x].src` into a data URI as a side effect is harmless\n * here — nothing this function reads afterward (`slides.length`, `theme.id`,\n * `placeholderNote`) depends on `src` — so there was no reason to write a\n * separate check-only variant; reusing the exact same function guarantees\n * identical rejection semantics with `render` by construction, not by\n * keeping two copies of the same logic in sync by hand.\n */\nexport async function runValidate(irPath: string, cwd = process.cwd()): Promise<string> {\n const [projectHit, userHit] = await Promise.all([findConfig(cwd), findUserConfig()])\n const { raw, baseDir, isDir } = await loadDeckTarget(irPath, cwd, projectHit, userHit)\n await applyDeckConfig(raw, { cwd, projectHit, userHit })\n const v = validateIr(raw)\n if (!v.ok)\n throw new PptfastError(\n `invalid IR (${v.errors.length} issue${v.errors.length === 1 ? \"\" : \"s\"}):\\n${formatIssues(v.errors)}`,\n )\n await resolveLocalAssets(v.ir!, baseDir)\n const ok = `OK — ${v.ir!.slides.length} slides, theme \"${v.ir!.theme.id}\"`\n const notes: string[] = []\n const warnNote = warningsNote(v.warnings)\n if (warnNote) notes.push(warnNote)\n const aliasNote = normalizedNote(v.normalized)\n if (aliasNote) notes.push(aliasNote)\n if (isDir) {\n const note = placeholderNote(v.ir!)\n if (note) notes.push(note)\n }\n return notes.length > 0 ? `${ok}\\n${notes.join(\"\\n\")}` : ok\n}\n\n/**\n * `\"page 3 (p-kpi): [low-contrast] ...\"` — one line per {@link AuditFinding},\n * echoing `formatIssues`' own `\"page N (id) — path: message\"` convention\n * (`../api.ts`) with a bracketed `[code]` standing in for `path` — an\n * `AuditFinding` has no `path` (it is not a schema-location error, see that\n * interface's own doc comment in `../svg/audit/deck-audit.ts`), and `code`\n * is the closest equivalent \"what kind of problem\" tag. The bracket keeps an\n * audit-finding line visually distinct from a validate-error line at a\n * glance, per the plan's own worked example.\n */\nfunction formatAuditFinding(f: AuditFinding): string {\n const idSuffix = f.slideId !== undefined ? ` (${f.slideId})` : \"\"\n return `page ${f.page}${idSuffix}: [${f.code}] ${f.message}`\n}\n\n/**\n * Human-readable `pptfast audit` report (W6 task 2, spec §7 workflow ④):\n * every finding as its own {@link formatAuditFinding} line — already\n * naturally grouped by page, since `auditDeck` pushes findings in slide\n * order (`../svg/audit/deck-audit.ts`) — followed by a trailing summary line\n * in the plan's own literal wording (\"audited N pages, M skipped, K\n * findings\") so an agent can read just the last line to decide whether to\n * keep iterating, instead of counting findings itself. {@link placeholderNote}\n * runs unconditionally (unlike `runValidate`'s dir-mode-only gating on that\n * same helper below) — audit has no pre-existing single-file-mode output to\n * keep byte-identical the way `runValidate` did when that gating was added,\n * so there is no reason to withhold a genuinely useful note from a\n * hand-authored IR that happens to carry placeholders too.\n *\n * `checks.pixels === \"completed\"` (audit-v2 phase B, i.e. `--pixels` was\n * passed) appends one more line — purely additive, gated on that exact\n * value so the far more common no-`--pixels` run stays byte-identical to\n * the wording pinned above (`checks.pixels` is `\"not-requested\"` there,\n * never `\"completed\"`). No line at all for the omitted case rather than an\n * explicit \"not requested\" note: the human already knows whether they\n * passed the flag, and the machine-readable `--json` path (never silent\n * about `checks` either way) is what an agent actually consumes to tell\n * \"not checked\" apart from \"checked and clean\".\n */\nfunction formatAuditReport(report: AuditReport, ir: PptxIR): string {\n const lines = report.findings.map(formatAuditFinding)\n lines.push(\n `audited ${report.pagesAudited} page${report.pagesAudited === 1 ? \"\" : \"s\"}, ${report.pagesSkipped} skipped, ${report.findings.length} finding${report.findings.length === 1 ? \"\" : \"s\"}`,\n )\n if (report.checks.pixels === \"completed\") {\n lines.push(\"pixel-contrast check: completed\")\n }\n const note = placeholderNote(ir)\n if (note) lines.push(note)\n return lines.join(\"\\n\")\n}\n\nexport interface AuditOptions {\n json?: boolean\n cwd?: string\n /** `--pixels` (audit-v2 phase B, spec §4.3/§11.7): also run the optional\n * pixel-contrast pass over image-backed text. Explicit opt-in only — see\n * `auditDeck`'s own overload doc comment for why this is threaded as a\n * ternary with a literal in each arm rather than passed straight through\n * as `{ pixels: opts.pixels }` (a plain `boolean` doesn't match either\n * overload). Missing rasterization capability or a remote asset\n * reference makes this command fail loudly (a rejected `auditDeck`\n * promise propagates straight out of this function, same as the\n * existing invalid-IR `PptfastError` path) rather than silently\n * reporting a clean pixel check that never ran. */\n pixels?: boolean\n}\n\nexport interface AuditCliResult {\n /** Human report ({@link formatAuditReport}) or, with `opts.json`, the raw\n * `JSON.stringify`'d {@link AuditReport} verbatim — the plan's own \"the\n * full AuditReport\" requirement, unmodified by any CLI-side enrichment. */\n output: string\n /** `true` when `report.findings.length > 0`. The CLI (`../cli.ts`) prints\n * `output` either way, then exits 1 on this signal alone — clean exits 0\n * (spec §7 workflow ④: advisory, not a hard gate, but still\n * agent-judgeable purely from the exit code without parsing output). */\n hasFindings: boolean\n}\n\n/**\n * `pptfast audit <target> [--json]` (W6 task 2, spec §7 workflow ④): resolve\n * `target` through the exact same `loadDeckTarget` path `runValidate`/\n * `runRender`/`runPreview` already use (IR file / deck project directory /\n * bare name under `~/.pptfast/decks`), validate first, then hand the\n * validated IR to `auditDeck` (`../svg/audit/deck-audit.ts`, pure, no I/O).\n *\n * An invalid deck fails exactly like `pptfast validate` — same message\n * shape, same `PptfastError` → CLI exit-1 path — and never reaches\n * `auditDeck` at all: the geometry/contrast/overlap checks only mean\n * anything over a schema-valid, already-quality-gated deck (`auditDeck`'s\n * own \"advisory, not a hard gate\" doc comment — `validateIr` is the hard\n * gate this command leans on rather than re-implements).\n *\n * `resolveLocalAssets` runs after validation, same as `runRender`/\n * `runPreview` — a deck referencing local (non-`data:`/non-`http(s)`) image\n * files must have them inlined before `auditDeck`'s internal `renderSlideSvg`\n * calls, otherwise a local asset's `src` would still be its raw relative\n * path when the contrast checker's background-region walk inspects it,\n * auditing a slide shape that doesn't match what `render`/`preview` actually\n * produce for the same deck.\n *\n * No `--theme`/`--style` flags (unlike `runRender`) — the plan's CLI surface\n * for this command is deliberately just `<target> [--json]` — but\n * `applyDeckConfig` still runs (with no CLI-flag overrides) so a project/user\n * config's own theme/style default still applies, the same \"config layers\n * apply even with no flag passed\" behavior `runValidate` already has.\n */\nexport async function runAudit(target: string, opts: AuditOptions = {}): Promise<AuditCliResult> {\n const cwd = opts.cwd ?? process.cwd()\n const [projectHit, userHit] = await Promise.all([findConfig(cwd), findUserConfig()])\n const { raw, baseDir } = await loadDeckTarget(target, cwd, projectHit, userHit)\n await applyDeckConfig(raw, { cwd, projectHit, userHit })\n const v = validateIr(raw)\n if (!v.ok) {\n throw new PptfastError(\n `invalid IR (${v.errors.length} issue${v.errors.length === 1 ? \"\" : \"s\"}):\\n${formatIssues(v.errors)}`,\n )\n }\n await resolveLocalAssets(v.ir!, baseDir)\n const report = opts.pixels ? await auditDeck(v.ir!, { pixels: true }) : auditDeck(v.ir!)\n const hasFindings = report.findings.length > 0\n const output = opts.json ? JSON.stringify(report, null, 2) : formatAuditReport(report, v.ir!)\n return { output, hasFindings }\n}\n\n/**\n * Validate a deck spec JSON file (W5 task 2: `pptfast plan validate`, renamed\n * to `pptfast spec validate` — vocabulary-v4 rename, task 2, spec §8.2).\n * `loadIrFile` is a generic \"read + JSON-parse with a readable failure\n * message\" helper despite its IR-scoped name (`./load-ir.ts`) — reused as-is\n * rather than duplicated, same pattern `runValidate` above uses for IR.\n * Returns human-readable report. Throws PptfastError when invalid (CLI exit 1).\n */\nexport async function runSpecValidate(specPath: string): Promise<string> {\n const raw = await loadIrFile(specPath, \"spec\")\n const v = validateSpec(raw)\n if (!v.ok) {\n throw new PptfastError(formatInvalidSpecError(v.errors))\n }\n const spec = v.spec!\n // Safe to call unguarded: validateSpec already resolved this same\n // expression successfully as part of its own hard-gate chain.\n const axes = resolveNarrative(spec.narrative as string | Partial<NarrativeProfile> | undefined)\n return `OK — ${spec.pages.length} pages, narrative ${axes.strategy}/${axes.pacing}/${axes.audience}, theme \"${resolveSpecThemeId(spec)}\"`\n}\n\n/** `mode` selects which JSON Schema to print (`pptfast schema [--style|--spec]`,\n * spec §8.2's `schema --plan`→`schema --spec` rename, task 2) — `\"plan\"` was\n * the pre-rename flag value, no longer accepted (`../cli.ts` hard-fails a\n * bare `--plan` before this function is ever called, see that file's own\n * comment). */\nexport function runSchema(mode?: \"style\" | \"spec\"): string {\n const schema = mode === \"style\" ? styleJsonSchema() : mode === \"spec\" ? specJsonSchema() : irJsonSchema()\n return JSON.stringify(schema, null, 2)\n}\n\nexport function runThemes(asJson: boolean): string {\n const themes = listThemes()\n if (asJson) return JSON.stringify(themes, null, 2)\n return themes.map((t) => `${t.id.padEnd(12)} ${t.label}`).join(\"\\n\")\n}\n\n/**\n * List the named narrative presets (spec §5): strategy/pacing/audience axes +\n * soft theme recommendations — never a hard constraint, see\n * `NarrativePreset.themeRecommendations`'s own doc comment in `narrative/index.ts`.\n * `--json` hands back the full machine-readable payload an agent would want\n * before picking a narrative: every preset, plus the raw strategy/pacing/audience\n * tables those presets are built from (`STRATEGY_DEFINITIONS`/`PACING_BUDGETS`\n * carry data this wave doesn't yet consume for selection — W4's job — but are\n * still useful for a caller inspecting what each axis value means).\n *\n * CLI surface renamed this task (spec §8.2's `scenarios`→`narratives`\n * rename, task 2): command name `narratives`, `--json` output field names\n * `strategies`/`pacings` (were `modes`/`deliveries`) — kept in step with the\n * command's own new name rather than leaving a `pptfast narratives --json`\n * caller staring at a `modes` key for what is now the `strategy` axis.\n */\nexport function runNarratives(asJson: boolean): string {\n if (asJson) {\n return JSON.stringify(\n {\n presets: NARRATIVE_PRESETS,\n strategies: STRATEGY_DEFINITIONS,\n pacings: PACING_BUDGETS,\n audiences: AUDIENCE_VALUES,\n },\n null,\n 2,\n )\n }\n const rows = Object.values(NARRATIVE_PRESETS).map((p) => ({\n id: p.id,\n axes: `${p.axes.strategy}/${p.axes.pacing}/${p.axes.audience}`,\n themes: p.themeRecommendations.join(\", \"),\n }))\n const idWidth = Math.max(...rows.map((r) => r.id.length))\n const axesWidth = Math.max(...rows.map((r) => r.axes.length))\n return rows\n .map((r) => `${r.id.padEnd(idWidth + 2)}${r.axes.padEnd(axesWidth + 2)}${r.themes}`)\n .join(\"\\n\")\n}\n\nconst CONFIG_TEMPLATE = {\n theme: \"consulting\",\n style: {\n colors: { primary: \"#0B5FFF\", accent: \"#FF6A00\" },\n },\n} as const\n\n/** Scaffold pptfast.config.json in cwd. Never overwrites. */\nexport async function runInit(cwd = process.cwd()): Promise<string> {\n const target = join(cwd, CONFIG_FILENAME)\n try {\n await writeFile(target, JSON.stringify(CONFIG_TEMPLATE, null, 2) + \"\\n\", { flag: \"wx\" })\n } catch (e) {\n if ((e as NodeJS.ErrnoException).code === \"EEXIST\") {\n throw new PptfastError(`${target} already exists — edit it instead`)\n }\n throw e\n }\n return `wrote ${target} — themes: \\`pptfast themes\\`, style schema: \\`pptfast schema --style\\``\n}\n\nexport interface PreviewOptions {\n cwd?: string\n /** `--html` (v0.3 W7 task 1, spec §7 workflow ⑤): also write a\n * self-contained `preview.html` alongside the per-slide SVG files —\n * every slide's already-rendered SVG inlined into one file (thumbnail\n * filmstrip + keyboard/click navigation, `buildPreviewHtml`,\n * `./preview-html.ts`) for a human (or an agent that can view HTML) to\n * flip through the whole deck at once instead of opening N separate SVG\n * files. Named `htmlOut` rather than `html` so `RenderOptions.draft`-style\n * option objects in this file all read as \"what to produce\", not\n * \"whether this is HTML\" (there is nothing else this bundle could be).\n * Known limitation (see `buildPreviewHtml`'s own doc comment,\n * `./preview-html.ts`): self-containment assumes every image asset is\n * local or already a `data:` URI — a remote `http(s):` asset src passes\n * through `resolveLocalAssets` untouched and lands in the bundle as a\n * live network reference, not an inlined file.\n *\n * Also gates the audit overlay (notes+preview wave, task 2): when set\n * and the deck has no placeholder page, `runPreview` runs `auditDeck`\n * (`../svg/audit/deck-audit.ts`) and embeds its findings and `checks`\n * into `preview.html` (per-page badges + a findings panel + a one-line\n * checks summary, `buildPreviewHtml`). A deck with any placeholder page\n * skips the audit entirely instead of running it partially — see\n * `runPreview`'s own doc comment for why. */\n htmlOut?: boolean\n}\n\n/**\n * `irPath` accepts a single IR/spec JSON file, a deck project directory, or\n * a bare deck name (same `loadDeckTarget` resolution `runRender` uses).\n * Preview never gates on placeholder pages either way (single-file or\n * dir-mode) — `renderSlideSvg` itself never calls the draft gate, spec §7:\n * preview always lets everything through — an agent iterating on a\n * partially-filled deck needs to see whatever page it just wrote without\n * every other still-empty page blocking it.\n *\n * Appends the same field-alias {@link normalizedNote} `runValidate`/\n * `runRender` print (W5 whole-branch review finding 3).\n *\n * `opts.htmlOut` reuses each slide's already-rendered SVG string\n * ({@link buildPreviewHtml}'s `slides[].svg`) rather than calling\n * `renderSlideSvg` a second time — the `.svg` file on disk and the copy\n * embedded in `preview.html` are then guaranteed byte-identical by\n * construction, not just by the renderer being deterministic.\n *\n * `opts.htmlOut` additionally runs `auditDeck` (notes+preview wave, task 2)\n * — but only when the deck has no placeholder page. `auditDeck` itself\n * silently skips a placeholder (`AuditReport.pagesSkipped`, nothing to audit\n * on an unfilled page) — running it over a deck that has some would produce\n * a *partial* report that still looks complete (zero findings reads as\n * \"clean\", not \"some pages were never checked\"), which is worse than not\n * running it at all. The plan's contract is the simpler \"any placeholder\n * present → skip the whole overlay, one-line notice instead\" — implemented\n * here as `hasPlaceholder`, and threaded into `buildPreviewHtml` as either\n * `findings` + `checks` (clean run) or `auditNote` (skipped), never both.\n * `checks` (`AuditReport.checks`, `../svg/audit/deck-audit.ts`) rides along\n * with `findings` on every clean run, not just a partial/findings-only one —\n * `buildPreviewHtml` renders it as its own one-line summary regardless of\n * `findings.length`, so a deck that audited clean because nothing was wrong\n * stays visually distinct from one that audited clean because the pixel\n * pass never ran (fix round, Important-1: this line was the task brief's\n * own scope for this wave and was missed in the first pass).\n */\nexport async function runPreview(irPath: string, outDir: string, opts: PreviewOptions = {}): Promise<string> {\n const cwd = opts.cwd ?? process.cwd()\n const [projectHit, userHit] = await Promise.all([findConfig(cwd), findUserConfig()])\n const { raw, baseDir } = await loadDeckTarget(irPath, cwd, projectHit, userHit)\n await applyDeckConfig(raw, { cwd, projectHit, userHit })\n const v = validateIr(raw)\n if (!v.ok) throw new PptfastError(`invalid IR:\\n${formatIssues(v.errors)}`)\n await resolveLocalAssets(v.ir!, baseDir)\n await mkdir(outDir, { recursive: true })\n const ir = v.ir!\n const svgs: string[] = []\n for (let i = 0; i < ir.slides.length; i++) {\n const svg = renderSlideSvg(ir, i)\n svgs.push(svg)\n const name = `${String(i + 1).padStart(3, \"0\")}-${ir.slides[i]!.type}.svg`\n await writeFile(join(outDir, name), svg)\n }\n const ok = `wrote ${ir.slides.length} SVG files to ${outDir}`\n const notes: string[] = []\n const aliasNote = normalizedNote(v.normalized)\n if (aliasNote) notes.push(aliasNote)\n if (opts.htmlOut) {\n const htmlPath = join(outDir, \"preview.html\")\n const hasPlaceholder = ir.slides.some((slide) => slide.placeholder)\n const auditReport = hasPlaceholder ? undefined : auditDeck(ir)\n const auditFindings = auditReport?.findings ?? []\n const html = buildPreviewHtml({\n title: ir.filename,\n slides: ir.slides.map((slide, i) => ({\n index: i,\n id: slide.id,\n type: slide.type,\n svg: svgs[i]!,\n placeholder: slide.placeholder,\n })),\n findings: auditFindings.map((f) => ({ page: f.page, slideId: f.slideId, code: f.code, message: f.message })),\n auditNote: hasPlaceholder\n ? \"audit overlay skipped — deck has unfilled placeholder pages; fill every page and re-run `pptfast preview --html` to see audit findings\"\n : undefined,\n checks: auditReport?.checks,\n })\n await writeFile(htmlPath, html)\n notes.push(`note: wrote self-contained preview to ${htmlPath}`)\n if (auditFindings.length > 0) {\n notes.push(`note: audit found ${auditFindings.length} finding${auditFindings.length === 1 ? \"\" : \"s\"} — see preview.html`)\n }\n }\n return notes.length > 0 ? `${ok}\\n${notes.join(\"\\n\")}` : ok\n}\n\nexport interface AssembleOptions {\n output?: string\n cwd?: string\n}\n\n/**\n * Rewrites every local (non-`data:`/non-`http(s)`) asset src so it keeps\n * resolving correctly when the assembled IR is written to `outDir`, a\n * different directory than the `deckDir` it was assembled from (`-o`\n * pointing outside the deck project, `runAssemble` below). `readDeckDir`'s\n * asset scan always produces a `deckDir`-relative src (`./deck-dir.ts`'s\n * `scanAssets` — always `assets/<file>`), so writing the IR anywhere else\n * unchanged would leave that src resolving against the *wrong* base the\n * next time this file is loaded (`loadDeckTarget`'s single-file branch\n * resolves relative asset srcs against the IR file's own directory, not\n * where it happened to be assembled from). Rebuilds `assets.images` rather\n * than mutating entries in place — the same \"never mutate a live IR's asset\n * map\" caution `readDeckDir` itself documents (`./deck-dir.ts`).\n */\nfunction withRewrittenAssetPaths(ir: PptxIR, deckDir: string, outDir: string): PptxIR {\n const images = Object.fromEntries(\n Object.entries(ir.assets.images).map(([id, asset]) => {\n if (asset.src.startsWith(\"data:\") || /^https?:\\/\\//.test(asset.src)) return [id, asset] as const\n return [id, { ...asset, src: relative(outDir, join(deckDir, asset.src)) }] as const\n }),\n )\n return { ...ir, assets: { images } }\n}\n\n/**\n * `pptfast assemble <dir|name>` (W5 task 5): resolve `target` (path or bare\n * deck name, `resolveDeckTarget`) → `readDeckDir` (spec + pages/ + assets/ →\n * IR, `./deck-dir.ts`) → write the assembled IR as pretty-printed JSON,\n * default `<deckDir>/deck.json` when `-o` is omitted. Deliberately does\n * *not* call `applyDeckConfig` — `assemble` materializes exactly what the\n * spec says plus each page's own auto-selected `layout` where the page file\n * left it implicit (`assembleDeck`'s own doc comment, W4 design decision\n * 10) — a portable IR file, self-contained down to which archetype each page\n * will render with. Theme/style overrides are `validate`/`render`/\n * `preview`'s job (each already applies the four-layer chain whether given\n * this same directory or the `deck.json` this command just wrote).\n *\n * `target` must resolve to an actual directory: a target that exists but\n * names a file gets a friendly `expected a deck project directory` error\n * right here rather than reaching `readDeckDir` and failing deeper, with a\n * confusing `ENOTDIR` message, trying to read `<file>/deck.spec.json`. A\n * target that does not exist *at all* is deliberately let through to\n * `readDeckDir` unchanged — its own missing-spec-file error already names\n * the expected layout, strictly more helpful than this shorter message.\n *\n * `-o` resolves against `cwd` (the same fix `resolveDeckTarget` already\n * needed — see that function's own doc comment) rather than the real\n * `process.cwd()`, so a caller that threads a custom `cwd` gets the output\n * where it actually asked for it. When the resolved output directory is not\n * `deckDir` itself, every local asset src is rewritten\n * ({@link withRewrittenAssetPaths}) to stay correct from the new location —\n * otherwise `assets/logo.png` (correct relative to `deckDir`) would silently\n * fail to resolve from wherever `-o` actually put the file.\n *\n * When the spec omitted `seed`, `readDeckDir` (via `assembleDeck`) generates\n * one deterministically and reports it as `generatedSeed` — surfaced here as\n * a suggestion to add it back to `deck.spec.json` for revision stability\n * (spec §5's seed-generation semantics). Never written automatically:\n * `assembleDeck` stays a pure function with no fs side effects, and silently\n * rewriting a file the user did not ask this command to touch would be a\n * worse surprise than asking them to paste one line in.\n *\n * `materializedLayoutCount` (also from `assembleDeck`, unset when every page\n * already named its own `layout` or landed on the image-cover bypass) gets\n * its own one-line note the same way, listed after the seed note when both\n * apply — purely informational, telling the caller how many pages just had\n * an auto-pick baked into `deck.json` rather than leaving them to notice by\n * diffing the file. The base summary line's `(N slides, M placeholders)`\n * parenthetical itself stays untouched by either note (`scripts/e2e.mts`\n * checks it by exact substring) — both notes are strictly additional lines.\n */\nexport async function runAssemble(target: string, opts: AssembleOptions = {}): Promise<string> {\n const cwd = opts.cwd ?? process.cwd()\n const [projectHit, userHit] = await Promise.all([findConfig(cwd), findUserConfig()])\n const dir = await resolveDeckTarget(target, resolveDecksDirSource(projectHit, userHit), cwd)\n if ((await pathExists(dir)) && !(await isDeckDirectory(dir))) {\n throw new PptfastError(`expected a deck project directory: ${dir}`)\n }\n const { ir, generatedSeed, materializedLayoutCount, deckDir } = await readDeckDir(dir)\n const outPath = opts.output ? resolve(cwd, opts.output) : join(deckDir, \"deck.json\")\n const outDir = dirname(outPath)\n const outIr = outDir === deckDir ? ir : withRewrittenAssetPaths(ir, deckDir, outDir)\n await mkdir(outDir, { recursive: true })\n await writeFile(outPath, JSON.stringify(outIr, null, 2) + \"\\n\")\n const placeholderCount = outIr.slides.filter((s) => s.placeholder).length\n const summary = `wrote ${outPath} (${outIr.slides.length} slides, ${placeholderCount} placeholder${placeholderCount === 1 ? \"\" : \"s\"})`\n const notes: string[] = []\n if (generatedSeed !== undefined) {\n notes.push(`note: generated seed ${generatedSeed} — add \"seed\": ${generatedSeed} to deck.spec.json for revision stability`)\n }\n if (materializedLayoutCount !== undefined) {\n notes.push(\n `note: ${materializedLayoutCount} layout${materializedLayoutCount === 1 ? \"\" : \"s\"} auto-selected into deck.json — pin \"layout\" in a page file to lock one`,\n )\n }\n return [summary, ...notes].join(\"\\n\")\n}\n\n/**\n * `pptfast disassemble <deck.json> -o <dir>` (W5 task 5): the CLI shell for\n * `disassembleDeck` (`../plan/assemble.ts`) — read + validate an IR file the\n * same way `runRender`/`runValidate` do, then write `deck.spec.json` +\n * `pages/<id>.json` for every non-placeholder page. Pretty-printed. Key\n * order is already stable because `disassembleDeck` builds every object\n * with the same fixed field order on every call, not by iterating the\n * input, so there is no separate \"stable stringify\" step to write. Refuses\n * to overwrite an existing `deck.spec.json` — same `wx`-flag EEXIST guard as\n * `runInit`'s config scaffold — so re-running this command never silently\n * clobbers a deck project someone has since started filling in. Page files\n * are freely (re)written since they only exist because this same command\n * produced them, and written concurrently (`Promise.all`) since each is an\n * independent file.\n *\n * Also materializes `assets/` ({@link writeDeckAssets}, `./deck-dir.ts`) —\n * `disassembleDeck` itself never touches `ir.assets.images` (see that\n * function's own doc comment for the full accounting), so this is the step\n * that actually closes the loop: without it, an image deck disassembles\n * with every `asset_id` reference intact but no bytes behind it, then\n * re-assembles and renders with the image silently missing.\n *\n * The summary never claims to have written a directory it did not create:\n * `pagesDir`/`assetsDir` are only named when at least one page/asset file\n * actually landed there (a spec-only deck with every slide a placeholder,\n * or an assetless deck, leaves either directory unwritten).\n *\n * Every page id is checked with {@link assertSafeFileSegment} (`./deck-dir.ts`)\n * before *any* file is written — not just ahead of `pages/<id>.json` (W5\n * whole-branch review finding 1, CRITICAL — CWE-22), but ahead of\n * `deck.spec.json` too (post-v0.3 W8 fix round, backlog item 8,\n * `.issues/notes/2026-07-18-post-v03-backlog.md` #8 — the check originally\n * ran after the spec write): `slide.id` is an unrestricted string at the\n * schema layer, so a hand-authored IR could otherwise set one to\n * `\"../../../../escape\"` and write outside `outDir`. `writeDeckAssets` below\n * (`./deck-dir.ts`) carries the matching check for asset keys, inside\n * `writeOneAsset` — that check stays per-asset rather than also moving\n * ahead of the spec write, since an unsafe id is only one of several ways\n * `writeOneAsset` can fail (malformed data URI, URL asset, unreadable local\n * file) and the others can't be front-loaded without doing the write itself.\n *\n * Failure rollback (post-v0.3 W8 fix round, backlog item 8): once\n * `deck.spec.json` is written, this call is the sole owner of that file for\n * the rest of its own execution, so any failure in the page/asset writes\n * below deletes it before rethrowing — a failed run never leaves a\n * `deck.spec.json` behind that doesn't match what actually landed in\n * `pages/`/`assets/`. The `wx` no-overwrite guard above still runs first and\n * throws before this rollback scope is ever entered, so a pre-existing\n * `deck.spec.json` this call did not itself create is never at risk of\n * being deleted — deleting only ever targets the file this same invocation\n * just wrote.\n */\nexport async function runDisassemble(irPath: string, outDir: string): Promise<string> {\n const raw = await loadIrFile(irPath)\n const v = validateIr(raw)\n if (!v.ok) throw new PptfastError(`invalid IR:\\n${formatIssues(v.errors)}`)\n const { spec, pages } = disassembleDeck(v.ir!)\n\n // W5 whole-branch review finding 1 (CRITICAL, CWE-22): `id` is `slide.id`\n // off the parsed input IR (`disassembleDeck` passes a bare `slide.id`\n // through unchanged when present, `../plan/assemble.ts`) — unrestricted at\n // the schema layer, so an id like `\"../../../../escape\"` would otherwise\n // write outside `outDir`. Post-v0.3 W8 fix round (backlog item 8): checked\n // here, ahead of every write including `deck.spec.json` itself, so a\n // single unsafe id fails the whole call with nothing written at all,\n // rather than leaving a `deck.spec.json` that then needs rolling back.\n const ids = Object.keys(pages)\n for (const id of ids) assertSafeFileSegment(id, \"slide id\")\n\n const specPath = join(outDir, \"deck.spec.json\")\n await mkdir(outDir, { recursive: true })\n try {\n await writeFile(specPath, JSON.stringify(spec, null, 2) + \"\\n\", { flag: \"wx\" })\n } catch (e) {\n if ((e as NodeJS.ErrnoException).code === \"EEXIST\") {\n throw new PptfastError(`${specPath} already exists — refusing to overwrite an existing deck project`)\n }\n throw e\n }\n\n // From here on `specPath` is a file this call just created (the `wx` flag\n // above guarantees no pre-existing file survived to this point), so it is\n // safe to delete on any failure below — backlog item 8: a mid-way failure\n // used to leave `deck.spec.json` on disk with no matching pages/assets,\n // misrepresenting the deck project as already, successfully disassembled.\n const pagesDir = join(outDir, \"pages\")\n try {\n if (ids.length > 0) {\n await mkdir(pagesDir, { recursive: true })\n await Promise.all(\n ids.map((id) => {\n const content: PageContent = pages[id]!\n return writeFile(join(pagesDir, `${id}.json`), JSON.stringify(content, null, 2) + \"\\n\")\n }),\n )\n }\n\n const { count: assetCount, assetsDir } = await writeDeckAssets(\n v.ir!.assets.images,\n outDir,\n dirname(resolve(irPath)),\n )\n\n const pagesNote =\n ids.length > 0\n ? `${ids.length} page file${ids.length === 1 ? \"\" : \"s\"} to ${pagesDir}`\n : \"no pages (every slide was a placeholder)\"\n const assetsNote = assetCount > 0 ? `, and ${assetCount} asset file${assetCount === 1 ? \"\" : \"s\"} to ${assetsDir}` : \"\"\n return `wrote ${specPath}, ${pagesNote}${assetsNote}`\n } catch (e) {\n // Best-effort cleanup: a failure to delete the spec file must never mask\n // the real failure `e` below, so its own error is swallowed, not thrown.\n await rm(specPath, { force: true }).catch(() => {})\n throw e\n }\n}\n\n// ── migrate ──────────────────────────────────────────────────────────────\n\n/**\n * `pptfast migrate <input> -o <output>` (spec §9.1/§9.2/§9.3, vocabulary-v4\n * rename, task 2): the one deterministic conversion surface for both\n * artifacts this rename touches. Dispatches purely on whether `<input>`\n * resolves to a directory ({@link isDeckDirectory}) — the same signal every\n * other deck-accepting command already uses to branch between single-file\n * and deck-project-directory mode:\n *\n * - a directory containing `deck.plan.json` → {@link runMigrateDeckDir}:\n * rewrites it to `deck.spec.json` per spec §9.2's field mapping\n * ({@link migrateDeckPlanToSpec}, `../plan/migrate.ts`), written to\n * `<output>` (a directory — `<output>/deck.spec.json`).\n * - a file → {@link runMigrateIrFile}: must be an IR v3 document\n * (`version: \"3\"`), wraps {@link migrateIrV3ToV4} (`../ir/migrate.ts`),\n * written to `<output>` (a file). IR v2 is explicitly not accepted here\n * (spec §15.3: \"v2 无真实用户\" — `pptfast migrate` only supports v3→v4,\n * `validateIr`'s own v2 hard-reject message carries the full v2→v4\n * combined mapping for a caller who needs to convert one by hand).\n *\n * Both branches never overwrite `<output>` — a pre-existing file at the\n * resolved output path is a hard `PptfastError`, the same `wx`-flag EEXIST\n * guard `runDisassemble`/`runInit` already use elsewhere in this file (spec\n * §9.2: \"迁移工具必须默认写到新目标,不覆盖原文件\"). Neither branch runs a\n * model or reinterprets content — both are thin CLI shells over an\n * already-pure mapping function, per spec §9.3: \"只做已声明的结构映射,不\n * 运行模型,不重写内容,不重新选择 layout\".\n */\nexport async function runMigrate(input: string, output: string, cwd = process.cwd()): Promise<string> {\n const resolvedInput = resolve(cwd, input)\n if (await isDeckDirectory(resolvedInput)) {\n return runMigrateDeckDir(resolvedInput, output, cwd)\n }\n return runMigrateIrFile(resolvedInput, output, cwd)\n}\n\n/**\n * Deck-project-directory leg of {@link runMigrate}: reads `deck.plan.json`\n * out of `dir` (`loadIrFile`'s generic read-plus-parse, `./load-ir.ts` —\n * same helper `runSpecValidate` above uses, \"plan\" naming its own failure\n * messages), maps it through {@link migrateDeckPlanToSpec}\n * (`../plan/migrate.ts`, spec §9.2's field mapping), and writes the result\n * to `<output>/deck.spec.json`. `pages/*.json` and `assets/*` are untouched\n * — spec §9.2's mapping only touches `deck.plan.json`'s own top-level\n * `scenario` field and each page's `rhythm` field, both entirely absent from\n * the pages/assets directories.\n *\n * Deliberately does not delete or rename the source `deck.plan.json` —\n * spec §9.2: \"不覆盖原文件\" applies to the migration direction generally,\n * and leaving the old file in place is what lets {@link readSpecFile}-style\n * dual-file detection (`../cli/deck-dir.ts`) catch a half-finished migration\n * (both files present) instead of one command silently deciding the old\n * file is now garbage. The success message tells the caller to delete it\n * once they have confirmed the new file is correct.\n *\n * Checks for `deck.plan.json` up front (task 3, routed from task 2's\n * review) instead of letting a missing file fall through to\n * `loadIrFile`'s generic \"cannot read plan file\" — a directory that has\n * already been migrated (a `deck.spec.json` sitting there with no\n * `deck.plan.json` left to convert, the plan file having since been\n * deleted per this function's own success message) gets a dedicated\n * \"already migrated\" error instead of a message that reads like the\n * directory was never a deck project at all. A directory with neither file\n * still reaches `loadIrFile`'s generic error — this function has no more\n * specific diagnosis to offer than that one already gives.\n */\nasync function runMigrateDeckDir(dir: string, output: string, cwd: string): Promise<string> {\n const planPath = join(dir, PLAN_FILENAME)\n const sourceSpecPath = join(dir, SPEC_FILENAME)\n if (!(await pathExists(planPath)) && (await pathExists(sourceSpecPath))) {\n throw new PptfastError(\n `${dir} has ${SPEC_FILENAME} but no ${PLAN_FILENAME} — this deck project is already migrated, nothing to do`,\n )\n }\n const raw = await loadIrFile(planPath, \"plan\")\n const migrated = migrateDeckPlanToSpec(raw)\n const outDir = resolve(cwd, output)\n const specPath = join(outDir, SPEC_FILENAME)\n await mkdir(outDir, { recursive: true })\n try {\n await writeFile(specPath, JSON.stringify(migrated, null, 2) + \"\\n\", { flag: \"wx\" })\n } catch (e) {\n if ((e as NodeJS.ErrnoException).code === \"EEXIST\") {\n throw new PptfastError(`${specPath} already exists — refusing to overwrite, delete it first or choose a different -o`)\n }\n throw e\n }\n return `wrote ${specPath} — run \\`pptfast spec validate ${specPath}\\` to confirm it, then delete ${planPath} (a directory with both files present is rejected)`\n}\n\n/**\n * Single-file leg of {@link runMigrate}: requires an explicit `version: \"3\"`\n * (spec §9.3: \"IR v4 入口遇到 v3 时硬拒绝\" is `validateIr`'s own job — this\n * command instead requires v3 *specifically*, since v3 is the only version\n * it knows how to convert). `version: \"2\"` gets its own message pointing at\n * `validateIr`'s existing combined v2→v4 mapping rather than silently\n * routing it through the v3 vocabulary as a stepping stone (spec §15.3:\n * \"v2 无真实用户\", \"`pptfast migrate` 只支持 v3→v4,不接 v2\"). Any other\n * version (already v4, or missing/malformed entirely) is rejected with a\n * message naming what this command does accept.\n */\nasync function runMigrateIrFile(filePath: string, output: string, cwd: string): Promise<string> {\n const raw = await loadIrFile(filePath)\n const version = typeof raw === \"object\" && raw !== null ? (raw as Record<string, unknown>).version : undefined\n if (version === \"2\") {\n throw new PptfastError(\n \"pptfast migrate does not support IR v2 (spec §15.3: v2 has no real users) — run `pptfast validate` on the v2 file to see the full v2→v4 combined field mapping and rewrite it by hand\",\n )\n }\n if (version !== \"3\") {\n throw new PptfastError(\n `pptfast migrate only converts an IR v3 file (version: \"3\") or a deck project directory containing ${PLAN_FILENAME} — got version ${JSON.stringify(version)} in ${filePath}`,\n )\n }\n const parsed = PptxIRV3Schema.safeParse(raw)\n if (!parsed.success) {\n const detail = parsed.error.issues.map((issue) => `${issue.path.join(\".\") || \"(root)\"}: ${issue.message}`).join(\"\\n\")\n throw new PptfastError(`invalid IR v3 file ${filePath}:\\n${detail}`)\n }\n const migrated = migrateIrV3ToV4(parsed.data)\n const outPath = resolve(cwd, output)\n await mkdir(dirname(outPath), { recursive: true })\n try {\n await writeFile(outPath, JSON.stringify(migrated, null, 2) + \"\\n\", { flag: \"wx\" })\n } catch (e) {\n if ((e as NodeJS.ErrnoException).code === \"EEXIST\") {\n throw new PptfastError(`${outPath} already exists — refusing to overwrite, delete it first or choose a different -o`)\n }\n throw e\n }\n return `wrote ${outPath} (migrated IR v3 → v4)`\n}\n","import { readFile } from \"node:fs/promises\"\nimport { dirname, join, resolve } from \"node:path\"\nimport { z } from \"zod\"\nimport { PptfastError } from \"../errors\"\nimport { StyleOverrideSchema } from \"../ir\"\nimport { userConfigPath } from \"./home\"\n\n/**\n * Project-level deck defaults. Precedence (spec §7's four-layer chain, W5\n * task 5): CLI flag > project config (this schema, cwd walk-up) > user\n * config (`UserConfigSchema` below) > whatever the artifact itself already\n * carries (an authored IR's own `theme`, or the schema's own \"consulting\"\n * default when nothing anywhere sets one) — see `commands.ts`'s\n * `applyDeckConfig` for where all four layers actually get merged. `theme`\n * is kept an open string at this schema layer (mirrors `ThemeSchema` in\n * `ir/index.ts`) on purpose: `readConfigFile` below no longer checks it\n * against the installed set at read time — a config file's theme value\n * might sit behind a CLI flag or another layer that never actually gets\n * used, so rejecting it here would hard-fail a command over a value that\n * was never going to apply. `applyDeckConfig` runs that check once, at\n * resolution time, against whichever layer's value actually wins the chain\n * — the same \"unknown → PptfastError with the available list\" UX as\n * `validateIr`, just applied to the resolved value instead of unconditionally\n * to every layer.\n *\n * `decksDir` (W5 task 6, spec §7: a team that wants deck project\n * directories checked into the repo instead of living under\n * `~/.pptfast/decks` declares it here): a relative value resolves against\n * *this config file's own directory* (wherever `findConfig`'s cwd walk-up\n * found it) — never the CLI's cwd, and never `pptfastHome()`. Wins over the\n * user config's own `decksDir` (`UserConfigSchema` below) when both are\n * set, same project-beats-user precedence as `theme`/`style` above. The two\n * layers resolve against different bases, so this schema alone can't\n * express the final answer — `commands.ts`'s `resolveDecksDirSource`\n * computes the already-resolved absolute path before handing it down to\n * `./deck-dir.ts`'s `resolveDeckTarget` / `./home.ts`'s `decksRoot`, neither\n * of which knows there are two possible bases, only the final one.\n */\nconst ConfigSchema = z\n .object({\n theme: z.string().optional(),\n style: StyleOverrideSchema.optional(),\n decksDir: z.string().optional(),\n })\n .strict()\n\nexport type PptfastConfig = z.infer<typeof ConfigSchema>\n\n/**\n * User-level config schema (spec §7's four-layer chain — the layer between\n * project config and the artifact's own value): the same three deck-default\n * fields as {@link ConfigSchema} (`theme`/`style`/`decksDir`) — `decksDir`\n * is no longer project-config-free as of W5 task 6 (see {@link ConfigSchema}'s\n * own doc comment on that field), but the two layers still resolve it\n * against different bases: this user layer always resolves against\n * `pptfastHome()` (`./home.ts`'s `decksRoot`, this layer's one fixed\n * location), the project layer against the project config file's own\n * directory. Declared as its own flat object literal rather than\n * `ConfigSchema.extend(...)` — a shape this small is not worth taking on\n * zod's extend-then-restrict chaining, and it keeps both schemas readable\n * independently.\n *\n * `decksDir`: a relative value resolves against this config file's own\n * directory (`./home.ts`'s `pptfastHome()` — the only directory a user\n * config can ever live in, see `decksRoot`), never the CLI's cwd. No tilde\n * expansion — a literal `~/decks` is the literal relative path segment\n * `~/decks` under that base, not the home directory. The resulting (almost\n * certainly missing) directory surfaces through whatever downstream error\n * reads it, same as any other bad path.\n */\nconst UserConfigSchema = z\n .object({\n theme: z.string().optional(),\n style: StyleOverrideSchema.optional(),\n decksDir: z.string().optional(),\n })\n .strict()\n\nexport type UserPptfastConfig = z.infer<typeof UserConfigSchema>\n\nexport const CONFIG_FILENAME = \"pptfast.config.json\"\n\n/**\n * Shared read+parse+validate body for both config layers (project and user)\n * — same failure posture either way: a missing file is `null` (\"fine, no\n * config at this level\"), invalid JSON or a failed schema parse is a hard\n * {@link PptfastError} naming `path`. Deliberately does *not* check `theme`\n * against the installed set here — see {@link ConfigSchema}'s own doc\n * comment for why that moved to `applyDeckConfig` (`../cli/commands.ts`) at\n * resolution time instead, applied only to whichever layer's value actually\n * wins the four-layer chain.\n */\nasync function readConfigFile<T>(\n path: string,\n schema: z.ZodType<T>,\n): Promise<{ path: string; config: T } | null> {\n let text: string\n try {\n text = await readFile(path, \"utf8\")\n } catch {\n return null // no config at this level\n }\n let raw: unknown\n try {\n raw = JSON.parse(text) as unknown\n } catch (e) {\n throw new PptfastError(`${path} is not valid JSON: ${(e as Error).message}`)\n }\n const r = schema.safeParse(raw)\n if (!r.success) {\n const detail = r.error.issues\n .map((i) => `${i.path.join(\".\") || \"(root)\"}: ${i.message}`)\n .join(\"\\n\")\n throw new PptfastError(`invalid ${path}:\\n${detail}`)\n }\n return { path, config: r.data }\n}\n\n/** Walk from startDir up to the filesystem root looking for pptfast.config.json.\n * Invalid config is a hard error (with the file path in the message), never silently ignored. */\nexport async function findConfig(\n startDir: string,\n): Promise<{ path: string; config: PptfastConfig } | null> {\n let dir = resolve(startDir)\n for (;;) {\n const hit = await readConfigFile(join(dir, CONFIG_FILENAME), ConfigSchema)\n if (hit) return hit\n const parent = dirname(dir)\n if (parent === dir) return null\n dir = parent\n }\n}\n\n/**\n * User-level config (spec §7's four-layer chain, the layer below project\n * config): a single fixed path (`userConfigPath()`, `./home.ts` —\n * `$PPTFAST_HOME` or `~/.pptfast`), no cwd walk-up — there is exactly one\n * user config, unlike project config which can live at any ancestor of cwd.\n * Same missing/invalid posture as {@link findConfig}: missing file is fine\n * (`null`), invalid JSON or schema is a hard {@link PptfastError} with the\n * path.\n */\nexport async function findUserConfig(): Promise<{ path: string; config: UserPptfastConfig } | null> {\n return readConfigFile(userConfigPath(), UserConfigSchema)\n}\n","import { homedir } from \"node:os\"\nimport { join, resolve } from \"node:path\"\n\n/**\n * Root directory for pptfast's user-level state — deck project defaults\n * (`decksRoot`) and the user config file (`userConfigPath`), spec §7's\n * storage-policy decision. `PPTFAST_HOME` overrides it wholesale (CI /\n * containers). Otherwise a single predictable dotdir under the user's home,\n * the same posture as `.ssh`/`.npmrc`/`.aws`/`~/.claude` — deliberately\n * *not* the per-OS XDG/AppData split an `env-paths`-style helper would give:\n * deck project directories are large working files an agent produces, not\n * roaming-synced app config, and this tool's users (developers and agents)\n * benefit more from one predictable path than from OS-idiomatic placement.\n * Read fresh on every call (never cached) — `PPTFAST_HOME` is meant to be\n * redirectable per-process (tests set it via `process.env` before calling).\n */\nexport function pptfastHome(): string {\n return process.env.PPTFAST_HOME ?? join(homedir(), \".pptfast\")\n}\n\n/**\n * Default parent directory for bare-name deck resolution\n * (`$PPTFAST_HOME/decks/<name>/`, `./deck-dir.ts`'s `resolveDeckTarget`).\n * `config` is deliberately a minimal structural shape (`{ decksDir?: string\n * }`), not `UserPptfastConfig` itself — `./config.ts` already imports\n * `userConfigPath` from this module, so importing its type back here would\n * be circular. Redirecting `decksDir` is a user-identity concern (spec §7:\n * user-identity-class config belongs to the user layer) — a team that wants\n * deck projects tracked inside a repo instead reaches for project-level\n * `pptfast.config.json`, a separate, unrelated mechanism.\n *\n * A relative `decksDir` resolves against `pptfastHome()` itself — the only\n * directory a user config file can ever live in (see `userConfigPath`\n * below) — never the CLI's cwd. An absolute value passes through unchanged\n * (`path.resolve`'s own semantics handle both in one call, no separate\n * `isAbsolute` branch needed). No tilde expansion: a literal `~/decks` is\n * one relative path segment, not shorthand for the home directory — see\n * `./config.ts`'s `UserConfigSchema` doc comment.\n */\nexport function decksRoot(config?: { decksDir?: string }): string {\n return resolve(pptfastHome(), config?.decksDir ?? \"decks\")\n}\n\n/** Path to the user-level config file (theme/style defaults + `decksDir` redirect, spec §7's four-layer chain). */\nexport function userConfigPath(): string {\n return join(pptfastHome(), \"config.json\")\n}\n","/**\n * Deck project directory fs shell (spec §7's \"deck project directory\"\n * scheme, W5 task 5). Everything here touches disk — the pure half\n * (locked-field injection, placeholder/orphan semantics) lives in\n * `../plan/assemble.ts`'s `assembleDeck`, zero-fs by design (`AGENTS.md`'s\n * layout rule: this module is the *only* place that reads `deck.spec.json`\n * / `pages/*.json` / `assets/*` off disk and calls straight through to it,\n * the same posture `./load-ir.ts` already holds for a single IR file). It\n * is also the only place that *writes* `assets/*`, on the disassemble side\n * ({@link writeDeckAssets}) — the mirror image of {@link scanAssets} below,\n * and the CLI-shell half of `disassembleDeck`'s otherwise-lossy asset\n * handling (see that function's own doc comment in `../plan/assemble.ts`).\n *\n * Directory layout (spec §6/§7 — the locked artifact renamed from\n * `deck.plan.json` to `deck.spec.json`, vocabulary-v4 rename, task 2):\n * ```\n * my-deck/\n * deck.spec.json the locked spec — page order's sole source of truth\n * pages/<page-id>.json one file per filled page, content only (no type/heading)\n * assets/ local images, auto-registered by filename\n * ```\n *\n * A directory carrying the pre-rename `deck.plan.json` only (no\n * `deck.spec.json` yet) is no longer read directly — `pptfast migrate\n * <dir> -o <dir>` (`./commands.ts`'s `runMigrate`) converts it in place per\n * spec §9.2's field mapping. A directory carrying *both* files at once is a\n * hard error ({@link readSpecFile} below) — spec §9.2: \"目录中同时出现\n * `deck.plan.json` 和 `deck.spec.json` 时应硬报错,不能猜测优先级\".\n */\nimport { copyFile, mkdir, readFile, readdir, stat, writeFile } from \"node:fs/promises\"\nimport { basename, extname, isAbsolute, join, relative, resolve } from \"node:path\"\nimport { PptfastError } from \"../errors\"\nimport { assembleDeck, type AssembleResult, type PageContent } from \"../plan/assemble\"\nimport { decksRoot } from \"./home\"\nimport { EXT_BY_MIME, loadIrFile } from \"./load-ir\"\n\n/** The pre-rename artifact name (vocabulary-v4 rename, spec §6/§9.2) — no\n * longer read directly by {@link readSpecFile}, but still needed to (a)\n * detect the dual-file hard-error case and (b) as the migrate command's own\n * read source (`./commands.ts`'s `runMigrate`). Both exported for that\n * second reason — `runMigrate` needs the exact same two filenames, and\n * duplicating the literal strings there would risk the two modules drifting\n * on spelling. */\nexport const PLAN_FILENAME = \"deck.plan.json\"\nexport const SPEC_FILENAME = \"deck.spec.json\"\nconst PAGES_DIRNAME = \"pages\"\nconst ASSETS_DIRNAME = \"assets\"\n\n// ── path-traversal safety (CWE-22 defense) ──────────────────────────────\n\n/**\n * Rejects an `id` that is unsafe to join into a page/asset file path (W5\n * whole-branch review finding 1, CRITICAL — reproduced by the reviewer\n * against both call sites below). `slide.id` and `assets.images` keys are\n * both open, unrestricted `z.string()` at the schema layer (`../ir/index.ts`\n * — no format rule there by design, cross-slide/id rules are `validateIr`'s\n * job, see `SlideSchema.id`'s own doc comment), so a hand-authored IR can set\n * either to anything, including `\"../../../../escape\"` — and both\n * {@link writeOneAsset} below and `runDisassemble`'s page write\n * (`./commands.ts`) join that value straight into a write path with no\n * check of their own before this task. Call this before building any path\n * from an id sourced off a parsed IR.\n *\n * A value `join()`'d as (a possibly-suffixed) single trailing path segment\n * can only ever escape `base` if it is itself absolute, contains a `/` or\n * `\\` separator (smuggling in extra segments, e.g. `\"../../../escape\"`), or\n * is exactly `\"..\"` (the one separator-free value that is still a traversal\n * on its own, e.g. when a sink appends an empty suffix) — those lexical\n * checks alone already make every call site in this file safe regardless of\n * what it joins `id` under. `relative(base, resolve(base, id))` escaping\n * `base` (starts with `\"..\"`, or is itself absolute) is checked too, as\n * defense-in-depth on top of the lexical checks, not a substitute for them —\n * `base` here is a fixed stand-in directory rather than either real sink's\n * actual `assetsDir`/`pagesDir`: the property under test (\"can this id ever\n * resolve outside whatever directory it's joined under\") is a function of\n * `id` alone once the lexical checks above hold, true for any base, so a\n * real caller-supplied base would add no extra precision — see this\n * function's own test suite for the two attack shapes this closes.\n *\n * `context` names the offending id's role (`\"slide id\"`, `\"asset id\"`) so\n * the thrown message points at which field was unsafe.\n */\nexport function assertSafeFileSegment(id: string, context: string): void {\n const safeBase = resolve(\"/pptfast-safe-base\")\n const rel = relative(safeBase, resolve(safeBase, id))\n const safe =\n !isAbsolute(id) && !id.includes(\"/\") && !id.includes(\"\\\\\") && id !== \"..\" && !rel.startsWith(\"..\") && !isAbsolute(rel)\n if (!safe) {\n throw new PptfastError(\n `${context} \"${id}\" is not a safe file name — ids used as page/asset file names must not contain path separators or \"..\"`,\n )\n }\n}\n\n// ── bare-name / path resolution ─────────────────────────────────────────\n\n/**\n * Returns true when `stat(path)` succeeds and names a directory — the\n * single source of truth every deck-accepting CLI command (`assemble`,\n * `disassemble`'s input is always a file so it never calls this, `validate`/\n * `render`/`preview`) uses to branch between single-file IR and deck-project\n * directory mode. A missing path (`ENOENT`) reads as \"not a directory\"\n * rather than propagating — the caller's next step (`loadIrFile` for the\n * single-file branch) already has its own readable \"cannot read\" error for\n * a path that turns out not to exist at all, and re-deriving that\n * distinction here would just duplicate it. Any *other* `stat` failure\n * (`EACCES`, `ENOTDIR` via a non-directory path segment, ...) rethrows\n * wrapped in {@link PptfastError} instead — silently reading a real\n * permission or filesystem problem as \"not a directory, try it as a single\n * IR file\" produces a strictly more confusing downstream error than\n * surfacing the actual failure here.\n */\nexport async function isDeckDirectory(path: string): Promise<boolean> {\n try {\n return (await stat(path)).isDirectory()\n } catch (e) {\n if ((e as NodeJS.ErrnoException).code === \"ENOENT\") return false\n throw new PptfastError(`cannot check ${path}: ${(e as Error).message}`)\n }\n}\n\n/**\n * true when `stat(path)` succeeds (file or directory, no distinction) — the\n * same ENOENT-vs-everything-else posture as {@link isDeckDirectory} just\n * above, factored out because `resolveDeckTarget` below needs plain\n * existence (a candidate can legitimately be a file *or* a directory) at\n * two different points, and `runAssemble` (`../cli/commands.ts`) needs it\n * once more, to tell \"target does not exist at all\" (the existing, detailed\n * `readDeckDir` error, expected-layout hint included) apart from \"target\n * exists but is not a directory\" (a friendlier, immediate error — see that\n * function).\n */\nexport async function pathExists(path: string): Promise<boolean> {\n try {\n await stat(path)\n return true\n } catch (e) {\n if ((e as NodeJS.ErrnoException).code === \"ENOENT\") return false\n throw new PptfastError(`cannot check path ${path}: ${(e as Error).message}`)\n }\n}\n\n/**\n * Path-vs-bare-name resolution (spec §7's CLI bare-name resolution): an\n * `arg` that contains a path separator, or that exists locally relative to\n * `cwd` (file *or* directory — a same-directory `deck.json` has no\n * separator but must still resolve as the obviously-intended local file,\n * not get redirected to the deck home), resolves against `cwd` and comes\n * back as a fully-resolved path in both cases — an explicit or\n * locally-resolvable path always wins over the bare-name interpretation,\n * but is never handed back unresolved: every downstream fs call (Node's\n * `readFile` et al.) always resolves a relative path against the process's\n * *real* `process.cwd()`, which only coincides with this function's `cwd`\n * parameter in production (`commands.ts` never passes one explicitly, so it\n * defaults to the real thing) — a test that exercises a different `cwd`\n * without an actual `process.chdir()` needs the already-resolved path back,\n * not the bare `arg`, or the caller's next fs call would silently resolve\n * against a completely different, real cwd. An absolute `arg` is unaffected\n * either way — `path.resolve` returns an absolute later segment as-is.\n *\n * Otherwise `arg` is treated as a deck name under `decksRoot(config)`\n * (`./home.ts` — `$PPTFAST_HOME/decks/<name>`, or a `decksDir` override) —\n * but only when that candidate actually exists. When *neither* the local\n * path nor the deck-home candidate exists, this returns the local\n * (cwd-resolved) path rather than the deck-home guess: a bare arg that was\n * actually a typo'd local filename (`pptfast validate typo.json`) should\n * have its eventual \"cannot read\" error name the file the user typed, not\n * an unrelated `~/.pptfast/decks/typo.json` path they never meant. `config`\n * is not either config layer's raw shape — it is the already-resolved\n * effective `decksDir` source (project `pptfast.config.json`'s own value,\n * spec §7's project-level escape hatch, W5 task 6, when it sets one, else\n * the user config's, `./config.ts`'s `UserPptfastConfig`) computed once by\n * the caller (`commands.ts`'s `resolveDecksDirSource`) and passed in here\n * already resolved to an absolute path when it came from the project layer\n * — this function itself has no reason to know there were ever two\n * possible layers or bases, only the final answer. The caller fetches both\n * config files at most once per command and passes them in rather than\n * this function reaching for either itself, so a command that already\n * needs one of them for other reasons (theme/style resolution) never reads\n * the same file twice.\n *\n * An empty or whitespace-only `arg` (W5 whole-branch review finding 4) is\n * rejected up front rather than silently resolving to `cwd` itself — without\n * this guard, `resolve(cwd, \"\")` returns `cwd` unchanged and `pathExists`\n * always finds it (a directory always exists), so the empty string would\n * otherwise quietly pass through as \"the target is cwd\", surfacing later as\n * a confusing missing-`deck.spec.json` error instead of naming the actual\n * problem (an empty target argument) up front.\n */\nexport async function resolveDeckTarget(\n arg: string,\n config?: { decksDir?: string },\n cwd: string = process.cwd(),\n): Promise<string> {\n if (arg.trim() === \"\") throw new PptfastError(\"deck target must not be empty\")\n if (arg.includes(\"/\") || arg.includes(\"\\\\\")) return resolve(cwd, arg)\n const local = resolve(cwd, arg)\n if (await pathExists(local)) return local\n const fallback = join(decksRoot(config), arg)\n return (await pathExists(fallback)) ? fallback : local\n}\n\n// ── deck.spec.json ──────────────────────────────────────────────────────\n\n/** The expected-layout block of {@link readSpecFile}'s missing-file error —\n * `padEnd`-aligned programmatically (not hand-counted spaces in a template\n * literal) so the three column widths can't silently drift out of line\n * when one of the three filename/`*_DIRNAME` constants above changes. */\nfunction expectedLayoutHint(): string {\n const rows: [string, string][] = [\n [SPEC_FILENAME, \"the locked spec (see `pptfast spec validate`)\"],\n [`${PAGES_DIRNAME}/<page-id>.json`, \"one file per filled page (missing pages become placeholders)\"],\n [`${ASSETS_DIRNAME}/`, \"optional local images\"],\n ]\n const width = Math.max(...rows.map(([name]) => name.length)) + 2\n return rows.map(([name, desc]) => ` ${name.padEnd(width)}${desc}`).join(\"\\n\")\n}\n\n/**\n * Reads `deck.spec.json` out of `dir` (vocabulary-v4 rename, task 2 —\n * this function used to read the pre-rename `deck.plan.json` directly; it\n * no longer does). Three failure shapes, each with its own message:\n *\n * - both `deck.plan.json` and `deck.spec.json` present — a hard error, spec\n * §9.2: \"目录中同时出现 `deck.plan.json` 和 `deck.spec.json` 时应硬报错,\n * 不能猜测优先级\" (\"hard error, never guess which one wins\"). Checked\n * before the missing-file branch below so a caller that just ran\n * `pptfast migrate <dir> -o <dir>` (which writes `deck.spec.json`\n * *alongside* the pre-existing `deck.plan.json`, never deleting it) gets\n * pointed at deleting the old file, not a generic \"not a deck project\"\n * message.\n * - only `deck.plan.json` present (no `deck.spec.json` yet) — this\n * directory predates the rename and is no longer read directly; the\n * message points at `pptfast migrate` instead of the generic missing-file\n * hint, since the fix here is a one-command conversion, not authoring a\n * fresh file from scratch.\n * - neither file present — the pre-existing \"friendlier message over\n * `loadIrFile`'s generic \"cannot read\"\" this function has always had: the\n * one failure a deck-directory caller is most likely to hit by typo or by\n * pointing at a directory that was never a deck project in the first\n * place, so the error spells out the expected layout and points at\n * `pptfast spec validate` rather than leaving the caller to guess.\n */\nasync function readSpecFile(dir: string): Promise<unknown> {\n const specPath = join(dir, SPEC_FILENAME)\n const planPath = join(dir, PLAN_FILENAME)\n const [specExists, planExists] = await Promise.all([pathExists(specPath), pathExists(planPath)])\n if (specExists && planExists) {\n throw new PptfastError(\n `both ${SPEC_FILENAME} and ${PLAN_FILENAME} exist in ${dir} — ambiguous, refusing to guess which one wins. Delete ${PLAN_FILENAME} once you have confirmed ${SPEC_FILENAME} is correct (\\`pptfast migrate\\` never deletes the source file it read)`,\n )\n }\n if (!specExists && planExists) {\n throw new PptfastError(\n `${dir} has ${PLAN_FILENAME} but no ${SPEC_FILENAME} — deck project directories now use ${SPEC_FILENAME}. Run \\`pptfast migrate ${dir} -o ${dir}\\` to convert it`,\n )\n }\n let text: string\n try {\n text = await readFile(specPath, \"utf8\")\n } catch (e) {\n if ((e as NodeJS.ErrnoException).code === \"ENOENT\") {\n throw new PptfastError(\n `no ${SPEC_FILENAME} in ${dir} — expected a deck project directory:\\n${expectedLayoutHint()}`,\n )\n }\n throw new PptfastError(`cannot read spec file: ${specPath}`)\n }\n try {\n return JSON.parse(text) as unknown\n } catch (e) {\n throw new PptfastError(`spec file ${specPath} is not valid JSON: ${(e as Error).message}`)\n }\n}\n\n// ── pages/<id>.json ──────────────────────────────────────────────────────\n\n/**\n * Reads every `pages/<id>.json` file into a `{ id: parsedContent }` record —\n * `id` is the filename sans `.json` (spec §7: one file per page, named by a\n * stable id). A missing `pages/` directory (`ENOENT`) is not an error, just\n * an empty record (a brand-new deck project with a spec and no filled pages\n * yet is exactly `assembleDeck`'s \"every page becomes a placeholder\" case)\n * — but `pages/` existing as something that cannot be read as a directory\n * (e.g. a file sitting where a directory was expected, `ENOTDIR`) is a real\n * problem and throws {@link PptfastError} naming the path, not silently\n * \"zero pages\" (same ENOENT-vs-everything-else posture as\n * {@link isDeckDirectory} above). Non-`.json` entries (a stray `.DS_Store`,\n * an editor swap file, a subdirectory) are silently skipped rather than fed\n * to `JSON.parse` — `.json` is the only declared file shape for this\n * directory, so anything else was never a page file to begin with, not a\n * malformed one. `pages` is deliberately typed `Record<string, unknown>`\n * here (not `Record<string, PageContent>`) — each value's actual shape is\n * checked by `assembleDeck` itself, the same `unknown`-until-validated\n * boundary its own doc comment describes. Entries are read concurrently\n * (`Promise.all`) — independent files, each writing its own `pages[id]` key,\n * nothing to race on.\n */\nasync function readPages(dir: string): Promise<Record<string, unknown>> {\n const pagesDir = join(dir, PAGES_DIRNAME)\n let entries: string[]\n try {\n entries = (await readdir(pagesDir, { withFileTypes: true }))\n .filter((entry) => entry.isFile() && extname(entry.name) === \".json\")\n .map((entry) => entry.name)\n } catch (e) {\n if ((e as NodeJS.ErrnoException).code === \"ENOENT\") return {}\n throw new PptfastError(`cannot read ${PAGES_DIRNAME}/ directory ${pagesDir}: ${(e as Error).message}`)\n }\n const pages: Record<string, unknown> = {}\n await Promise.all(\n entries.map(async (entry) => {\n const id = basename(entry, \".json\")\n pages[id] = await loadIrFile(join(pagesDir, entry), `page \"${id}\"`)\n }),\n )\n return pages\n}\n\n// ── assets/ ──────────────────────────────────────────────────────────────\n\n/**\n * Scans `assets/` and maps each file to an `assets.images` entry (spec §7's\n * assets-mapping rule): `id` is the filename sans extension, `src` is the\n * `assets/<filename>` path *relative to the deck directory* — resolved to\n * actual bytes later by the existing `resolveLocalAssets` (`./load-ir.ts`),\n * called with the deck directory as its base (see `commands.ts`). A missing\n * `assets/` directory (`ENOENT`) is zero assets, same as a missing `pages/`\n * above — anything else (`ENOTDIR`, a permission error, ...) throws\n * {@link PptfastError} naming the path rather than silently reading as \"no\n * assets here\" (see {@link readPages}'s own note on this). Dotfiles\n * (`.DS_Store` and friends — `extname` returns `\"\"` for these, so their\n * \"id\" would otherwise be the whole filename) are skipped: they are never a\n * legitimate image, and `resolveLocalAssets` inlines *every* registered\n * entry unconditionally, so a stray metadata file left registered would\n * fail the whole render with a confusing \"unsupported image format\" error\n * for an asset nothing in the deck ever references. Two files that\n * normalize to the same id (`logo.png` and `logo.jpg`) is a genuine\n * authoring ambiguity — same \"structural mismatch always errors\" posture as\n * `assembleDeck`'s orphan-page check — reported with both filenames so the\n * fix (rename one) is obvious.\n */\nasync function scanAssets(dir: string): Promise<Record<string, { src: string }>> {\n const assetsDir = join(dir, ASSETS_DIRNAME)\n let entries: string[]\n try {\n entries = (await readdir(assetsDir, { withFileTypes: true }))\n .filter((entry) => entry.isFile() && !entry.name.startsWith(\".\"))\n .map((entry) => entry.name)\n } catch (e) {\n if ((e as NodeJS.ErrnoException).code === \"ENOENT\") return {}\n throw new PptfastError(`cannot read ${ASSETS_DIRNAME}/ directory ${assetsDir}: ${(e as Error).message}`)\n }\n const images: Record<string, { src: string }> = {}\n const sourceFile = new Map<string, string>()\n for (const entry of entries) {\n const id = basename(entry, extname(entry))\n const previous = sourceFile.get(id)\n if (previous !== undefined) {\n throw new PptfastError(\n `${ASSETS_DIRNAME}/${previous} and ${ASSETS_DIRNAME}/${entry} both register image id \"${id}\" — rename one of the files`,\n )\n }\n sourceFile.set(id, entry)\n images[id] = { src: `${ASSETS_DIRNAME}/${entry}` }\n }\n return images\n}\n\n// ── readDeckDir ──────────────────────────────────────────────────────────\n\nexport interface DeckDirResult extends AssembleResult {\n /** Absolute path to the deck directory — the base `resolveLocalAssets`\n * should resolve this IR's (relative) asset paths against. */\n deckDir: string\n}\n\n/**\n * Reads a deck project directory end to end: spec + pages → `assembleDeck`\n * (locked-field injection, placeholder/orphan semantics — see that\n * function's own doc comment) → assets/ scan merged into the assembled IR's\n * `assets.images` (assemble first, then inject — a deck spec has no `assets`\n * field of its own, so there is never a pre-existing id for a scanned asset\n * to collide with, only the intra-`assets/`-directory collision\n * {@link scanAssets} itself guards against).\n *\n * The merge rebuilds `ir.assets` as a fresh object (`{ images: { ...,\n * ...images } }`) rather than assigning into `ir.assets.images` in place —\n * deliberately, not just style: `PptxIRSchema`'s `assets` field defaults to\n * a *static* object literal (`AssetsSchema.default({ images: {} })`,\n * `../ir/index.ts`), and a deck spec never sets its own `assets` (assembleDeck's\n * raw object omits the key entirely, same as every other field it lets the\n * schema default), so *every* assembled deck's `ir.assets.images` starts out\n * as that one schema-default object — zod does not deep-clone a static\n * default per parse, only the immediately-defaulted field itself, so nested\n * defaults below it (`images: {}` inside `AssetsSchema`'s own default) keep\n * one shared identity across unrelated parses. Mutating that shared object\n * in place (`ir.assets.images[id] = asset`, the first version of this\n * function) would silently register one deck project's local images onto\n * every other deck assembled in the same process — confirmed with a\n * standalone repro against this exact schema shape before writing this\n * comment. Rebuilding the object sidesteps the shared reference without\n * needing to touch the schema itself.\n *\n * The merged result is spliced in via a shallow clone of the whole `ir`\n * object (`{ ...ir, assets: ... }`), not a `ir.assets = ...` reassignment\n * onto the object `assembleDeck` returned (post-v0.3 W8 fix round, backlog\n * item 4, `.issues/notes/2026-07-18-post-v03-backlog.md` #4): the earlier\n * version mutated `assembleDeck`'s own return value in place, which is\n * harmless *today* only because `variety.ts`'s `deckSeedCache` and\n * `effective-layout.ts`'s `deckEffectiveLayoutIdsCache` — the only two\n * consumers that key a `WeakMap` off an `ir` object's identity — never read\n * `.assets` (confirmed by reading both cache-populating functions: they only\n * touch `seed`/`filename`/`theme.id`/`theme.style`/`narrative`/\n * `slides[].heading`/`.id`/`.type`/`.layout`/`.background`). Cloning\n * instead of mutating means the object\n * `assembleDeck` returned is never touched, and this function's own return\n * value is a distinct identity no earlier reference could have already\n * cached against — correct-by-construction regardless of what a future\n * cache keys on, not just correct because of what today's two caches happen\n * to skip.\n */\nexport async function readDeckDir(dir: string): Promise<DeckDirResult> {\n const deckDir = resolve(dir)\n const spec = await readSpecFile(deckDir)\n const pages = await readPages(deckDir)\n const { ir, generatedSeed, materializedLayoutCount } = assembleDeck(spec, pages as Record<string, PageContent>)\n const images = await scanAssets(deckDir)\n const merged = { ...ir, assets: { images: { ...ir.assets.images, ...images } } }\n return { ir: merged, generatedSeed, materializedLayoutCount, deckDir }\n}\n\n// ── assets/ (write direction — disassemble) ─────────────────────────────\n\nexport interface WriteDeckAssetsResult {\n /** Number of `ir.assets.images` entries materialized into `assets/`. */\n count: number\n /** Absolute path to the `assets/` directory written into — never created\n * (and this path never exists) when `count` is 0. */\n assetsDir: string\n}\n\n/**\n * Write direction of the assets/ concept — the mirror image of\n * {@link scanAssets} above, and the CLI-shell half of `disassembleDeck`'s\n * documented-lossy `assets` handling (`../plan/assemble.ts`'s own doc\n * comment on that function): that pure function never touches\n * `ir.assets.images` at all (its `{ spec, pages }` return has no `assets`\n * field), so without this step a disassembled directory would carry\n * `asset_id` references inside `pages/*.json` with nothing under `assets/`\n * backing them — exactly the \"image deck round-trips to a missing image\"\n * bug this function exists to close. Called by `runDisassemble`\n * (`../cli/commands.ts`) with the source IR's own `assets.images` map and\n * `sourceBaseDir` (the *input* IR file's own directory — the same base\n * `resolveLocalAssets`, `./load-ir.ts`, would resolve a relative local src\n * against at render time).\n *\n * Three source shapes, three outcomes (per entry, all independent —\n * written concurrently via `Promise.all`):\n * - `data:<mime>;base64,<payload>` → decoded and written to\n * `assets/<id><ext>`, `ext` looked up from `mime` via `EXT_BY_MIME`\n * (`./load-ir.ts`). An unrecognized mime or a non-base64 data URI is a\n * hard {@link PptfastError} naming the asset, not a silent skip.\n * - a local file path (relative resolves against `sourceBaseDir`, the same\n * `isAbsolute(src) ? src : resolve(base, src)` rule `resolveLocalAssets`\n * itself uses) → copied byte-for-byte into `assets/<id><origExt>`. An\n * unreadable source (moved/deleted/permission-denied since the IR was\n * generated) is a hard {@link PptfastError} naming the asset and the path\n * that could not be read.\n * - `http(s)://` → always a hard {@link PptfastError} — a URL asset has no\n * local bytes to write at all. The fix is on the deck author's side\n * (inline it as a data URI, or download it first), not something this\n * function can paper over.\n *\n * Written entries need no spec or page record of their own: `readDeckDir`'s\n * own {@link scanAssets} re-registers every file under `assets/` purely by\n * scanning the directory, the same way it would for a hand-added image —\n * this function's only job is making sure the bytes are there.\n */\nexport async function writeDeckAssets(\n images: Record<string, { src: string }>,\n outDir: string,\n sourceBaseDir: string,\n): Promise<WriteDeckAssetsResult> {\n const entries = Object.entries(images)\n const assetsDir = join(outDir, ASSETS_DIRNAME)\n if (entries.length === 0) return { count: 0, assetsDir }\n await mkdir(assetsDir, { recursive: true })\n await Promise.all(entries.map(([id, asset]) => writeOneAsset(id, asset.src, assetsDir, sourceBaseDir)))\n return { count: entries.length, assetsDir }\n}\n\n/** `data:<mime>;base64,<payload>` — the only data-URI shape any producer in\n * this codebase ever writes (`resolveLocalAssets`, `./load-ir.ts`, and the\n * sharp/canvas recode paths in `../platform/`) — matched strictly rather\n * than handling arbitrary charset params or non-base64 payloads nothing\n * here produces. */\nconst DATA_URI_RE = /^data:([^;,]+);base64,(.*)$/s\n\nasync function writeOneAsset(id: string, src: string, assetsDir: string, sourceBaseDir: string): Promise<void> {\n // W5 whole-branch review finding 1: one guard at the top covers both\n // write branches below (data-URI and local-file-copy) — `id` is the same\n // value regardless of which branch runs, so there is nothing branch-\n // specific about the check itself, only about what gets appended after it.\n assertSafeFileSegment(id, \"asset id\")\n if (src.startsWith(\"data:\")) {\n const match = DATA_URI_RE.exec(src)\n if (!match) {\n throw new PptfastError(`asset \"${id}\": only base64-encoded data URIs can be disassembled (malformed data URI)`)\n }\n const mime = match[1]\n const payload = match[2]\n const ext = EXT_BY_MIME[mime]\n if (!ext) {\n throw new PptfastError(\n `asset \"${id}\": cannot disassemble a data URI with mime \"${mime}\" — expected one of ${Object.keys(EXT_BY_MIME).join(\", \")}`,\n )\n }\n await writeFile(join(assetsDir, `${id}${ext}`), Buffer.from(payload, \"base64\"))\n return\n }\n if (/^https?:\\/\\//.test(src)) {\n throw new PptfastError(\n `asset \"${id}\": URL assets cannot be disassembled into a deck directory — inline it as a data URI or download it first`,\n )\n }\n const abs = isAbsolute(src) ? src : resolve(sourceBaseDir, src)\n try {\n await copyFile(abs, join(assetsDir, `${id}${extname(abs)}`))\n } catch {\n throw new PptfastError(`asset \"${id}\": cannot read source image ${abs} (from src \"${src}\") — cannot disassemble`)\n }\n}\n","import { readFile } from \"node:fs/promises\"\nimport { extname, isAbsolute, resolve } from \"node:path\"\nimport { PptfastError } from \"../errors\"\nimport type { PptxIR } from \"../ir\"\nimport { FORMAT_BY_MIME, MIME_BY_SNIFFED_FORMAT, sniffImageFormat } from \"../ir/asset-sniff\"\nimport { getPlatform } from \"../platform/registry\"\n\nconst MIME_BY_EXT: Record<string, string> = {\n \".png\": \"image/png\",\n \".jpg\": \"image/jpeg\",\n \".jpeg\": \"image/jpeg\",\n \".gif\": \"image/gif\",\n}\n\n/**\n * Mime → extension (with leading dot), one canonical extension per mime\n * (\"image/jpeg\" → \".jpg\", not \".jpeg\") — the reverse direction of\n * {@link MIME_BY_EXT} above, plus `image/webp`, which that table\n * deliberately omits: a local `.webp` file must keep taking the sharp\n * recode-to-png path in {@link resolveLocalAssets} below (see the e2e\n * \"webp asset regression leg\", `scripts/e2e.mts` — adding webp to\n * `MIME_BY_EXT` would silently skip that path for local files). This table\n * only serves the opposite direction — naming a file already decoded from a\n * `data:` URI (`writeDeckAssets`, `./deck-dir.ts`'s disassemble asset\n * materialization) — where a webp *payload* is a real possibility (e.g. an\n * upload-derived asset already embedded as webp) with no local file or\n * recode step involved at all.\n */\nexport const EXT_BY_MIME: Record<string, string> = {\n \"image/png\": \".png\",\n \"image/jpeg\": \".jpg\",\n \"image/gif\": \".gif\",\n \"image/webp\": \".webp\",\n}\n\n/** Read and JSON-parse a file with readable failure messages. `kind` names\n * what the file is expected to hold (e.g. \"spec\") for both failure\n * messages — defaults to \"IR\" for this function's original, still most\n * common caller (`runRender`/`runValidate`/`runPreview`, `./commands.ts`);\n * `runSpecValidate` passes \"spec\" so its own errors read correctly instead\n * of borrowing IR's wording for a file that was never one. */\nexport async function loadIrFile(irPath: string, kind = \"IR\"): Promise<unknown> {\n let text: string\n try {\n text = await readFile(irPath, \"utf8\")\n } catch {\n throw new PptfastError(`cannot read ${kind} file: ${irPath}`)\n }\n try {\n return JSON.parse(text) as unknown\n } catch (e) {\n throw new PptfastError(`${kind} file ${irPath} is not valid JSON: ${(e as Error).message}`)\n }\n}\n\n/**\n * Rewrite local file paths in assets.images to data URIs (CLI-only concern).\n * data: and http(s): sources pass through — the export pipeline inlines URLs\n * itself.\n *\n * Byte-level validation (borrow wave, Task 2 — D3, the local-file half of\n * `api.ts`'s `checkAssetBytes` doc comment — read that one for the full\n * rationale, this is its Node-only counterpart for the ingestion form\n * `validateIr` itself can't reach): every file's bytes are read once here\n * regardless of extension, then a zero-byte file is rejected loud\n * immediately (`dr/d-robustness.md`'s zero-byte-PNG probe — previously this\n * silently produced a 0-byte media part in the exported .pptx). For the four\n * extensions {@link MIME_BY_EXT} recognizes, the bytes are additionally\n * magic-byte-sniffed ({@link sniffImageFormat}) and checked against what the\n * extension claims: a corrupt/unrecognized header is rejected\n * (garbage-bytes-PNG probe), and so is a mismatch — a real PNG saved as\n * `.jpg` (the third D3 probe) — same reject-not-silently-relabel disposition\n * `checkAssetBytes` documents, for the same reason: an extension/content\n * mismatch would otherwise land in the exported package as a media part\n * whose declared type and actual bytes disagree, which `package-audit.ts`'s\n * structural rules never check. A file whose extension isn't one of those\n * four (webp and friends) skips the sniff/mismatch check and keeps taking\n * the `recodeImageToPng` path below unconditionally — sharp decodes by\n * content, not extension, so a mislabeled-but-decodable file there is\n * already harmless, and a genuinely corrupt one surfaces as sharp's own\n * decode error.\n */\nexport async function resolveLocalAssets(ir: PptxIR, baseDir: string): Promise<void> {\n for (const [name, asset] of Object.entries(ir.assets.images)) {\n const src = asset.src\n if (src.startsWith(\"data:\") || /^https?:\\/\\//.test(src)) continue\n const abs = isAbsolute(src) ? src : resolve(baseDir, src)\n let bytes: Buffer\n try {\n bytes = await readFile(abs)\n } catch {\n throw new PptfastError(`asset \"${name}\": cannot read image file ${abs} (from src \"${src}\")`)\n }\n if (bytes.length === 0) {\n throw new PptfastError(`asset \"${name}\": image file ${abs} is zero bytes — re-export or re-select the file`)\n }\n const ext = extname(abs).toLowerCase()\n const mime = MIME_BY_EXT[ext]\n if (mime) {\n const sniffed = sniffImageFormat(bytes)\n if (sniffed === null) {\n throw new PptfastError(\n `asset \"${name}\": image file ${abs} has a corrupt or unrecognized header (extension claims ${mime}) — re-export or re-select the file`,\n )\n }\n const expected = FORMAT_BY_MIME[mime]\n if (expected && sniffed !== expected) {\n throw new PptfastError(\n `asset \"${name}\": image file ${abs} is named \"${ext}\" but its bytes are actually ${MIME_BY_SNIFFED_FORMAT[sniffed]} — rename the file to match its real format, or re-export/re-save it as a genuine ${mime}`,\n )\n }\n asset.src = `data:${mime};base64,${bytes.toString(\"base64\")}`\n continue\n }\n const recode = getPlatform().recodeImageToPng\n if (!recode) {\n throw new PptfastError(\n `asset \"${name}\": unsupported image format \"${extname(abs)}\" — install sharp or convert to png/jpeg/gif`\n )\n }\n asset.src = await recode(`data:application/octet-stream;base64,${bytes.toString(\"base64\")}`)\n }\n}\n","/**\n * Pure string builder for `pptfast preview --html`'s self-contained review\n * bundle (v0.3 W7 task 1, spec §7 workflow ⑤): one `preview.html` with every\n * slide's already-rendered SVG (`renderSlideSvg`, `../api.ts`) inlined\n * directly into the markup, a bottom thumbnail filmstrip, keyboard (←/→) and\n * click navigation, and a page counter. No `fs` here on purpose — this\n * module only assembles a string; `runPreview` (`./commands.ts`) is the only\n * caller and the only place that touches disk, which keeps this file\n * trivially unit-testable (feed it slide data, assert on the returned\n * string) despite living under the Node-only `src/cli*` tree (AGENTS.md's\n * \"no Node-only deps\" layout rule is about `src/index.ts`'s own dependency\n * closure, not every file under `src/cli` — this one just happens to need\n * none anyway).\n *\n * Self-containment (the plan's hard requirement): every slide's SVG is\n * embedded as raw markup — never `<img src>`, never any other reference to\n * an external file — and the only CSS/JS in the document is inlined in\n * `<style>`/`<script>`. Local image assets are already `data:` URIs by the\n * time `runPreview` calls `renderSlideSvg` (`resolveLocalAssets`'s job,\n * `./load-ir.ts` — this module never touches assets itself) — *assuming*\n * every image asset the deck references is local or already a `data:` URI.\n * Known limitation: `resolveLocalAssets` deliberately passes a remote\n * `http(s):` asset `src` through untouched (the export pipeline inlines\n * those itself), so that src is left un-inlined and lands verbatim in this\n * bundle's embedded SVG as a live network reference, not a namespace URI —\n * breaking the zero-network-request guarantee for that one slide. Barring\n * that case, the only `http(s)` substrings that can appear anywhere in the\n * output are SVG namespace URIs (`xmlns=\"http://www.w3.org/2000/svg\"`,\n * emitted by `../svg/serialize.ts` on every slide) — XML namespace\n * identifiers, not network requests.\n *\n * Embed strategy (one `<svg>` per slide, not two): the thumbnail filmstrip\n * and the large \"stage\" view share the exact same DOM node per slide rather\n * than each holding its own copy — duplicating every slide's SVG (including\n * any inlined `data:` image payloads) would double the file's byte size for\n * an image-heavy deck for zero benefit, since only one size is ever on\n * screen for a given slide at a time. The one node that exists for a slide\n * lives in exactly one of two homes: `#pf-stage` (the slide currently being\n * viewed large) or its own `.pf-thumb-slot` (every other slide, shown small\n * in the filmstrip) — `<script>`'s `activate()` moves it between the two\n * with a plain `appendChild` (which detaches a node from its previous parent\n * automatically) when the viewer clicks a thumbnail or presses ←/→. Because\n * an inactive slide's badge travels with its node, and the active slide's\n * thumbnail button carries its own always-present badge, a placeholder page\n * still shows its \"unfilled\" mark in both places even though only one copy\n * of the slide's markup ever exists.\n *\n * Audit overlay + annotations (notes+preview wave, task 2): `buildPreviewHtml`\n * is still a pure renderer — `findings` (`../svg/audit/deck-audit.ts`'s\n * `AuditFinding`, reshaped locally as {@link PreviewHtmlFinding} so this file\n * still has no `../ir`/`../svg` import) and the placeholder-skip\n * {@link PreviewHtmlInput.auditNote} both arrive as plain input, the same way\n * `slides` already does; the caller (`runPreview`, `./commands.ts`) decides\n * *whether* to run `auditDeck` at all (skipped whenever the deck has any\n * placeholder page — a placeholder has nothing to audit and `auditDeck`\n * itself silently skips it, so surfacing a half-audited deck as if it were\n * clean would be misleading; the plan's contract is \"any placeholder present\n * → skip the whole overlay, one-line notice instead\"). Per-page finding\n * counts (thumbnail/stage badges) and the findings panel are rendered as\n * static markup at build time, not computed by client-side JS from the\n * embedded JSON — `findings` is known up front here, so there is nothing for\n * the browser to compute; the embedded `<script type=\"application/json\"\n * id=\"pf-audit-findings\">` blob exists only so a saved `preview.html` still\n * carries the structured findings for later tooling, not to drive the UI.\n * User content still flows through {@link escapeHtml} everywhere it lands in\n * HTML (a finding's `message` embeds a truncated quote of the offending\n * slide's own text) — the JSON blob instead goes through {@link embedJson},\n * which additionally neutralizes any literal `</script` sequence a slide's\n * text could contain (escaping every `<` to its unicode escape — valid\n * inside a JSON string, and the only character the HTML tokenizer would\n * otherwise use to end the `<script>` element early), the standard technique\n * for safely inlining untrusted JSON into a script tag.\n *\n * Annotations are a pure client-side, in-memory feature (no `fs`, no network\n * — this module builds a static page, `<script>`'s own closure holds the\n * state) keyed by each slide's 0-based array index, not by its `pageId` —\n * the id/index duality only matters at *export* time (`pageIdFor()` in\n * `JS` below derives `slide.id` when the active `.pf-slide` node carries a\n * `data-id`, else falls back to the 1-based page number — matching\n * `AuditFinding.page`'s own established \"1-based page number when there is\n * no slide id\" convention, `../svg/audit/deck-audit.ts`, rather than the\n * 0-based `data-index` this file otherwise uses internally). \"Export\n * revision requests\" reads the deck title back out of `#pf-title`'s already-\n * escaped `textContent` (browser-decoded HTML entities, exactly the original\n * `title` string) instead of embedding a second JS string literal for it —\n * one fewer thing that needs its own escaping discipline. The exported file\n * never touches disk or a server: `URL.createObjectURL` on an in-memory\n * `Blob` plus a synthetic `<a download>` click, the standard zero-backend\n * browser download pattern — keeping the self-containment invariant this\n * whole module exists to protect (no `fetch`, no `XMLHttpRequest`, no form\n * `action`).\n */\n\nexport interface PreviewHtmlSlideInput {\n /** Authoritative page number (1-based labels derive from this, not array\n * position, so a caller that filters/reorders `slides` still gets\n * correct output). */\n index: number\n /** Stable slide id (`slide.id`, `../ir/index.ts`'s `SlideSchema`) when the\n * deck sets one. User content — HTML-escaped wherever it is shown. */\n id?: string\n /** `slide.type` — `cover`/`chapter`/`content`/`ending` in practice, kept\n * as a plain `string` here so this module has no dependency on `../ir`. */\n type: string\n /** Already-rendered standalone SVG markup for this slide (`renderSlideSvg`,\n * `../api.ts`) — embedded verbatim: trusted, self-produced markup, never\n * escaped (escaping it would corrupt the SVG/XML syntax itself). */\n svg: string\n /** `slide.placeholder` — an unfilled page (assemble's stand-in for content\n * nobody has written yet, W5 task 1). Renders a visible \"unfilled\" badge:\n * this bundle exists for a human/agent visual review, so an unfilled page\n * must never look indistinguishable from a finished one. */\n placeholder?: boolean\n}\n\n/**\n * One `auditDeck` finding (`AuditFinding`, `../svg/audit/deck-audit.ts`),\n * reshaped to this module's own minimal fields only — dropping `detail`\n * (never shown) keeps this file dependency-free of `../svg` the same way it\n * is already dependency-free of `../ir` (see the module doc comment). `page`\n * is 1-based, matching `PreviewHtmlSlideInput.index + 1` for the slide it\n * belongs to (both ultimately trace back to the same `ir.slides` array\n * position in `runPreview`, `./commands.ts`). User content — `message`\n * embeds a truncated quote of the offending slide's own text — HTML-escaped\n * wherever it is shown, same as every other user-content field in this file.\n */\nexport interface PreviewHtmlFinding {\n page: number\n slideId?: string\n code: string\n message: string\n}\n\n/**\n * `AuditChecks` (`../svg/audit/deck-audit.ts`), reshaped locally the same\n * way `findings` is reshaped to {@link PreviewHtmlFinding} — keeps this file\n * free of a `../svg` import (see the module doc comment). Literal state\n * words only, mirroring the source type exactly: this wave's soul\n * constraint is \"not checked must never read as passed\", so `pixels` being\n * `\"not-requested\"` has to survive unchanged all the way into the rendered\n * line — no checkmark, no \"passed\"/\"ok\" substitute that could be misread as\n * a completed pixel pass.\n */\nexport interface PreviewHtmlChecks {\n svg: \"completed\"\n pixels: \"not-requested\" | \"completed\"\n}\n\nexport interface PreviewHtmlInput {\n /** Deck title (`ir.filename`) — shown in the `<title>` tag and the header.\n * User content — HTML-escaped wherever it is shown. */\n title: string\n slides: PreviewHtmlSlideInput[]\n /** `auditDeck(ir).findings` (`../svg/audit/deck-audit.ts`), reshaped to\n * {@link PreviewHtmlFinding} — omit or pass `[]` when the caller skipped\n * the audit (no findings to show at all, e.g. the deck has a placeholder\n * page, see {@link auditNote}) or the deck audited clean. Drives the\n * thumbnail/stage finding-count badges and the findings panel — see the\n * module doc comment for why those are rendered as static markup here\n * rather than computed by client-side JS from the embedded JSON blob. */\n findings?: PreviewHtmlFinding[]\n /** One-line notice shown in the header in place of any findings UI — the\n * plan's placeholder-skip contract: `runPreview` sets this (and passes no\n * `findings`) whenever the deck has any placeholder page, since\n * `auditDeck` itself silently skips a placeholder (nothing to audit) and\n * showing a placeholder-heavy deck as audit-clean would be misleading.\n * User content only in the sense that it is caller-supplied prose, not\n * deck content — HTML-escaped like everything else in this file\n * regardless. */\n auditNote?: string\n /** `auditDeck(...).checks` (`../svg/audit/deck-audit.ts`), reshaped to\n * {@link PreviewHtmlChecks} — omit whenever the caller skipped the audit\n * (the same condition {@link auditNote}/{@link findings} already use: a\n * deck with a placeholder page never calls `auditDeck` at all, so there\n * is nothing to report here either). Rendered as one line, independent of\n * `findings.length` — a clean 0-finding report and a report where\n * `pixels` never ran would otherwise look identical once the findings\n * panel is empty in both cases, and the whole point of surfacing `checks`\n * here is to keep that distinction visible even then. */\n checks?: PreviewHtmlChecks\n}\n\nfunction escapeHtml(s: string): string {\n return s\n .replace(/&/g, \"&amp;\")\n .replace(/</g, \"&lt;\")\n .replace(/>/g, \"&gt;\")\n .replace(/\"/g, \"&quot;\")\n .replace(/'/g, \"&#39;\")\n}\n\n/**\n * `JSON.stringify(value)`, with every `<` escaped to `<` — safe to\n * inline inside a `<script>` element's text content (this file's\n * `#pf-audit-findings` data blob): the HTML tokenizer looks only for the\n * literal byte sequence `</script` to end a script element, regardless of\n * the script's `type` or of what its content actually parses as, so a\n * finding `message` that happens to contain that substring (it embeds a\n * truncated quote of the offending slide's own text — user content) could\n * otherwise truncate the document early. `<` never appears in JSON outside a\n * string value (the syntax has no structural use for it), so this blanket\n * replace only ever touches characters that were already inside string\n * content — the standard technique for embedding untrusted JSON in a page.\n */\nfunction embedJson(value: unknown): string {\n return JSON.stringify(value).replace(/</g, \"\\\\u003c\")\n}\n\n/** `\"3\"` (a finding-count badge) or `\"\"` when the slide has no findings —\n * shared by {@link slideNode} (the moving node's own badge) and\n * {@link thumbButton} (the thumbnail's always-present one), same\n * \"one class for the node's own copy, one for the thumbnail button's\"\n * split the existing `pf-badge`/`pf-thumb-badge` pair already uses. In\n * practice a slide never carries both this badge and the \"unfilled\" one —\n * `runPreview` only ever passes non-empty `findings` when *no* slide in the\n * deck is a placeholder (the plan's skip-the-whole-overlay contract, see\n * `PreviewHtmlInput.auditNote`'s doc comment) — but nothing here assumes\n * that invariant: the two badges use different classes and corners (this\n * one top-left, \"unfilled\" top-right) specifically so a caller that did\n * pass both for one slide would still render two distinct, non-overlapping\n * marks rather than a garbled stack. */\nfunction findingBadge(count: number, className: string): string {\n if (count === 0) return \"\"\n return `<div class=\"${className}\" aria-hidden=\"true\">${count}</div>`\n}\n\n/** The one `.pf-slide` node this slide will ever have — moved between\n * `#pf-stage` and its `.pf-thumb-slot` by `<script>` at runtime, never\n * duplicated (see this module's own doc comment). Carries its own\n * \"unfilled\" badge so the badge travels with it wherever it currently is,\n * plus (independently) a finding-count badge when `findingCount > 0`. */\nfunction slideNode(slide: PreviewHtmlSlideInput, findingCount: number): string {\n const idAttr = slide.id !== undefined ? ` data-id=\"${escapeHtml(slide.id)}\"` : \"\"\n const badge = slide.placeholder ? `<div class=\"pf-badge\" aria-hidden=\"true\">unfilled</div>` : \"\"\n const fBadge = findingBadge(findingCount, \"pf-finding-badge\")\n return `<div class=\"pf-slide\" id=\"pf-slide-${slide.index}\" data-index=\"${slide.index}\"${idAttr}>${badge}${fBadge}${slide.svg}</div>`\n}\n\n/** `\"slide 3 · content · p-body · unfilled\"` — shared by the thumbnail\n * button's `title`/`aria-label` (raw pieces joined, then escaped once —\n * escaping each piece separately and joining after would be equally\n * correct, but this reads simpler and is exactly as safe). */\nfunction thumbDescription(slide: PreviewHtmlSlideInput): string {\n const parts = [`slide ${slide.index + 1}`, slide.type]\n if (slide.id !== undefined) parts.push(slide.id)\n if (slide.placeholder) parts.push(\"unfilled\")\n return escapeHtml(parts.join(\" · \"))\n}\n\n/** `\"3 · p-body\"` (or just `\"3\"` without an id) — the thumbnail's small\n * printed label, and (via {@link counterText}) the page counter's format. */\nfunction positionLabel(slide: PreviewHtmlSlideInput): string {\n const idPart = slide.id !== undefined ? ` · ${slide.id}` : \"\"\n return escapeHtml(`${slide.index + 1}${idPart}`)\n}\n\n/** `\"1 / 8 · p-cover\"` — the page counter's initial text (rendered directly\n * into the static markup so it is correct even before `<script>` runs;\n * `<script>`'s own `updateCounter()` keeps it in sync after that). */\nfunction counterText(slide: PreviewHtmlSlideInput, total: number): string {\n const idPart = slide.id !== undefined ? ` · ${slide.id}` : \"\"\n return escapeHtml(`${slide.index + 1} / ${total}${idPart}`)\n}\n\n/** One always-present thumbnail button. `slotContent` is the slide's own\n * {@link slideNode} markup when this slide starts inactive (every slide but\n * the first), or `\"\"` when it starts active (the first slide — its node\n * lives in `#pf-stage` instead, see {@link buildPreviewHtml}). Either way\n * the button itself, its label, its own \"unfilled\" badge (if the slide is a\n * placeholder), and its own finding-count badge (if `findingCount > 0`) are\n * always rendered — only the slot's content moves. */\nfunction thumbButton(slide: PreviewHtmlSlideInput, isActive: boolean, slotContent: string, findingCount: number): string {\n const description = thumbDescription(slide)\n const badge = slide.placeholder ? `<span class=\"pf-thumb-badge\" aria-hidden=\"true\">unfilled</span>` : \"\"\n const fBadge = findingBadge(findingCount, \"pf-thumb-finding-badge\")\n return (\n `<button type=\"button\" class=\"pf-thumb${isActive ? \" pf-thumb-active\" : \"\"}\" id=\"pf-thumb-${slide.index}\" ` +\n `data-index=\"${slide.index}\" title=\"${description}\" aria-label=\"${description}\">` +\n `<span class=\"pf-thumb-slot\" id=\"pf-slot-${slide.index}\">${slotContent}</span>` +\n `<span class=\"pf-thumb-label\">${positionLabel(slide)}</span>` +\n `${badge}${fBadge}</button>`\n )\n}\n\n/** One row in the audit findings panel — `data-page-index` is the finding's\n * owning slide's 0-based array index (`f.page - 1`, `PreviewHtmlFinding.page`\n * is 1-based), the same identity `<script>`'s existing `activate(i)` already\n * navigates by, so a click just calls the same function every thumbnail\n * click already does. */\nfunction findingPanelEntry(f: PreviewHtmlFinding): string {\n const idPart = f.slideId !== undefined ? ` · ${escapeHtml(f.slideId)}` : \"\"\n return (\n `<button type=\"button\" class=\"pf-finding\" data-page-index=\"${f.page - 1}\">` +\n `<span class=\"pf-finding-loc\">page ${f.page}${idPart}</span>` +\n `<span class=\"pf-finding-code\">[${escapeHtml(f.code)}]</span> ` +\n `<span class=\"pf-finding-msg\">${escapeHtml(f.message)}</span>` +\n `</button>`\n )\n}\n\nconst CSS = `\n:root{color-scheme:light}\n*{box-sizing:border-box}\nhtml,body{height:100%;margin:0}\nbody{display:flex;flex-direction:column;font-family:-apple-system,BlinkMacSystemFont,\"Segoe UI\",Helvetica,Arial,sans-serif;background:#f4f4f4;color:#1a1a1a}\nheader{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:10px 16px;background:#fff;border-bottom:1px solid #ddd;font-size:14px;flex:0 0 auto}\n#pf-title{font-weight:600;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}\n#pf-counter{font-variant-numeric:tabular-nums;color:#555;white-space:nowrap}\n#pf-audit-note{color:#b45309;font-size:12px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}\n#pf-export-btn{font:inherit;font-size:13px;padding:6px 10px;border:1px solid #2563eb;background:#2563eb;color:#fff;border-radius:4px;cursor:pointer;white-space:nowrap;flex:0 0 auto}\n#pf-export-btn:hover{background:#1d4ed8}\n#pf-stage-wrap{flex:1 1 auto;min-height:0;display:flex;align-items:center;justify-content:center;gap:16px;padding:16px}\n#pf-stage{position:relative;background:#000;box-shadow:0 2px 16px rgba(0,0,0,.15);aspect-ratio:16/9;width:min(100%,calc((100vh - 190px) * 16 / 9));max-height:100%}\n#pf-stage,.pf-thumb-slot{position:relative}\n.pf-slide{position:absolute;inset:0}\n.pf-slide svg{display:block;width:100%;height:100%}\n#pf-side{flex:0 0 260px;align-self:stretch;overflow-y:auto;background:#fff;border:1px solid #ddd;border-radius:6px;padding:12px;font-size:13px}\n#pf-side h2{margin:0 0 8px;font-size:11px;text-transform:uppercase;letter-spacing:.04em;color:#666}\n#pf-side section+section{margin-top:16px;padding-top:16px;border-top:1px solid #eee}\n#pf-audit-checks{margin:0 0 12px;font-size:12px;color:#555}\n.pf-finding{display:block;width:100%;text-align:left;background:#fff;border:1px solid #eee;border-radius:4px;padding:6px 8px;margin-bottom:6px;cursor:pointer;font:inherit}\n.pf-finding:hover{border-color:#93c5fd}\n.pf-finding-loc{display:block;font-size:11px;color:#888}\n.pf-finding-code{display:inline-block;font-size:11px;font-weight:700;color:#b91c1c}\n.pf-finding-msg{font-size:12px;color:#333}\n#pf-annotate-current-label{font-size:11px;color:#888;margin-bottom:6px}\n#pf-annotate-list{list-style:none;margin:0 0 8px;padding:0}\n.pf-annotate-item{display:flex;justify-content:space-between;gap:6px;align-items:flex-start;padding:4px 0;border-bottom:1px solid #f0f0f0;font-size:12px}\n.pf-annotate-remove{border:none;background:none;color:#999;cursor:pointer;font-size:14px;line-height:1;padding:0 2px}\n.pf-annotate-remove:hover{color:#dc2626}\n#pf-annotate-input{width:100%;box-sizing:border-box;font:inherit;font-size:12px;padding:6px;border:1px solid #ddd;border-radius:4px;resize:vertical}\n#pf-annotate-add{font:inherit;font-size:12px;margin-top:6px;width:100%;padding:6px 10px;border:1px solid #2563eb;background:#2563eb;color:#fff;border-radius:4px;cursor:pointer}\n#pf-annotate-add:hover{background:#1d4ed8}\n#pf-filmstrip{display:flex;gap:8px;padding:10px 16px;overflow-x:auto;background:#fff;border-top:1px solid #ddd;flex:0 0 auto}\n.pf-thumb{flex:0 0 auto;width:160px;padding:0;margin:0;border:2px solid transparent;background:#eee;cursor:pointer;border-radius:6px;overflow:hidden;position:relative;font:inherit;text-align:left}\n.pf-thumb:hover{border-color:#93c5fd}\n.pf-thumb-active,.pf-thumb-active:hover{border-color:#2563eb;background:#dbeafe}\n.pf-thumb-slot{display:block;width:100%;aspect-ratio:16/9;background:#ddd}\n.pf-thumb-label{display:block;font-size:11px;line-height:1.4;padding:3px 6px;color:#444;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}\n.pf-badge,.pf-thumb-badge{position:absolute;top:4px;right:4px;background:#d97706;color:#fff;font-size:10px;font-weight:700;letter-spacing:.03em;padding:2px 6px;border-radius:3px;text-transform:uppercase;z-index:2;pointer-events:none}\n.pf-finding-badge,.pf-thumb-finding-badge{position:absolute;top:4px;left:4px;background:#dc2626;color:#fff;font-size:10px;font-weight:700;padding:2px 6px;border-radius:3px;z-index:2;pointer-events:none}\n`.trim()\n\nconst JS = `\n(function () {\n var stage = document.getElementById('pf-stage')\n var counter = document.getElementById('pf-counter')\n var thumbs = Array.prototype.slice.call(document.querySelectorAll('.pf-thumb'))\n var total = thumbs.length\n if (total === 0) return\n var current = parseInt(thumbs[0].getAttribute('data-index'), 10)\n\n function slideEl(i) { return document.getElementById('pf-slide-' + i) }\n function slotEl(i) { return document.getElementById('pf-slot-' + i) }\n function thumbEl(i) { return document.getElementById('pf-thumb-' + i) }\n function thumbPos(i) {\n for (var t = 0; t < thumbs.length; t++) {\n if (parseInt(thumbs[t].getAttribute('data-index'), 10) === i) return t\n }\n return -1\n }\n\n function updateCounter(i) {\n var el = slideEl(i)\n if (!el) return\n var text = (thumbPos(i) + 1) + ' / ' + total\n var id = el.getAttribute('data-id')\n if (id) text += ' · ' + id\n counter.textContent = text\n }\n\n function activate(i) {\n if (i === current) return\n var nextSlide = slideEl(i)\n var nextThumb = thumbEl(i)\n if (!nextSlide || !nextThumb) return\n var prevSlide = slideEl(current)\n var prevSlot = slotEl(current)\n if (prevSlide && prevSlot) prevSlot.appendChild(prevSlide)\n var prevThumb = thumbEl(current)\n if (prevThumb) prevThumb.classList.remove('pf-thumb-active')\n stage.appendChild(nextSlide)\n nextThumb.classList.add('pf-thumb-active')\n current = i\n updateCounter(i)\n renderAnnotations()\n }\n\n thumbs.forEach(function (t) {\n t.addEventListener('click', function () {\n activate(parseInt(t.getAttribute('data-index'), 10))\n })\n })\n\n document.addEventListener('keydown', function (e) {\n var pos = thumbPos(current)\n if (e.key === 'ArrowRight' && pos < total - 1) {\n activate(parseInt(thumbs[pos + 1].getAttribute('data-index'), 10))\n } else if (e.key === 'ArrowLeft' && pos > 0) {\n activate(parseInt(thumbs[pos - 1].getAttribute('data-index'), 10))\n }\n })\n\n // ---- audit findings panel: click a finding, jump to its page (same\n // activate() every thumbnail click already uses) ----\n Array.prototype.slice.call(document.querySelectorAll('.pf-finding')).forEach(function (b) {\n b.addEventListener('click', function () {\n activate(parseInt(b.getAttribute('data-page-index'), 10))\n })\n })\n\n // ---- annotations: in-memory only, keyed by the slide's 0-based array\n // index (current) — never by pageId, since an object key coerces every\n // key to a string and would silently collide a numeric page-fallback\n // pageId with a same-looking slide id (or lose the numeric/string type\n // distinction the exported JSON needs); pageIdFor() below resolves the\n // real pageId fresh from the DOM only at render/export time. ----\n var annotations = {}\n var annotateList = document.getElementById('pf-annotate-list')\n var annotateInput = document.getElementById('pf-annotate-input')\n var annotateLabel = document.getElementById('pf-annotate-current-label')\n var annotateAdd = document.getElementById('pf-annotate-add')\n var exportBtn = document.getElementById('pf-export-btn')\n\n // Slide id when the active slide has one, else its 1-based page number —\n // mirrors AuditFinding.page's own \"1-based page number when there is no\n // slide id\" convention (../svg/audit/deck-audit.ts) rather than this\n // file's internal 0-based data-index, so a revision-request.json's\n // pageId lines up with what pptfast audit/validate already print.\n function pageIdFor(i) {\n var el = slideEl(i)\n var id = el ? el.getAttribute('data-id') : null\n return id !== null ? id : thumbPos(i) + 1\n }\n\n function renderAnnotations() {\n var pid = pageIdFor(current)\n annotateLabel.textContent = 'page ' + (thumbPos(current) + 1) + (typeof pid === 'string' ? ' · ' + pid : '')\n var list = annotations[current] || []\n annotateList.innerHTML = ''\n list.forEach(function (text, idx) {\n var li = document.createElement('li')\n li.className = 'pf-annotate-item'\n var span = document.createElement('span')\n span.textContent = text\n var rm = document.createElement('button')\n rm.type = 'button'\n rm.className = 'pf-annotate-remove'\n rm.setAttribute('aria-label', 'remove annotation')\n rm.textContent = '\\\\u00d7'\n rm.addEventListener('click', function () {\n list.splice(idx, 1)\n renderAnnotations()\n })\n li.appendChild(span)\n li.appendChild(rm)\n annotateList.appendChild(li)\n })\n }\n\n annotateAdd.addEventListener('click', function () {\n var text = annotateInput.value.trim()\n if (!text) return\n if (!annotations[current]) annotations[current] = []\n annotations[current].push(text)\n annotateInput.value = ''\n renderAnnotations()\n })\n\n // \"Export revision requests\": preview.html stays read-only end to end —\n // this never writes back into the deck itself, only produces a JSON file\n // of requests for an agent/human to route through pages/*.json (see\n // skills/pptfast/SKILL.md's phase-6 revision-request handling). Zero\n // network/storage — an in-memory Blob + a synthetic <a download> click,\n // the standard browser-only download pattern.\n exportBtn.addEventListener('click', function () {\n var requests = []\n Object.keys(annotations).forEach(function (key) {\n var i = parseInt(key, 10)\n var pid = pageIdFor(i)\n annotations[i].forEach(function (text) {\n requests.push({ pageId: pid, annotation: text, createdAt: new Date().toISOString() })\n })\n })\n var deckTitle = document.getElementById('pf-title').textContent\n var payload = { version: '1', deck: deckTitle, requests: requests }\n var blob = new Blob([JSON.stringify(payload, null, 2)], { type: 'application/json' })\n var url = URL.createObjectURL(blob)\n var a = document.createElement('a')\n a.href = url\n a.download = 'revision-request.json'\n document.body.appendChild(a)\n a.click()\n document.body.removeChild(a)\n URL.revokeObjectURL(url)\n })\n\n renderAnnotations()\n})()\n`.trim()\n\n/** Always-present \"Annotations\" side panel — static markup, no per-build\n * data (the annotation list itself lives only in `<script>`'s in-memory\n * `annotations` state, populated and re-rendered at runtime, see `JS`\n * above's `renderAnnotations()`). Unlike the audit findings panel, this one\n * is never conditionally omitted — annotating is available regardless of\n * whether the audit ran, found anything, or was skipped for placeholders. */\nconst ANNOTATE_PANEL = `<section id=\"pf-annotate-panel\">\n<h2>Annotations</h2>\n<div id=\"pf-annotate-current-label\"></div>\n<ul id=\"pf-annotate-list\"></ul>\n<textarea id=\"pf-annotate-input\" rows=\"3\" placeholder=\"Add a note for this page…\"></textarea>\n<button type=\"button\" id=\"pf-annotate-add\">Add annotation</button>\n</section>`\n\n/**\n * Build the self-contained `preview.html` bundle. Pure — no `fs`, safe to\n * unit-test directly (`./preview-html.test.ts`). See this module's own doc\n * comment for the self-containment and single-embed-per-slide design notes.\n */\nexport function buildPreviewHtml(input: PreviewHtmlInput): string {\n const { title, slides, findings = [], auditNote, checks } = input\n const total = slides.length\n const escapedTitle = escapeHtml(title)\n\n // Group findings by the 1-based page number they belong to, so each\n // slide's badge count is a single map lookup rather than an O(findings)\n // scan per slide.\n const findingsByPage = new Map<number, PreviewHtmlFinding[]>()\n for (const f of findings) {\n const list = findingsByPage.get(f.page)\n if (list) list.push(f)\n else findingsByPage.set(f.page, [f])\n }\n const countFor = (slide: PreviewHtmlSlideInput) => findingsByPage.get(slide.index + 1)?.length ?? 0\n\n const stageSlide = total > 0 ? slideNode(slides[0]!, countFor(slides[0]!)) : \"\"\n const thumbs = slides\n .map((s, i) => thumbButton(s, i === 0, i === 0 ? \"\" : slideNode(s, countFor(s)), countFor(s)))\n .join(\"\")\n const initialCounter = total > 0 ? counterText(slides[0]!, total) : \"0 / 0\"\n\n // Findings panel + embedded JSON blob (see this module's own doc comment\n // for why the panel is static markup, not client-computed from the blob)\n // — both entirely omitted when there is nothing to show, so a clean or\n // audit-skipped deck's preview.html carries no trace of either (matches\n // the pre-existing \"never shows an 'unfilled' badge when no slide is a\n // placeholder\" precedent for the badge markup itself).\n const auditPanel =\n findings.length > 0\n ? `<section id=\"pf-audit-panel\"><h2>Audit findings (${findings.length})</h2><div id=\"pf-audit-list\">${findings.map(findingPanelEntry).join(\"\")}</div></section>`\n : \"\"\n const findingsDataScript =\n findings.length > 0\n ? `<script type=\"application/json\" id=\"pf-audit-findings\">${embedJson(findings)}</script>`\n : \"\"\n const auditNoteHtml = auditNote !== undefined ? `<span id=\"pf-audit-note\">${escapeHtml(auditNote)}</span>` : \"\"\n\n // One-line \"which check families actually ran\" summary (fix round,\n // Important-1: the task brief's own scope for this wave, missed in the\n // first pass) — independent of `auditPanel`'s `findings.length > 0` gate\n // on purpose, see `PreviewHtmlInput.checks`'s own doc comment for why.\n // Renders the literal state words straight out of `PreviewHtmlChecks`\n // (`\"completed\"` / `\"not-requested\"`) with no checkmark/tick substitute —\n // the soul constraint this whole wave is built on is \"not checked must\n // never read as passed\", and a glyph here would be exactly that misread\n // for a `pixels: \"not-requested\"` report.\n const checksLine =\n checks !== undefined\n ? `<div id=\"pf-audit-checks\">audit checks: svg ${checks.svg} · pixels ${checks.pixels}</div>`\n : \"\"\n\n return `<!doctype html>\n<html lang=\"en\">\n<head>\n<meta charset=\"utf-8\">\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n<title>${escapedTitle} — pptfast preview</title>\n<style>${CSS}</style>\n</head>\n<body>\n<header>\n<span id=\"pf-title\">${escapedTitle}</span>\n<span id=\"pf-counter\">${initialCounter}</span>\n${auditNoteHtml}\n<button type=\"button\" id=\"pf-export-btn\">Export revision requests</button>\n</header>\n<div id=\"pf-stage-wrap\"><div id=\"pf-stage\">${stageSlide}</div><aside id=\"pf-side\">${checksLine}${auditPanel}${ANNOTATE_PANEL}</aside></div>\n<nav id=\"pf-filmstrip\" aria-label=\"slides\">${thumbs}</nav>\n${findingsDataScript}\n<script>${JS}</script>\n</body>\n</html>\n`\n}\n","// Ported from markpress src/update.ts (same author), minus its playwright\n// post-install step — pptfast has no browser dependency to refresh.\nimport { execFile } from \"node:child_process\"\n\nexport const PACKAGE_NAME = \"@liustack/pptfast\"\n\nexport interface UpdateInfo {\n packageName: string\n currentVersion: string\n latestVersion: string | null\n updateAvailable: boolean\n checked: boolean\n error?: string\n}\n\nexport interface SelfUpdateResult extends UpdateInfo {\n updated: boolean\n}\n\nexport type CommandRunner = (command: string, args: string[]) => Promise<string>\n\nfunction normalizeVersion(version: string): string {\n const normalized = version.trim().replace(/^v/i, \"\").split(\"-\")[0]\n if (!normalized) throw new Error(`invalid version: ${version}`)\n return normalized\n}\n\nfunction parseVersion(version: string): number[] {\n return normalizeVersion(version)\n .split(\".\")\n .map((segment) => {\n const value = Number.parseInt(segment, 10)\n if (!Number.isFinite(value)) throw new Error(`invalid version segment: ${segment}`)\n return value\n })\n}\n\nexport function compareVersions(left: string, right: string): number {\n const l = parseVersion(left)\n const r = parseVersion(right)\n for (let i = 0; i < Math.max(l.length, r.length); i++) {\n const a = l[i] ?? 0\n const b = r[i] ?? 0\n if (a !== b) return a < b ? -1 : 1\n }\n return 0\n}\n\nexport const runCommand: CommandRunner = (command, args) =>\n new Promise((resolve, reject) => {\n execFile(command, args, { encoding: \"utf8\" }, (error, stdout, stderr) => {\n if (error) {\n reject(new Error(stderr.trim() || error.message))\n return\n }\n resolve(stdout.trim())\n })\n })\n\nexport interface CheckForUpdateOptions {\n currentVersion: string\n packageName?: string\n run?: CommandRunner\n}\n\n/** Never throws — an unreachable registry reports { checked: false, error }. */\nexport async function checkForUpdate({\n currentVersion,\n packageName = PACKAGE_NAME,\n run = runCommand,\n}: CheckForUpdateOptions): Promise<UpdateInfo> {\n const current = normalizeVersion(currentVersion)\n try {\n const latest = normalizeVersion(await run(\"npm\", [\"view\", packageName, \"version\"]))\n return {\n packageName,\n currentVersion: current,\n latestVersion: latest,\n updateAvailable: compareVersions(current, latest) < 0,\n checked: true,\n }\n } catch (error) {\n return {\n packageName,\n currentVersion: current,\n latestVersion: null,\n updateAvailable: false,\n checked: false,\n error: error instanceof Error ? error.message : String(error),\n }\n }\n}\n\nexport function createSelfUpdater(run: CommandRunner = runCommand) {\n return async ({\n currentVersion,\n packageName = PACKAGE_NAME,\n }: {\n currentVersion: string\n packageName?: string\n }): Promise<SelfUpdateResult> => {\n const info = await checkForUpdate({ currentVersion, packageName, run })\n if (!info.checked) throw new Error(`unable to check for updates: ${info.error ?? \"unknown error\"}`)\n if (!info.updateAvailable) return { ...info, updated: false }\n await run(\"npm\", [\"install\", \"-g\", `${packageName}@latest`])\n return { ...info, updated: true }\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AACA,SAAS,eAAe;;;ACDxB,SAAS,SAAAA,QAAO,IAAI,aAAAC,kBAAiB;AACrC,SAAS,WAAAC,UAAS,QAAAC,OAAM,YAAAC,WAAU,WAAAC,gBAAe;;;ACDjD,SAAS,gBAAgB;AACzB,SAAS,SAAS,QAAAC,OAAM,WAAAC,gBAAe;AACvC,SAAS,SAAS;;;ACFlB,SAAS,eAAe;AACxB,SAAS,MAAM,eAAe;AAevB,SAAS,cAAsB;AACpC,SAAO,QAAQ,IAAI,gBAAgB,KAAK,QAAQ,GAAG,UAAU;AAC/D;AAqBO,SAAS,UAAU,QAAwC;AAChE,SAAO,QAAQ,YAAY,GAAG,QAAQ,YAAY,OAAO;AAC3D;AAGO,SAAS,iBAAyB;AACvC,SAAO,KAAK,YAAY,GAAG,aAAa;AAC1C;;;ADRA,IAAM,eAAe,EAClB,OAAO;AAAA,EACN,OAAO,EAAE,OAAO,EAAE,SAAS;AAAA,EAC3B,OAAO,oBAAoB,SAAS;AAAA,EACpC,UAAU,EAAE,OAAO,EAAE,SAAS;AAChC,CAAC,EACA,OAAO;AA0BV,IAAM,mBAAmB,EACtB,OAAO;AAAA,EACN,OAAO,EAAE,OAAO,EAAE,SAAS;AAAA,EAC3B,OAAO,oBAAoB,SAAS;AAAA,EACpC,UAAU,EAAE,OAAO,EAAE,SAAS;AAChC,CAAC,EACA,OAAO;AAIH,IAAM,kBAAkB;AAY/B,eAAe,eACb,MACA,QAC6C;AAC7C,MAAI;AACJ,MAAI;AACF,WAAO,MAAM,SAAS,MAAM,MAAM;AAAA,EACpC,QAAQ;AACN,WAAO;AAAA,EACT;AACA,MAAI;AACJ,MAAI;AACF,UAAM,KAAK,MAAM,IAAI;AAAA,EACvB,SAAS,GAAG;AACV,UAAM,IAAI,aAAa,GAAG,IAAI,uBAAwB,EAAY,OAAO,EAAE;AAAA,EAC7E;AACA,QAAM,IAAI,OAAO,UAAU,GAAG;AAC9B,MAAI,CAAC,EAAE,SAAS;AACd,UAAM,SAAS,EAAE,MAAM,OACpB,IAAI,CAAC,MAAM,GAAG,EAAE,KAAK,KAAK,GAAG,KAAK,QAAQ,KAAK,EAAE,OAAO,EAAE,EAC1D,KAAK,IAAI;AACZ,UAAM,IAAI,aAAa,WAAW,IAAI;AAAA,EAAM,MAAM,EAAE;AAAA,EACtD;AACA,SAAO,EAAE,MAAM,QAAQ,EAAE,KAAK;AAChC;AAIA,eAAsB,WACpB,UACyD;AACzD,MAAI,MAAMC,SAAQ,QAAQ;AAC1B,aAAS;AACP,UAAM,MAAM,MAAM,eAAeC,MAAK,KAAK,eAAe,GAAG,YAAY;AACzE,QAAI,IAAK,QAAO;AAChB,UAAM,SAAS,QAAQ,GAAG;AAC1B,QAAI,WAAW,IAAK,QAAO;AAC3B,UAAM;AAAA,EACR;AACF;AAWA,eAAsB,iBAA8E;AAClG,SAAO,eAAe,eAAe,GAAG,gBAAgB;AAC1D;;;AEnHA,SAAS,UAAU,OAAO,YAAAC,WAAU,SAAS,MAAM,iBAAiB;AACpE,SAAS,UAAU,WAAAC,UAAS,cAAAC,aAAY,QAAAC,OAAM,UAAU,WAAAC,gBAAe;;;AC9BvE,SAAS,YAAAC,iBAAgB;AACzB,SAAS,SAAS,YAAY,WAAAC,gBAAe;AAM7C,IAAM,cAAsC;AAAA,EAC1C,QAAQ;AAAA,EACR,QAAQ;AAAA,EACR,SAAS;AAAA,EACT,QAAQ;AACV;AAgBO,IAAM,cAAsC;AAAA,EACjD,aAAa;AAAA,EACb,cAAc;AAAA,EACd,aAAa;AAAA,EACb,cAAc;AAChB;AAQA,eAAsB,WAAW,QAAgB,OAAO,MAAwB;AAC9E,MAAI;AACJ,MAAI;AACF,WAAO,MAAMC,UAAS,QAAQ,MAAM;AAAA,EACtC,QAAQ;AACN,UAAM,IAAI,aAAa,eAAe,IAAI,UAAU,MAAM,EAAE;AAAA,EAC9D;AACA,MAAI;AACF,WAAO,KAAK,MAAM,IAAI;AAAA,EACxB,SAAS,GAAG;AACV,UAAM,IAAI,aAAa,GAAG,IAAI,SAAS,MAAM,uBAAwB,EAAY,OAAO,EAAE;AAAA,EAC5F;AACF;AA6BA,eAAsB,mBAAmB,IAAY,SAAgC;AACnF,aAAW,CAAC,MAAM,KAAK,KAAK,OAAO,QAAQ,GAAG,OAAO,MAAM,GAAG;AAC5D,UAAM,MAAM,MAAM;AAClB,QAAI,IAAI,WAAW,OAAO,KAAK,eAAe,KAAK,GAAG,EAAG;AACzD,UAAM,MAAM,WAAW,GAAG,IAAI,MAAMC,SAAQ,SAAS,GAAG;AACxD,QAAI;AACJ,QAAI;AACF,cAAQ,MAAMD,UAAS,GAAG;AAAA,IAC5B,QAAQ;AACN,YAAM,IAAI,aAAa,UAAU,IAAI,6BAA6B,GAAG,eAAe,GAAG,IAAI;AAAA,IAC7F;AACA,QAAI,MAAM,WAAW,GAAG;AACtB,YAAM,IAAI,aAAa,UAAU,IAAI,iBAAiB,GAAG,uDAAkD;AAAA,IAC7G;AACA,UAAM,MAAM,QAAQ,GAAG,EAAE,YAAY;AACrC,UAAM,OAAO,YAAY,GAAG;AAC5B,QAAI,MAAM;AACR,YAAM,UAAU,iBAAiB,KAAK;AACtC,UAAI,YAAY,MAAM;AACpB,cAAM,IAAI;AAAA,UACR,UAAU,IAAI,iBAAiB,GAAG,2DAA2D,IAAI;AAAA,QACnG;AAAA,MACF;AACA,YAAM,WAAW,eAAe,IAAI;AACpC,UAAI,YAAY,YAAY,UAAU;AACpC,cAAM,IAAI;AAAA,UACR,UAAU,IAAI,iBAAiB,GAAG,cAAc,GAAG,gCAAgC,uBAAuB,OAAO,CAAC,0FAAqF,IAAI;AAAA,QAC7M;AAAA,MACF;AACA,YAAM,MAAM,QAAQ,IAAI,WAAW,MAAM,SAAS,QAAQ,CAAC;AAC3D;AAAA,IACF;AACA,UAAM,SAAS,YAAY,EAAE;AAC7B,QAAI,CAAC,QAAQ;AACX,YAAM,IAAI;AAAA,QACR,UAAU,IAAI,gCAAgC,QAAQ,GAAG,CAAC;AAAA,MAC5D;AAAA,IACF;AACA,UAAM,MAAM,MAAM,OAAO,wCAAwC,MAAM,SAAS,QAAQ,CAAC,EAAE;AAAA,EAC7F;AACF;;;AD/EO,IAAM,gBAAgB;AACtB,IAAM,gBAAgB;AAC7B,IAAM,gBAAgB;AACtB,IAAM,iBAAiB;AAoChB,SAAS,sBAAsB,IAAY,SAAuB;AACvE,QAAM,WAAWE,SAAQ,oBAAoB;AAC7C,QAAM,MAAM,SAAS,UAAUA,SAAQ,UAAU,EAAE,CAAC;AACpD,QAAM,OACJ,CAACC,YAAW,EAAE,KAAK,CAAC,GAAG,SAAS,GAAG,KAAK,CAAC,GAAG,SAAS,IAAI,KAAK,OAAO,QAAQ,CAAC,IAAI,WAAW,IAAI,KAAK,CAACA,YAAW,GAAG;AACvH,MAAI,CAAC,MAAM;AACT,UAAM,IAAI;AAAA,MACR,GAAG,OAAO,KAAK,EAAE;AAAA,IACnB;AAAA,EACF;AACF;AAoBA,eAAsB,gBAAgB,MAAgC;AACpE,MAAI;AACF,YAAQ,MAAM,KAAK,IAAI,GAAG,YAAY;AAAA,EACxC,SAAS,GAAG;AACV,QAAK,EAA4B,SAAS,SAAU,QAAO;AAC3D,UAAM,IAAI,aAAa,gBAAgB,IAAI,KAAM,EAAY,OAAO,EAAE;AAAA,EACxE;AACF;AAaA,eAAsB,WAAW,MAAgC;AAC/D,MAAI;AACF,UAAM,KAAK,IAAI;AACf,WAAO;AAAA,EACT,SAAS,GAAG;AACV,QAAK,EAA4B,SAAS,SAAU,QAAO;AAC3D,UAAM,IAAI,aAAa,qBAAqB,IAAI,KAAM,EAAY,OAAO,EAAE;AAAA,EAC7E;AACF;AAiDA,eAAsB,kBACpB,KACA,QACA,MAAc,QAAQ,IAAI,GACT;AACjB,MAAI,IAAI,KAAK,MAAM,GAAI,OAAM,IAAI,aAAa,+BAA+B;AAC7E,MAAI,IAAI,SAAS,GAAG,KAAK,IAAI,SAAS,IAAI,EAAG,QAAOD,SAAQ,KAAK,GAAG;AACpE,QAAM,QAAQA,SAAQ,KAAK,GAAG;AAC9B,MAAI,MAAM,WAAW,KAAK,EAAG,QAAO;AACpC,QAAM,WAAWE,MAAK,UAAU,MAAM,GAAG,GAAG;AAC5C,SAAQ,MAAM,WAAW,QAAQ,IAAK,WAAW;AACnD;AAQA,SAAS,qBAA6B;AACpC,QAAM,OAA2B;AAAA,IAC/B,CAAC,eAAe,+CAA+C;AAAA,IAC/D,CAAC,GAAG,aAAa,mBAAmB,8DAA8D;AAAA,IAClG,CAAC,GAAG,cAAc,KAAK,uBAAuB;AAAA,EAChD;AACA,QAAM,QAAQ,KAAK,IAAI,GAAG,KAAK,IAAI,CAAC,CAAC,IAAI,MAAM,KAAK,MAAM,CAAC,IAAI;AAC/D,SAAO,KAAK,IAAI,CAAC,CAAC,MAAM,IAAI,MAAM,KAAK,KAAK,OAAO,KAAK,CAAC,GAAG,IAAI,EAAE,EAAE,KAAK,IAAI;AAC/E;AA2BA,eAAe,aAAa,KAA+B;AACzD,QAAM,WAAWA,MAAK,KAAK,aAAa;AACxC,QAAM,WAAWA,MAAK,KAAK,aAAa;AACxC,QAAM,CAAC,YAAY,UAAU,IAAI,MAAM,QAAQ,IAAI,CAAC,WAAW,QAAQ,GAAG,WAAW,QAAQ,CAAC,CAAC;AAC/F,MAAI,cAAc,YAAY;AAC5B,UAAM,IAAI;AAAA,MACR,QAAQ,aAAa,QAAQ,aAAa,aAAa,GAAG,+DAA0D,aAAa,4BAA4B,aAAa;AAAA,IAC5K;AAAA,EACF;AACA,MAAI,CAAC,cAAc,YAAY;AAC7B,UAAM,IAAI;AAAA,MACR,GAAG,GAAG,QAAQ,aAAa,WAAW,aAAa,4CAAuC,aAAa,2BAA2B,GAAG,OAAO,GAAG;AAAA,IACjJ;AAAA,EACF;AACA,MAAI;AACJ,MAAI;AACF,WAAO,MAAMC,UAAS,UAAU,MAAM;AAAA,EACxC,SAAS,GAAG;AACV,QAAK,EAA4B,SAAS,UAAU;AAClD,YAAM,IAAI;AAAA,QACR,MAAM,aAAa,OAAO,GAAG;AAAA,EAA0C,mBAAmB,CAAC;AAAA,MAC7F;AAAA,IACF;AACA,UAAM,IAAI,aAAa,0BAA0B,QAAQ,EAAE;AAAA,EAC7D;AACA,MAAI;AACF,WAAO,KAAK,MAAM,IAAI;AAAA,EACxB,SAAS,GAAG;AACV,UAAM,IAAI,aAAa,aAAa,QAAQ,uBAAwB,EAAY,OAAO,EAAE;AAAA,EAC3F;AACF;AAyBA,eAAe,UAAU,KAA+C;AACtE,QAAM,WAAWD,MAAK,KAAK,aAAa;AACxC,MAAI;AACJ,MAAI;AACF,eAAW,MAAM,QAAQ,UAAU,EAAE,eAAe,KAAK,CAAC,GACvD,OAAO,CAAC,UAAU,MAAM,OAAO,KAAKE,SAAQ,MAAM,IAAI,MAAM,OAAO,EACnE,IAAI,CAAC,UAAU,MAAM,IAAI;AAAA,EAC9B,SAAS,GAAG;AACV,QAAK,EAA4B,SAAS,SAAU,QAAO,CAAC;AAC5D,UAAM,IAAI,aAAa,eAAe,aAAa,eAAe,QAAQ,KAAM,EAAY,OAAO,EAAE;AAAA,EACvG;AACA,QAAM,QAAiC,CAAC;AACxC,QAAM,QAAQ;AAAA,IACZ,QAAQ,IAAI,OAAO,UAAU;AAC3B,YAAM,KAAK,SAAS,OAAO,OAAO;AAClC,YAAM,EAAE,IAAI,MAAM,WAAWF,MAAK,UAAU,KAAK,GAAG,SAAS,EAAE,GAAG;AAAA,IACpE,CAAC;AAAA,EACH;AACA,SAAO;AACT;AAyBA,eAAe,WAAW,KAAuD;AAC/E,QAAM,YAAYA,MAAK,KAAK,cAAc;AAC1C,MAAI;AACJ,MAAI;AACF,eAAW,MAAM,QAAQ,WAAW,EAAE,eAAe,KAAK,CAAC,GACxD,OAAO,CAAC,UAAU,MAAM,OAAO,KAAK,CAAC,MAAM,KAAK,WAAW,GAAG,CAAC,EAC/D,IAAI,CAAC,UAAU,MAAM,IAAI;AAAA,EAC9B,SAAS,GAAG;AACV,QAAK,EAA4B,SAAS,SAAU,QAAO,CAAC;AAC5D,UAAM,IAAI,aAAa,eAAe,cAAc,eAAe,SAAS,KAAM,EAAY,OAAO,EAAE;AAAA,EACzG;AACA,QAAM,SAA0C,CAAC;AACjD,QAAM,aAAa,oBAAI,IAAoB;AAC3C,aAAW,SAAS,SAAS;AAC3B,UAAM,KAAK,SAAS,OAAOE,SAAQ,KAAK,CAAC;AACzC,UAAM,WAAW,WAAW,IAAI,EAAE;AAClC,QAAI,aAAa,QAAW;AAC1B,YAAM,IAAI;AAAA,QACR,GAAG,cAAc,IAAI,QAAQ,QAAQ,cAAc,IAAI,KAAK,4BAA4B,EAAE;AAAA,MAC5F;AAAA,IACF;AACA,eAAW,IAAI,IAAI,KAAK;AACxB,WAAO,EAAE,IAAI,EAAE,KAAK,GAAG,cAAc,IAAI,KAAK,GAAG;AAAA,EACnD;AACA,SAAO;AACT;AAuDA,eAAsB,YAAY,KAAqC;AACrE,QAAM,UAAUJ,SAAQ,GAAG;AAC3B,QAAMK,QAAO,MAAM,aAAa,OAAO;AACvC,QAAM,QAAQ,MAAM,UAAU,OAAO;AACrC,QAAM,EAAE,IAAI,eAAe,wBAAwB,IAAI,aAAaA,OAAM,KAAoC;AAC9G,QAAM,SAAS,MAAM,WAAW,OAAO;AACvC,QAAM,SAAS,EAAE,GAAG,IAAI,QAAQ,EAAE,QAAQ,EAAE,GAAG,GAAG,OAAO,QAAQ,GAAG,OAAO,EAAE,EAAE;AAC/E,SAAO,EAAE,IAAI,QAAQ,eAAe,yBAAyB,QAAQ;AACvE;AAiDA,eAAsB,gBACpB,QACA,QACA,eACgC;AAChC,QAAM,UAAU,OAAO,QAAQ,MAAM;AACrC,QAAM,YAAYH,MAAK,QAAQ,cAAc;AAC7C,MAAI,QAAQ,WAAW,EAAG,QAAO,EAAE,OAAO,GAAG,UAAU;AACvD,QAAM,MAAM,WAAW,EAAE,WAAW,KAAK,CAAC;AAC1C,QAAM,QAAQ,IAAI,QAAQ,IAAI,CAAC,CAAC,IAAI,KAAK,MAAM,cAAc,IAAI,MAAM,KAAK,WAAW,aAAa,CAAC,CAAC;AACtG,SAAO,EAAE,OAAO,QAAQ,QAAQ,UAAU;AAC5C;AAOA,IAAM,cAAc;AAEpB,eAAe,cAAc,IAAY,KAAa,WAAmB,eAAsC;AAK7G,wBAAsB,IAAI,UAAU;AACpC,MAAI,IAAI,WAAW,OAAO,GAAG;AAC3B,UAAM,QAAQ,YAAY,KAAK,GAAG;AAClC,QAAI,CAAC,OAAO;AACV,YAAM,IAAI,aAAa,UAAU,EAAE,2EAA2E;AAAA,IAChH;AACA,UAAM,OAAO,MAAM,CAAC;AACpB,UAAM,UAAU,MAAM,CAAC;AACvB,UAAM,MAAM,YAAY,IAAI;AAC5B,QAAI,CAAC,KAAK;AACR,YAAM,IAAI;AAAA,QACR,UAAU,EAAE,+CAA+C,IAAI,4BAAuB,OAAO,KAAK,WAAW,EAAE,KAAK,IAAI,CAAC;AAAA,MAC3H;AAAA,IACF;AACA,UAAM,UAAUA,MAAK,WAAW,GAAG,EAAE,GAAG,GAAG,EAAE,GAAG,OAAO,KAAK,SAAS,QAAQ,CAAC;AAC9E;AAAA,EACF;AACA,MAAI,eAAe,KAAK,GAAG,GAAG;AAC5B,UAAM,IAAI;AAAA,MACR,UAAU,EAAE;AAAA,IACd;AAAA,EACF;AACA,QAAM,MAAMD,YAAW,GAAG,IAAI,MAAMD,SAAQ,eAAe,GAAG;AAC9D,MAAI;AACF,UAAM,SAAS,KAAKE,MAAK,WAAW,GAAG,EAAE,GAAGE,SAAQ,GAAG,CAAC,EAAE,CAAC;AAAA,EAC7D,QAAQ;AACN,UAAM,IAAI,aAAa,UAAU,EAAE,+BAA+B,GAAG,eAAe,GAAG,8BAAyB;AAAA,EAClH;AACF;;;AE9VA,SAAS,WAAW,GAAmB;AACrC,SAAO,EACJ,QAAQ,MAAM,OAAO,EACrB,QAAQ,MAAM,MAAM,EACpB,QAAQ,MAAM,MAAM,EACpB,QAAQ,MAAM,QAAQ,EACtB,QAAQ,MAAM,OAAO;AAC1B;AAeA,SAAS,UAAU,OAAwB;AACzC,SAAO,KAAK,UAAU,KAAK,EAAE,QAAQ,MAAM,SAAS;AACtD;AAeA,SAAS,aAAa,OAAe,WAA2B;AAC9D,MAAI,UAAU,EAAG,QAAO;AACxB,SAAO,eAAe,SAAS,wBAAwB,KAAK;AAC9D;AAOA,SAAS,UAAU,OAA8B,cAA8B;AAC7E,QAAM,SAAS,MAAM,OAAO,SAAY,aAAa,WAAW,MAAM,EAAE,CAAC,MAAM;AAC/E,QAAM,QAAQ,MAAM,cAAc,4DAA4D;AAC9F,QAAM,SAAS,aAAa,cAAc,kBAAkB;AAC5D,SAAO,sCAAsC,MAAM,KAAK,iBAAiB,MAAM,KAAK,IAAI,MAAM,IAAI,KAAK,GAAG,MAAM,GAAG,MAAM,GAAG;AAC9H;AAMA,SAAS,iBAAiB,OAAsC;AAC9D,QAAM,QAAQ,CAAC,SAAS,MAAM,QAAQ,CAAC,IAAI,MAAM,IAAI;AACrD,MAAI,MAAM,OAAO,OAAW,OAAM,KAAK,MAAM,EAAE;AAC/C,MAAI,MAAM,YAAa,OAAM,KAAK,UAAU;AAC5C,SAAO,WAAW,MAAM,KAAK,QAAK,CAAC;AACrC;AAIA,SAAS,cAAc,OAAsC;AAC3D,QAAM,SAAS,MAAM,OAAO,SAAY,SAAM,MAAM,EAAE,KAAK;AAC3D,SAAO,WAAW,GAAG,MAAM,QAAQ,CAAC,GAAG,MAAM,EAAE;AACjD;AAKA,SAAS,YAAY,OAA8B,OAAuB;AACxE,QAAM,SAAS,MAAM,OAAO,SAAY,SAAM,MAAM,EAAE,KAAK;AAC3D,SAAO,WAAW,GAAG,MAAM,QAAQ,CAAC,MAAM,KAAK,GAAG,MAAM,EAAE;AAC5D;AASA,SAAS,YAAY,OAA8B,UAAmB,aAAqB,cAA8B;AACvH,QAAM,cAAc,iBAAiB,KAAK;AAC1C,QAAM,QAAQ,MAAM,cAAc,oEAAoE;AACtG,QAAM,SAAS,aAAa,cAAc,wBAAwB;AAClE,SACE,wCAAwC,WAAW,qBAAqB,EAAE,kBAAkB,MAAM,KAAK,iBACxF,MAAM,KAAK,YAAY,WAAW,iBAAiB,WAAW,6CAClC,MAAM,KAAK,KAAK,WAAW,uCACtC,cAAc,KAAK,CAAC,UACjD,KAAK,GAAG,MAAM;AAErB;AAOA,SAAS,kBAAkB,GAA+B;AACxD,QAAM,SAAS,EAAE,YAAY,SAAY,SAAM,WAAW,EAAE,OAAO,CAAC,KAAK;AACzE,SACE,6DAA6D,EAAE,OAAO,CAAC,uCAClC,EAAE,IAAI,GAAG,MAAM,yCAClB,WAAW,EAAE,IAAI,CAAC,yCACpB,WAAW,EAAE,OAAO,CAAC;AAGzD;AAEA,IAAM,MAAM;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAyCV,KAAK;AAEP,IAAM,KAAK;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EA4JT,KAAK;AAQP,IAAM,iBAAiB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAahB,SAAS,iBAAiB,OAAiC;AAChE,QAAM,EAAE,OAAO,QAAQ,WAAW,CAAC,GAAG,WAAW,OAAO,IAAI;AAC5D,QAAM,QAAQ,OAAO;AACrB,QAAM,eAAe,WAAW,KAAK;AAKrC,QAAM,iBAAiB,oBAAI,IAAkC;AAC7D,aAAW,KAAK,UAAU;AACxB,UAAM,OAAO,eAAe,IAAI,EAAE,IAAI;AACtC,QAAI,KAAM,MAAK,KAAK,CAAC;AAAA,QAChB,gBAAe,IAAI,EAAE,MAAM,CAAC,CAAC,CAAC;AAAA,EACrC;AACA,QAAM,WAAW,CAAC,UAAiC,eAAe,IAAI,MAAM,QAAQ,CAAC,GAAG,UAAU;AAElG,QAAM,aAAa,QAAQ,IAAI,UAAU,OAAO,CAAC,GAAI,SAAS,OAAO,CAAC,CAAE,CAAC,IAAI;AAC7E,QAAM,SAAS,OACZ,IAAI,CAAC,GAAG,MAAM,YAAY,GAAG,MAAM,GAAG,MAAM,IAAI,KAAK,UAAU,GAAG,SAAS,CAAC,CAAC,GAAG,SAAS,CAAC,CAAC,CAAC,EAC5F,KAAK,EAAE;AACV,QAAM,iBAAiB,QAAQ,IAAI,YAAY,OAAO,CAAC,GAAI,KAAK,IAAI;AAQpE,QAAM,aACJ,SAAS,SAAS,IACd,oDAAoD,SAAS,MAAM,iCAAiC,SAAS,IAAI,iBAAiB,EAAE,KAAK,EAAE,CAAC,qBAC5I;AACN,QAAM,qBACJ,SAAS,SAAS,IACd,0DAA0D,UAAU,QAAQ,CAAC,cAC7E;AACN,QAAM,gBAAgB,cAAc,SAAY,4BAA4B,WAAW,SAAS,CAAC,YAAY;AAW7G,QAAM,aACJ,WAAW,SACP,+CAA+C,OAAO,GAAG,gBAAa,OAAO,MAAM,WACnF;AAEN,SAAO;AAAA;AAAA;AAAA;AAAA;AAAA,SAKA,YAAY;AAAA,SACZ,GAAG;AAAA;AAAA;AAAA;AAAA,sBAIU,YAAY;AAAA,wBACV,cAAc;AAAA,EACpC,aAAa;AAAA;AAAA;AAAA,6CAG8B,UAAU,6BAA6B,UAAU,GAAG,UAAU,GAAG,cAAc;AAAA,6CAC/E,MAAM;AAAA,EACjD,kBAAkB;AAAA,UACV,EAAE;AAAA;AAAA;AAAA;AAIZ;;;ALliBA,eAAe,cAAc,MAAsC;AACjE,QAAM,MAAM,MAAM,WAAW,IAAI;AACjC,QAAM,IAAI,oBAAoB,UAAU,GAAG;AAC3C,MAAI,CAAC,EAAE,SAAS;AACd,UAAM,SAAS,EAAE,MAAM,OACpB,IAAI,CAAC,MAAM,GAAG,EAAE,KAAK,KAAK,GAAG,KAAK,QAAQ,KAAK,EAAE,OAAO,EAAE,EAC1D,KAAK,IAAI;AACZ,UAAM,IAAI,aAAa,sBAAsB,IAAI;AAAA,EAAM,MAAM,EAAE;AAAA,EACjE;AACA,SAAO,EAAE;AACX;AAMA,SAAS,oBACP,MACA,YACA,SACQ;AACR,MAAI,KAAK,UAAU,OAAW,QAAO;AACrC,MAAI,YAAY,OAAO,UAAU,OAAW,QAAO,WAAW;AAC9D,MAAI,SAAS,OAAO,UAAU,OAAW,QAAO,QAAQ;AACxD,SAAO;AACT;AAwBA,SAAS,sBACP,YACA,SACmC;AACnC,MAAI,YAAY,OAAO,aAAa,QAAW;AAC7C,WAAO,EAAE,UAAUE,SAAQC,SAAQ,WAAW,IAAI,GAAG,WAAW,OAAO,QAAQ,EAAE;AAAA,EACnF;AACA,SAAO,SAAS;AAClB;AA+BA,eAAsB,gBACpB,KACA,MACe;AACf,MAAI,OAAO,QAAQ,YAAY,QAAQ,KAAM;AAC7C,QAAM,OAAO;AACb,QAAM,UACJ,OAAO,KAAK,UAAU,YAAY,KAAK,UAAU,OAC5C,KAAK,QACN,CAAC;AACP,QAAM,CAAC,YAAY,OAAO,IAAI,MAAM,QAAQ,IAAI;AAAA,IAC9C,KAAK,eAAe,SAAY,QAAQ,QAAQ,KAAK,UAAU,IAAI,WAAW,KAAK,GAAG;AAAA,IACtF,KAAK,YAAY,SAAY,QAAQ,QAAQ,KAAK,OAAO,IAAI,eAAe;AAAA,EAC9E,CAAC;AACD,QAAM,QAAQ,KAAK,SAAS,YAAY,OAAO,SAAS,SAAS,OAAO,SAAU,QAAQ;AAC1F,QAAM,QAAQ,KAAK,YACf,MAAM,cAAc,KAAK,SAAS,IACjC,YAAY,OAAO,SAAS,SAAS,OAAO,SAAS,QAAQ;AAClE,MAAI,UAAU,QAAW;AACvB,UAAM,oBAAoB,qBAAqB;AAC/C,QAAI,CAAC,kBAAkB,SAAS,KAAK,GAAG;AACtC,YAAM,IAAI;AAAA,QACR,kBAAkB,KAAK,WAAW,oBAAoB,MAAM,YAAY,OAAO,CAAC,uBAAkB,kBAAkB,KAAK,IAAI,CAAC;AAAA,MAChI;AAAA,IACF;AAAA,EACF;AACA,MAAI,UAAU,UAAa,UAAU,OAAW;AAChD,OAAK,QAAQ,EAAE,GAAG,SAAS,IAAI,OAAO,GAAI,UAAU,SAAY,EAAE,MAAM,IAAI,CAAC,EAAG;AAClF;AA8BA,eAAe,eACb,KACA,KACA,YACA,SAC4D;AAC5D,QAAM,SAAS,MAAM,kBAAkB,KAAK,sBAAsB,YAAY,OAAO,GAAG,GAAG;AAC3F,MAAI,MAAM,gBAAgB,MAAM,GAAG;AACjC,UAAM,EAAE,IAAI,QAAQ,IAAI,MAAM,YAAY,MAAM;AAChD,WAAO,EAAE,KAAK,IAAI,SAAS,SAAS,OAAO,KAAK;AAAA,EAClD;AACA,QAAM,MAAM,MAAM,WAAW,MAAM;AACnC,SAAO,EAAE,KAAK,SAASA,SAAQD,SAAQ,MAAM,CAAC,GAAG,OAAO,MAAM;AAChE;AA6BA,eAAsB,UAAU,QAAgB,MAAsC;AACpF,QAAM,MAAM,KAAK,OAAO,QAAQ,IAAI;AACpC,QAAM,CAAC,YAAY,OAAO,IAAI,MAAM,QAAQ,IAAI,CAAC,WAAW,GAAG,GAAG,eAAe,CAAC,CAAC;AACnF,QAAM,EAAE,KAAK,QAAQ,IAAI,MAAM,eAAe,QAAQ,KAAK,YAAY,OAAO;AAC9E,QAAM,gBAAgB,KAAK,EAAE,OAAO,KAAK,OAAO,WAAW,KAAK,WAAW,KAAK,YAAY,QAAQ,CAAC;AACrG,QAAM,IAAI,WAAW,GAAG;AACxB,MAAI,CAAC,EAAE,GAAI,OAAM,IAAI,aAAa;AAAA,EAAgB,aAAa,EAAE,MAAM,CAAC,EAAE;AAC1E,QAAM,mBAAmB,EAAE,IAAK,OAAO;AACvC,QAAM,QAAQ,MAAM,aAAa,EAAE,IAAK,EAAE,OAAO,KAAK,MAAM,CAAC;AAC7D,QAAME,OAAMD,SAAQD,SAAQ,KAAK,MAAM,CAAC,GAAG,EAAE,WAAW,KAAK,CAAC;AAC9D,QAAMG,WAAU,KAAK,QAAQ,KAAK;AAClC,QAAM,KAAK,SAAS,KAAK,MAAM,KAAK,EAAE,GAAI,OAAO,MAAM,YAAY,MAAM,MAAM;AAC/E,QAAM,QAAQ,CAAC,aAAa,EAAE,QAAQ,GAAG,eAAe,EAAE,UAAU,CAAC,EAAE,OAAO,CAAC,MAAmB,MAAM,MAAS;AACjH,SAAO,MAAM,SAAS,IAAI,GAAG,EAAE;AAAA,EAAK,MAAM,KAAK,IAAI,CAAC,KAAK;AAC3D;AAiBA,SAAS,eAAe,YAAsD;AAC5E,MAAI,CAAC,cAAc,WAAW,WAAW,EAAG,QAAO;AACnD,QAAM,IAAI,WAAW;AACrB,SAAO,SAAS,CAAC,eAAe,MAAM,IAAI,KAAK,IAAI;AAAA,EAAgB,WAAW,IAAI,CAAC,SAAS,KAAK,IAAI,EAAE,EAAE,KAAK,IAAI,CAAC;AACrH;AAcA,SAAS,aAAa,UAA6D;AACjF,MAAI,CAAC,YAAY,SAAS,WAAW,EAAG,QAAO;AAC/C,SAAO,eAAe,QAAQ;AAChC;AAYA,SAAS,gBAAgB,IAAgC;AACvD,QAAM,eAAe,GAAG,OACrB,IAAI,CAAC,OAAO,OAAO,EAAE,OAAO,MAAM,IAAI,EAAE,EAAE,EAC1C,OAAO,CAAC,EAAE,MAAM,MAAM,MAAM,WAAW;AAC1C,MAAI,aAAa,WAAW,EAAG,QAAO;AACtC,QAAM,OAAO,aACV,IAAI,CAAC,EAAE,OAAO,KAAK,MAAO,MAAM,KAAK,GAAG,MAAM,EAAE,UAAU,IAAI,MAAM,QAAQ,IAAI,EAAG,EACnF,KAAK,IAAI;AACZ,SAAO,SAAS,aAAa,MAAM,6BAA6B,aAAa,WAAW,IAAI,KAAK,GAAG,KAAK,IAAI;AAC/G;AAuCA,eAAsB,YAAY,QAAgB,MAAM,QAAQ,IAAI,GAAoB;AACtF,QAAM,CAAC,YAAY,OAAO,IAAI,MAAM,QAAQ,IAAI,CAAC,WAAW,GAAG,GAAG,eAAe,CAAC,CAAC;AACnF,QAAM,EAAE,KAAK,SAAS,MAAM,IAAI,MAAM,eAAe,QAAQ,KAAK,YAAY,OAAO;AACrF,QAAM,gBAAgB,KAAK,EAAE,KAAK,YAAY,QAAQ,CAAC;AACvD,QAAM,IAAI,WAAW,GAAG;AACxB,MAAI,CAAC,EAAE;AACL,UAAM,IAAI;AAAA,MACR,eAAe,EAAE,OAAO,MAAM,SAAS,EAAE,OAAO,WAAW,IAAI,KAAK,GAAG;AAAA,EAAO,aAAa,EAAE,MAAM,CAAC;AAAA,IACtG;AACF,QAAM,mBAAmB,EAAE,IAAK,OAAO;AACvC,QAAM,KAAK,aAAQ,EAAE,GAAI,OAAO,MAAM,mBAAmB,EAAE,GAAI,MAAM,EAAE;AACvE,QAAM,QAAkB,CAAC;AACzB,QAAM,WAAW,aAAa,EAAE,QAAQ;AACxC,MAAI,SAAU,OAAM,KAAK,QAAQ;AACjC,QAAM,YAAY,eAAe,EAAE,UAAU;AAC7C,MAAI,UAAW,OAAM,KAAK,SAAS;AACnC,MAAI,OAAO;AACT,UAAM,OAAO,gBAAgB,EAAE,EAAG;AAClC,QAAI,KAAM,OAAM,KAAK,IAAI;AAAA,EAC3B;AACA,SAAO,MAAM,SAAS,IAAI,GAAG,EAAE;AAAA,EAAK,MAAM,KAAK,IAAI,CAAC,KAAK;AAC3D;AAYA,SAAS,mBAAmB,GAAyB;AACnD,QAAM,WAAW,EAAE,YAAY,SAAY,KAAK,EAAE,OAAO,MAAM;AAC/D,SAAO,QAAQ,EAAE,IAAI,GAAG,QAAQ,MAAM,EAAE,IAAI,KAAK,EAAE,OAAO;AAC5D;AA0BA,SAAS,kBAAkB,QAAqB,IAAoB;AAClE,QAAM,QAAQ,OAAO,SAAS,IAAI,kBAAkB;AACpD,QAAM;AAAA,IACJ,WAAW,OAAO,YAAY,QAAQ,OAAO,iBAAiB,IAAI,KAAK,GAAG,KAAK,OAAO,YAAY,aAAa,OAAO,SAAS,MAAM,WAAW,OAAO,SAAS,WAAW,IAAI,KAAK,GAAG;AAAA,EACzL;AACA,MAAI,OAAO,OAAO,WAAW,aAAa;AACxC,UAAM,KAAK,iCAAiC;AAAA,EAC9C;AACA,QAAM,OAAO,gBAAgB,EAAE;AAC/B,MAAI,KAAM,OAAM,KAAK,IAAI;AACzB,SAAO,MAAM,KAAK,IAAI;AACxB;AA0DA,eAAsB,SAAS,QAAgB,OAAqB,CAAC,GAA4B;AAC/F,QAAM,MAAM,KAAK,OAAO,QAAQ,IAAI;AACpC,QAAM,CAAC,YAAY,OAAO,IAAI,MAAM,QAAQ,IAAI,CAAC,WAAW,GAAG,GAAG,eAAe,CAAC,CAAC;AACnF,QAAM,EAAE,KAAK,QAAQ,IAAI,MAAM,eAAe,QAAQ,KAAK,YAAY,OAAO;AAC9E,QAAM,gBAAgB,KAAK,EAAE,KAAK,YAAY,QAAQ,CAAC;AACvD,QAAM,IAAI,WAAW,GAAG;AACxB,MAAI,CAAC,EAAE,IAAI;AACT,UAAM,IAAI;AAAA,MACR,eAAe,EAAE,OAAO,MAAM,SAAS,EAAE,OAAO,WAAW,IAAI,KAAK,GAAG;AAAA,EAAO,aAAa,EAAE,MAAM,CAAC;AAAA,IACtG;AAAA,EACF;AACA,QAAM,mBAAmB,EAAE,IAAK,OAAO;AACvC,QAAM,SAAS,KAAK,SAAS,MAAM,UAAU,EAAE,IAAK,EAAE,QAAQ,KAAK,CAAC,IAAI,UAAU,EAAE,EAAG;AACvF,QAAM,cAAc,OAAO,SAAS,SAAS;AAC7C,QAAM,SAAS,KAAK,OAAO,KAAK,UAAU,QAAQ,MAAM,CAAC,IAAI,kBAAkB,QAAQ,EAAE,EAAG;AAC5F,SAAO,EAAE,QAAQ,YAAY;AAC/B;AAUA,eAAsB,gBAAgB,UAAmC;AACvE,QAAM,MAAM,MAAM,WAAW,UAAU,MAAM;AAC7C,QAAM,IAAI,aAAa,GAAG;AAC1B,MAAI,CAAC,EAAE,IAAI;AACT,UAAM,IAAI,aAAa,uBAAuB,EAAE,MAAM,CAAC;AAAA,EACzD;AACA,QAAMC,QAAO,EAAE;AAGf,QAAM,OAAO,iBAAiBA,MAAK,SAA2D;AAC9F,SAAO,aAAQA,MAAK,MAAM,MAAM,qBAAqB,KAAK,QAAQ,IAAI,KAAK,MAAM,IAAI,KAAK,QAAQ,YAAY,mBAAmBA,KAAI,CAAC;AACxI;AAOO,SAAS,UAAU,MAAiC;AACzD,QAAM,SAAS,SAAS,UAAU,gBAAgB,IAAI,SAAS,SAAS,eAAe,IAAI,aAAa;AACxG,SAAO,KAAK,UAAU,QAAQ,MAAM,CAAC;AACvC;AAEO,SAAS,UAAU,QAAyB;AACjD,QAAM,SAAS,WAAW;AAC1B,MAAI,OAAQ,QAAO,KAAK,UAAU,QAAQ,MAAM,CAAC;AACjD,SAAO,OAAO,IAAI,CAAC,MAAM,GAAG,EAAE,GAAG,OAAO,EAAE,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE,KAAK,IAAI;AACrE;AAkBO,SAAS,cAAc,QAAyB;AACrD,MAAI,QAAQ;AACV,WAAO,KAAK;AAAA,MACV;AAAA,QACE,SAAS;AAAA,QACT,YAAY;AAAA,QACZ,SAAS;AAAA,QACT,WAAW;AAAA,MACb;AAAA,MACA;AAAA,MACA;AAAA,IACF;AAAA,EACF;AACA,QAAM,OAAO,OAAO,OAAO,iBAAiB,EAAE,IAAI,CAAC,OAAO;AAAA,IACxD,IAAI,EAAE;AAAA,IACN,MAAM,GAAG,EAAE,KAAK,QAAQ,IAAI,EAAE,KAAK,MAAM,IAAI,EAAE,KAAK,QAAQ;AAAA,IAC5D,QAAQ,EAAE,qBAAqB,KAAK,IAAI;AAAA,EAC1C,EAAE;AACF,QAAM,UAAU,KAAK,IAAI,GAAG,KAAK,IAAI,CAAC,MAAM,EAAE,GAAG,MAAM,CAAC;AACxD,QAAM,YAAY,KAAK,IAAI,GAAG,KAAK,IAAI,CAAC,MAAM,EAAE,KAAK,MAAM,CAAC;AAC5D,SAAO,KACJ,IAAI,CAAC,MAAM,GAAG,EAAE,GAAG,OAAO,UAAU,CAAC,CAAC,GAAG,EAAE,KAAK,OAAO,YAAY,CAAC,CAAC,GAAG,EAAE,MAAM,EAAE,EAClF,KAAK,IAAI;AACd;AAEA,IAAM,kBAAkB;AAAA,EACtB,OAAO;AAAA,EACP,OAAO;AAAA,IACL,QAAQ,EAAE,SAAS,WAAW,QAAQ,UAAU;AAAA,EAClD;AACF;AAGA,eAAsB,QAAQ,MAAM,QAAQ,IAAI,GAAoB;AAClE,QAAM,SAASC,MAAK,KAAK,eAAe;AACxC,MAAI;AACF,UAAMF,WAAU,QAAQ,KAAK,UAAU,iBAAiB,MAAM,CAAC,IAAI,MAAM,EAAE,MAAM,KAAK,CAAC;AAAA,EACzF,SAAS,GAAG;AACV,QAAK,EAA4B,SAAS,UAAU;AAClD,YAAM,IAAI,aAAa,GAAG,MAAM,wCAAmC;AAAA,IACrE;AACA,UAAM;AAAA,EACR;AACA,SAAO,SAAS,MAAM;AACxB;AAiEA,eAAsB,WAAW,QAAgB,QAAgB,OAAuB,CAAC,GAAoB;AAC3G,QAAM,MAAM,KAAK,OAAO,QAAQ,IAAI;AACpC,QAAM,CAAC,YAAY,OAAO,IAAI,MAAM,QAAQ,IAAI,CAAC,WAAW,GAAG,GAAG,eAAe,CAAC,CAAC;AACnF,QAAM,EAAE,KAAK,QAAQ,IAAI,MAAM,eAAe,QAAQ,KAAK,YAAY,OAAO;AAC9E,QAAM,gBAAgB,KAAK,EAAE,KAAK,YAAY,QAAQ,CAAC;AACvD,QAAM,IAAI,WAAW,GAAG;AACxB,MAAI,CAAC,EAAE,GAAI,OAAM,IAAI,aAAa;AAAA,EAAgB,aAAa,EAAE,MAAM,CAAC,EAAE;AAC1E,QAAM,mBAAmB,EAAE,IAAK,OAAO;AACvC,QAAMD,OAAM,QAAQ,EAAE,WAAW,KAAK,CAAC;AACvC,QAAM,KAAK,EAAE;AACb,QAAM,OAAiB,CAAC;AACxB,WAAS,IAAI,GAAG,IAAI,GAAG,OAAO,QAAQ,KAAK;AACzC,UAAM,MAAM,eAAe,IAAI,CAAC;AAChC,SAAK,KAAK,GAAG;AACb,UAAM,OAAO,GAAG,OAAO,IAAI,CAAC,EAAE,SAAS,GAAG,GAAG,CAAC,IAAI,GAAG,OAAO,CAAC,EAAG,IAAI;AACpE,UAAMC,WAAUE,MAAK,QAAQ,IAAI,GAAG,GAAG;AAAA,EACzC;AACA,QAAM,KAAK,SAAS,GAAG,OAAO,MAAM,iBAAiB,MAAM;AAC3D,QAAM,QAAkB,CAAC;AACzB,QAAM,YAAY,eAAe,EAAE,UAAU;AAC7C,MAAI,UAAW,OAAM,KAAK,SAAS;AACnC,MAAI,KAAK,SAAS;AAChB,UAAM,WAAWA,MAAK,QAAQ,cAAc;AAC5C,UAAM,iBAAiB,GAAG,OAAO,KAAK,CAAC,UAAU,MAAM,WAAW;AAClE,UAAM,cAAc,iBAAiB,SAAY,UAAU,EAAE;AAC7D,UAAM,gBAAgB,aAAa,YAAY,CAAC;AAChD,UAAM,OAAO,iBAAiB;AAAA,MAC5B,OAAO,GAAG;AAAA,MACV,QAAQ,GAAG,OAAO,IAAI,CAAC,OAAO,OAAO;AAAA,QACnC,OAAO;AAAA,QACP,IAAI,MAAM;AAAA,QACV,MAAM,MAAM;AAAA,QACZ,KAAK,KAAK,CAAC;AAAA,QACX,aAAa,MAAM;AAAA,MACrB,EAAE;AAAA,MACF,UAAU,cAAc,IAAI,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,SAAS,EAAE,SAAS,MAAM,EAAE,MAAM,SAAS,EAAE,QAAQ,EAAE;AAAA,MAC3G,WAAW,iBACP,gJACA;AAAA,MACJ,QAAQ,aAAa;AAAA,IACvB,CAAC;AACD,UAAMF,WAAU,UAAU,IAAI;AAC9B,UAAM,KAAK,yCAAyC,QAAQ,EAAE;AAC9D,QAAI,cAAc,SAAS,GAAG;AAC5B,YAAM,KAAK,qBAAqB,cAAc,MAAM,WAAW,cAAc,WAAW,IAAI,KAAK,GAAG,0BAAqB;AAAA,IAC3H;AAAA,EACF;AACA,SAAO,MAAM,SAAS,IAAI,GAAG,EAAE;AAAA,EAAK,MAAM,KAAK,IAAI,CAAC,KAAK;AAC3D;AAqBA,SAAS,wBAAwB,IAAY,SAAiB,QAAwB;AACpF,QAAM,SAAS,OAAO;AAAA,IACpB,OAAO,QAAQ,GAAG,OAAO,MAAM,EAAE,IAAI,CAAC,CAAC,IAAI,KAAK,MAAM;AACpD,UAAI,MAAM,IAAI,WAAW,OAAO,KAAK,eAAe,KAAK,MAAM,GAAG,EAAG,QAAO,CAAC,IAAI,KAAK;AACtF,aAAO,CAAC,IAAI,EAAE,GAAG,OAAO,KAAKG,UAAS,QAAQD,MAAK,SAAS,MAAM,GAAG,CAAC,EAAE,CAAC;AAAA,IAC3E,CAAC;AAAA,EACH;AACA,SAAO,EAAE,GAAG,IAAI,QAAQ,EAAE,OAAO,EAAE;AACrC;AAiDA,eAAsB,YAAY,QAAgB,OAAwB,CAAC,GAAoB;AAC7F,QAAM,MAAM,KAAK,OAAO,QAAQ,IAAI;AACpC,QAAM,CAAC,YAAY,OAAO,IAAI,MAAM,QAAQ,IAAI,CAAC,WAAW,GAAG,GAAG,eAAe,CAAC,CAAC;AACnF,QAAM,MAAM,MAAM,kBAAkB,QAAQ,sBAAsB,YAAY,OAAO,GAAG,GAAG;AAC3F,MAAK,MAAM,WAAW,GAAG,KAAM,CAAE,MAAM,gBAAgB,GAAG,GAAI;AAC5D,UAAM,IAAI,aAAa,sCAAsC,GAAG,EAAE;AAAA,EACpE;AACA,QAAM,EAAE,IAAI,eAAe,yBAAyB,QAAQ,IAAI,MAAM,YAAY,GAAG;AACrF,QAAM,UAAU,KAAK,SAASL,SAAQ,KAAK,KAAK,MAAM,IAAIK,MAAK,SAAS,WAAW;AACnF,QAAM,SAASJ,SAAQ,OAAO;AAC9B,QAAM,QAAQ,WAAW,UAAU,KAAK,wBAAwB,IAAI,SAAS,MAAM;AACnF,QAAMC,OAAM,QAAQ,EAAE,WAAW,KAAK,CAAC;AACvC,QAAMC,WAAU,SAAS,KAAK,UAAU,OAAO,MAAM,CAAC,IAAI,IAAI;AAC9D,QAAM,mBAAmB,MAAM,OAAO,OAAO,CAAC,MAAM,EAAE,WAAW,EAAE;AACnE,QAAM,UAAU,SAAS,OAAO,KAAK,MAAM,OAAO,MAAM,YAAY,gBAAgB,eAAe,qBAAqB,IAAI,KAAK,GAAG;AACpI,QAAM,QAAkB,CAAC;AACzB,MAAI,kBAAkB,QAAW;AAC/B,UAAM,KAAK,wBAAwB,aAAa,uBAAkB,aAAa,2CAA2C;AAAA,EAC5H;AACA,MAAI,4BAA4B,QAAW;AACzC,UAAM;AAAA,MACJ,SAAS,uBAAuB,UAAU,4BAA4B,IAAI,KAAK,GAAG;AAAA,IACpF;AAAA,EACF;AACA,SAAO,CAAC,SAAS,GAAG,KAAK,EAAE,KAAK,IAAI;AACtC;AAsDA,eAAsB,eAAe,QAAgB,QAAiC;AACpF,QAAM,MAAM,MAAM,WAAW,MAAM;AACnC,QAAM,IAAI,WAAW,GAAG;AACxB,MAAI,CAAC,EAAE,GAAI,OAAM,IAAI,aAAa;AAAA,EAAgB,aAAa,EAAE,MAAM,CAAC,EAAE;AAC1E,QAAM,EAAE,MAAAC,OAAM,MAAM,IAAI,gBAAgB,EAAE,EAAG;AAU7C,QAAM,MAAM,OAAO,KAAK,KAAK;AAC7B,aAAW,MAAM,IAAK,uBAAsB,IAAI,UAAU;AAE1D,QAAM,WAAWC,MAAK,QAAQ,gBAAgB;AAC9C,QAAMH,OAAM,QAAQ,EAAE,WAAW,KAAK,CAAC;AACvC,MAAI;AACF,UAAMC,WAAU,UAAU,KAAK,UAAUC,OAAM,MAAM,CAAC,IAAI,MAAM,EAAE,MAAM,KAAK,CAAC;AAAA,EAChF,SAAS,GAAG;AACV,QAAK,EAA4B,SAAS,UAAU;AAClD,YAAM,IAAI,aAAa,GAAG,QAAQ,uEAAkE;AAAA,IACtG;AACA,UAAM;AAAA,EACR;AAOA,QAAM,WAAWC,MAAK,QAAQ,OAAO;AACrC,MAAI;AACF,QAAI,IAAI,SAAS,GAAG;AAClB,YAAMH,OAAM,UAAU,EAAE,WAAW,KAAK,CAAC;AACzC,YAAM,QAAQ;AAAA,QACZ,IAAI,IAAI,CAAC,OAAO;AACd,gBAAM,UAAuB,MAAM,EAAE;AACrC,iBAAOC,WAAUE,MAAK,UAAU,GAAG,EAAE,OAAO,GAAG,KAAK,UAAU,SAAS,MAAM,CAAC,IAAI,IAAI;AAAA,QACxF,CAAC;AAAA,MACH;AAAA,IACF;AAEA,UAAM,EAAE,OAAO,YAAY,UAAU,IAAI,MAAM;AAAA,MAC7C,EAAE,GAAI,OAAO;AAAA,MACb;AAAA,MACAJ,SAAQD,SAAQ,MAAM,CAAC;AAAA,IACzB;AAEA,UAAM,YACJ,IAAI,SAAS,IACT,GAAG,IAAI,MAAM,aAAa,IAAI,WAAW,IAAI,KAAK,GAAG,OAAO,QAAQ,KACpE;AACN,UAAM,aAAa,aAAa,IAAI,SAAS,UAAU,cAAc,eAAe,IAAI,KAAK,GAAG,OAAO,SAAS,KAAK;AACrH,WAAO,SAAS,QAAQ,KAAK,SAAS,GAAG,UAAU;AAAA,EACrD,SAAS,GAAG;AAGV,UAAM,GAAG,UAAU,EAAE,OAAO,KAAK,CAAC,EAAE,MAAM,MAAM;AAAA,IAAC,CAAC;AAClD,UAAM;AAAA,EACR;AACF;AA+BA,eAAsB,WAAW,OAAe,QAAgB,MAAM,QAAQ,IAAI,GAAoB;AACpG,QAAM,gBAAgBA,SAAQ,KAAK,KAAK;AACxC,MAAI,MAAM,gBAAgB,aAAa,GAAG;AACxC,WAAO,kBAAkB,eAAe,QAAQ,GAAG;AAAA,EACrD;AACA,SAAO,iBAAiB,eAAe,QAAQ,GAAG;AACpD;AAgCA,eAAe,kBAAkB,KAAa,QAAgB,KAA8B;AAC1F,QAAM,WAAWK,MAAK,KAAK,aAAa;AACxC,QAAM,iBAAiBA,MAAK,KAAK,aAAa;AAC9C,MAAI,CAAE,MAAM,WAAW,QAAQ,KAAO,MAAM,WAAW,cAAc,GAAI;AACvE,UAAM,IAAI;AAAA,MACR,GAAG,GAAG,QAAQ,aAAa,WAAW,aAAa;AAAA,IACrD;AAAA,EACF;AACA,QAAM,MAAM,MAAM,WAAW,UAAU,MAAM;AAC7C,QAAM,WAAW,sBAAsB,GAAG;AAC1C,QAAM,SAASL,SAAQ,KAAK,MAAM;AAClC,QAAM,WAAWK,MAAK,QAAQ,aAAa;AAC3C,QAAMH,OAAM,QAAQ,EAAE,WAAW,KAAK,CAAC;AACvC,MAAI;AACF,UAAMC,WAAU,UAAU,KAAK,UAAU,UAAU,MAAM,CAAC,IAAI,MAAM,EAAE,MAAM,KAAK,CAAC;AAAA,EACpF,SAAS,GAAG;AACV,QAAK,EAA4B,SAAS,UAAU;AAClD,YAAM,IAAI,aAAa,GAAG,QAAQ,wFAAmF;AAAA,IACvH;AACA,UAAM;AAAA,EACR;AACA,SAAO,SAAS,QAAQ,uCAAkC,QAAQ,iCAAiC,QAAQ;AAC7G;AAaA,eAAe,iBAAiB,UAAkB,QAAgB,KAA8B;AAC9F,QAAM,MAAM,MAAM,WAAW,QAAQ;AACrC,QAAM,UAAU,OAAO,QAAQ,YAAY,QAAQ,OAAQ,IAAgC,UAAU;AACrG,MAAI,YAAY,KAAK;AACnB,UAAM,IAAI;AAAA,MACR;AAAA,IACF;AAAA,EACF;AACA,MAAI,YAAY,KAAK;AACnB,UAAM,IAAI;AAAA,MACR,qGAAqG,aAAa,uBAAkB,KAAK,UAAU,OAAO,CAAC,OAAO,QAAQ;AAAA,IAC5K;AAAA,EACF;AACA,QAAM,SAAS,eAAe,UAAU,GAAG;AAC3C,MAAI,CAAC,OAAO,SAAS;AACnB,UAAM,SAAS,OAAO,MAAM,OAAO,IAAI,CAAC,UAAU,GAAG,MAAM,KAAK,KAAK,GAAG,KAAK,QAAQ,KAAK,MAAM,OAAO,EAAE,EAAE,KAAK,IAAI;AACpH,UAAM,IAAI,aAAa,sBAAsB,QAAQ;AAAA,EAAM,MAAM,EAAE;AAAA,EACrE;AACA,QAAM,WAAW,gBAAgB,OAAO,IAAI;AAC5C,QAAM,UAAUH,SAAQ,KAAK,MAAM;AACnC,QAAME,OAAMD,SAAQ,OAAO,GAAG,EAAE,WAAW,KAAK,CAAC;AACjD,MAAI;AACF,UAAME,WAAU,SAAS,KAAK,UAAU,UAAU,MAAM,CAAC,IAAI,MAAM,EAAE,MAAM,KAAK,CAAC;AAAA,EACnF,SAAS,GAAG;AACV,QAAK,EAA4B,SAAS,UAAU;AAClD,YAAM,IAAI,aAAa,GAAG,OAAO,wFAAmF;AAAA,IACtH;AACA,UAAM;AAAA,EACR;AACA,SAAO,SAAS,OAAO;AACzB;;;AMniCA,SAAS,gBAAgB;AAElB,IAAM,eAAe;AAiB5B,SAAS,iBAAiB,SAAyB;AACjD,QAAM,aAAa,QAAQ,KAAK,EAAE,QAAQ,OAAO,EAAE,EAAE,MAAM,GAAG,EAAE,CAAC;AACjE,MAAI,CAAC,WAAY,OAAM,IAAI,MAAM,oBAAoB,OAAO,EAAE;AAC9D,SAAO;AACT;AAEA,SAAS,aAAa,SAA2B;AAC/C,SAAO,iBAAiB,OAAO,EAC5B,MAAM,GAAG,EACT,IAAI,CAAC,YAAY;AAChB,UAAM,QAAQ,OAAO,SAAS,SAAS,EAAE;AACzC,QAAI,CAAC,OAAO,SAAS,KAAK,EAAG,OAAM,IAAI,MAAM,4BAA4B,OAAO,EAAE;AAClF,WAAO;AAAA,EACT,CAAC;AACL;AAEO,SAAS,gBAAgB,MAAc,OAAuB;AACnE,QAAM,IAAI,aAAa,IAAI;AAC3B,QAAM,IAAI,aAAa,KAAK;AAC5B,WAAS,IAAI,GAAG,IAAI,KAAK,IAAI,EAAE,QAAQ,EAAE,MAAM,GAAG,KAAK;AACrD,UAAM,IAAI,EAAE,CAAC,KAAK;AAClB,UAAM,IAAI,EAAE,CAAC,KAAK;AAClB,QAAI,MAAM,EAAG,QAAO,IAAI,IAAI,KAAK;AAAA,EACnC;AACA,SAAO;AACT;AAEO,IAAM,aAA4B,CAAC,SAAS,SACjD,IAAI,QAAQ,CAACI,UAAS,WAAW;AAC/B,WAAS,SAAS,MAAM,EAAE,UAAU,OAAO,GAAG,CAAC,OAAO,QAAQ,WAAW;AACvE,QAAI,OAAO;AACT,aAAO,IAAI,MAAM,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAChD;AAAA,IACF;AACA,IAAAA,SAAQ,OAAO,KAAK,CAAC;AAAA,EACvB,CAAC;AACH,CAAC;AASH,eAAsB,eAAe;AAAA,EACnC;AAAA,EACA,cAAc;AAAA,EACd,MAAM;AACR,GAA+C;AAC7C,QAAM,UAAU,iBAAiB,cAAc;AAC/C,MAAI;AACF,UAAM,SAAS,iBAAiB,MAAM,IAAI,OAAO,CAAC,QAAQ,aAAa,SAAS,CAAC,CAAC;AAClF,WAAO;AAAA,MACL;AAAA,MACA,gBAAgB;AAAA,MAChB,eAAe;AAAA,MACf,iBAAiB,gBAAgB,SAAS,MAAM,IAAI;AAAA,MACpD,SAAS;AAAA,IACX;AAAA,EACF,SAAS,OAAO;AACd,WAAO;AAAA,MACL;AAAA,MACA,gBAAgB;AAAA,MAChB,eAAe;AAAA,MACf,iBAAiB;AAAA,MACjB,SAAS;AAAA,MACT,OAAO,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;AAAA,IAC9D;AAAA,EACF;AACF;AAEO,SAAS,kBAAkB,MAAqB,YAAY;AACjE,SAAO,OAAO;AAAA,IACZ;AAAA,IACA,cAAc;AAAA,EAChB,MAGiC;AAC/B,UAAM,OAAO,MAAM,eAAe,EAAE,gBAAgB,aAAa,IAAI,CAAC;AACtE,QAAI,CAAC,KAAK,QAAS,OAAM,IAAI,MAAM,gCAAgC,KAAK,SAAS,eAAe,EAAE;AAClG,QAAI,CAAC,KAAK,gBAAiB,QAAO,EAAE,GAAG,MAAM,SAAS,MAAM;AAC5D,UAAM,IAAI,OAAO,CAAC,WAAW,MAAM,GAAG,WAAW,SAAS,CAAC;AAC3D,WAAO,EAAE,GAAG,MAAM,SAAS,KAAK;AAAA,EAClC;AACF;;;APvFA,oBAAoB;AAEpB,IAAM,UAAU,IAAI,QAAQ;AAC5B,QACG,KAAK,SAAS,EACd,YAAY,4FAAuF,EACnG,QAAQ,OAAO;AAElB,SAAS,KAAK,GAAmB;AAC/B,UAAQ,MAAM,aAAa,QAAQ,EAAE,UAAU,OAAO,CAAC,CAAC;AACxD,UAAQ,KAAK,CAAC;AAChB;AAEA,QACG,QAAQ,QAAQ,EAChB,YAAY,8EAA8E,EAC1F,SAAS,YAAY,2EAA2E,EAChG,eAAe,uBAAuB,mBAAmB,EACzD,OAAO,gBAAgB,gDAAgD,EACvE,OAAO,kBAAkB,2EAA2E,EACpG,OAAO,WAAW,wDAAwD,EAC1E,OAAO,OAAO,QAAgB,SAA8E;AAC3G,MAAI;AACF,YAAQ;AAAA,MACN,MAAM,UAAU,QAAQ;AAAA,QACtB,QAAQ,KAAK;AAAA,QACb,OAAO,KAAK;AAAA,QACZ,WAAW,KAAK;AAAA,QAChB,OAAO,KAAK;AAAA,MACd,CAAC;AAAA,IACH;AAAA,EACF,SAAS,GAAG;AACV,SAAK,CAAC;AAAA,EACR;AACF,CAAC;AAEH,QACG,QAAQ,UAAU,EAClB,YAAY,wFAAwF,EACpG,SAAS,YAAY,2EAA2E,EAChG,OAAO,OAAO,WAAmB;AAChC,MAAI;AACF,YAAQ,IAAI,MAAM,YAAY,MAAM,CAAC;AAAA,EACvC,SAAS,GAAG;AACV,SAAK,CAAC;AAAA,EACR;AACF,CAAC;AAEH,QACG,QAAQ,OAAO,EACf;AAAA,EACC;AACF,EACC,SAAS,YAAY,2EAA2E,EAChG,OAAO,UAAU,gDAAgD,EACjE,OAAO,YAAY,mFAAmF,EACtG,OAAO,OAAO,QAAgB,SAA+C;AAC5E,MAAI;AACF,UAAM,EAAE,QAAQ,YAAY,IAAI,MAAM,SAAS,QAAQ,EAAE,MAAM,KAAK,MAAM,QAAQ,KAAK,OAAO,CAAC;AAC/F,YAAQ,IAAI,MAAM;AAClB,QAAI,YAAa,SAAQ,KAAK,CAAC;AAAA,EACjC,SAAS,GAAG;AACV,SAAK,CAAC;AAAA,EACR;AACF,CAAC;AAEH,QACG,QAAQ,QAAQ,EAChB,YAAY,qEAAqE,EACjF,OAAO,WAAW,yCAAyC,EAC3D,OAAO,UAAU,oCAAoC,EACrD,OAAO,UAAU,mCAA8B,EAC/C,OAAO,CAAC,SAA8D;AAIrE,MAAI,KAAK,MAAM;AACb,SAAK,IAAI,MAAM,gHAA2G,CAAC;AAAA,EAC7H;AACA,UAAQ,IAAI,UAAU,KAAK,OAAO,SAAS,KAAK,QAAQ,UAAU,MAAS,CAAC;AAC9E,CAAC;AAMH,IAAM,OAAO,QAAQ,QAAQ,MAAM,EAAE,YAAY,2CAAsC;AACvF,KACG,QAAQ,UAAU,EAClB,YAAY,oDAA+C,EAC3D,SAAS,QAAQ,EACjB,OAAO,MAAM;AACZ,OAAK,IAAI,MAAM,uHAAkH,CAAC;AACpI,CAAC;AAEH,IAAM,OAAO,QAAQ,QAAQ,MAAM,EAAE,YAAY,iCAA8B;AAC/E,KACG,QAAQ,UAAU,EAClB,YAAY,iFAAiF,EAC7F,SAAS,aAAa,EACtB,OAAO,OAAO,aAAqB;AAClC,MAAI;AACF,YAAQ,IAAI,MAAM,gBAAgB,QAAQ,CAAC;AAAA,EAC7C,SAAS,GAAG;AACV,SAAK,CAAC;AAAA,EACR;AACF,CAAC;AAEH,QACG,QAAQ,UAAU,EAClB,YAAY,4FAA4F,EACxG,SAAS,cAAc,6DAA6D,EACpF,OAAO,uBAAuB,gDAAgD,EAC9E,OAAO,OAAO,QAAgB,SAA8B;AAC3D,MAAI;AACF,YAAQ,IAAI,MAAM,YAAY,QAAQ,EAAE,QAAQ,KAAK,OAAO,CAAC,CAAC;AAAA,EAChE,SAAS,GAAG;AACV,SAAK,CAAC;AAAA,EACR;AACF,CAAC;AAEH,QACG,QAAQ,aAAa,EACrB,YAAY,+EAA+E,EAC3F,SAAS,aAAa,qBAAqB,EAC3C,eAAe,sBAAsB,+BAA+B,EACpE,OAAO,OAAO,QAAgB,SAA6B;AAC1D,MAAI;AACF,YAAQ,IAAI,MAAM,eAAe,QAAQ,KAAK,MAAM,CAAC;AAAA,EACvD,SAAS,GAAG;AACV,SAAK,CAAC;AAAA,EACR;AACF,CAAC;AAEH,QACG,QAAQ,SAAS,EACjB,YAAY,oHAA+G,EAC3H,SAAS,WAAW,wEAAwE,EAC5F,eAAe,yBAAyB,wGAAmG,EAC3I,OAAO,OAAO,OAAe,SAA6B;AACzD,MAAI;AACF,YAAQ,IAAI,MAAM,WAAW,OAAO,KAAK,MAAM,CAAC;AAAA,EAClD,SAAS,GAAG;AACV,SAAK,CAAC;AAAA,EACR;AACF,CAAC;AAEH,QACG,QAAQ,QAAQ,EAChB,YAAY,sBAAsB,EAClC,OAAO,UAAU,yBAAyB,EAC1C,OAAO,CAAC,SAA6B,QAAQ,IAAI,UAAU,QAAQ,KAAK,IAAI,CAAC,CAAC,CAAC;AAElF,QACG,QAAQ,YAAY,EACpB,YAAY,sFAAsF,EAClG,OAAO,UAAU,yBAAyB,EAC1C,OAAO,CAAC,SAA6B,QAAQ,IAAI,cAAc,QAAQ,KAAK,IAAI,CAAC,CAAC,CAAC;AAKtF,QACG,QAAQ,WAAW,EACnB,YAAY,iDAA4C,EACxD,OAAO,MAAM;AACZ,OAAK,IAAI,MAAM,sGAAiG,CAAC;AACnH,CAAC;AAEH,QACG,QAAQ,MAAM,EACd,YAAY,yDAAyD,EACrE,OAAO,YAAY;AAClB,MAAI;AACF,YAAQ,IAAI,MAAM,QAAQ,CAAC;AAAA,EAC7B,SAAS,GAAG;AACV,SAAK,CAAC;AAAA,EACR;AACF,CAAC;AAEH,QACG,QAAQ,SAAS,EACjB,YAAY,wDAAwD,EACpE,SAAS,YAAY,2EAA2E,EAChG,eAAe,sBAAsB,kBAAkB,EACvD,OAAO,UAAU,4HAAuH,EACxI,OAAO,OAAO,QAAgB,SAA6C;AAC1E,MAAI;AACF,YAAQ,IAAI,MAAM,WAAW,QAAQ,KAAK,QAAQ,EAAE,SAAS,KAAK,KAAK,CAAC,CAAC;AAAA,EAC3E,SAAS,GAAG;AACV,SAAK,CAAC;AAAA,EACR;AACF,CAAC;AAEH,QACG,QAAQ,cAAc,EACtB,YAAY,uCAAuC,EACnD,OAAO,YAAY;AAClB,QAAM,OAAO,MAAM,eAAe,EAAE,gBAAgB,QAAQ,CAAC;AAC7D,MAAI,CAAC,KAAK,QAAS,MAAK,IAAI,MAAM,wBAAwB,KAAK,KAAK,EAAE,CAAC;AACvE,UAAQ;AAAA,IACN,KAAK,kBACD,qBAAqB,KAAK,cAAc,WAAM,KAAK,aAAa,mCAChE,WAAW,KAAK,cAAc;AAAA,EACpC;AACF,CAAC;AAEH,QACG,QAAQ,aAAa,EACrB,YAAY,yDAAyD,EACrE,OAAO,YAAY;AAClB,MAAI;AACF,UAAM,SAAS,MAAM,kBAAkB,EAAE,EAAE,gBAAgB,QAAQ,CAAC;AACpE,YAAQ;AAAA,MACN,OAAO,UACH,YAAY,OAAO,cAAc,WAAM,OAAO,aAAa,KAC3D,kCAAkC,OAAO,cAAc;AAAA,IAC7D;AAAA,EACF,SAAS,GAAG;AACV,SAAK,CAAC;AAAA,EACR;AACF,CAAC;AAEH,QAAQ,WAAW,EAAE,MAAM,IAAI;","names":["mkdir","writeFile","dirname","join","relative","resolve","join","resolve","resolve","join","readFile","extname","isAbsolute","join","resolve","readFile","resolve","readFile","resolve","resolve","isAbsolute","join","readFile","extname","spec","resolve","dirname","mkdir","writeFile","spec","join","relative","resolve"]}
1
+ {"version":3,"sources":["../src/cli.ts","../src/cli/commands.ts","../src/cli/config.ts","../src/cli/home.ts","../src/cli/deck-dir.ts","../src/cli/load-ir.ts","../src/cli/preview-html.ts","../src/cli/serve.ts","../src/cli/update.ts"],"sourcesContent":["#!/usr/bin/env node\nimport { Command } from \"commander\"\nimport { installNodePlatform } from \"./platform/node\"\nimport {\n runAssemble,\n runAudit,\n runDisassemble,\n runInit,\n runMigrate,\n runNarratives,\n runPreview,\n runRender,\n runSchema,\n runSpecValidate,\n runThemes,\n runValidate,\n} from \"./cli/commands\"\nimport { DEFAULT_PORT, runServe } from \"./cli/serve\"\nimport { checkForUpdate, createSelfUpdater } from \"./cli/update\"\nimport { VERSION } from \"./version\"\n\ninstallNodePlatform()\n\nconst program = new Command()\nprogram\n .name(\"pptfast\")\n .description(\"Stable, editable PPTX generation for AI agents — semantic IR in, native DrawingML out\")\n .version(VERSION)\n\nfunction fail(e: unknown): never {\n console.error(e instanceof Error ? e.message : String(e))\n process.exit(1)\n}\n\nprogram\n .command(\"render\")\n .description(\"Render an IR JSON file, deck project directory, or bare deck name to a .pptx\")\n .argument(\"<target>\", \"IR JSON file, deck project directory, or bare name under ~/.pptfast/decks\")\n .requiredOption(\"-o, --output <file>\", \"output .pptx path\")\n .option(\"--theme <id>\", \"override the deck theme (see `pptfast themes`)\")\n .option(\"--style <path>\", \"style overrides JSON re-coloring the theme (see `pptfast schema --style`)\")\n .option(\"--draft\", \"allow unfilled placeholder pages (skip the draft gate)\")\n .action(async (target: string, opts: { output: string; theme?: string; style?: string; draft?: boolean }) => {\n try {\n console.log(\n await runRender(target, {\n output: opts.output,\n theme: opts.theme,\n stylePath: opts.style,\n draft: opts.draft,\n }),\n )\n } catch (e) {\n fail(e)\n }\n })\n\nprogram\n .command(\"validate\")\n .description(\"Validate an IR JSON file, deck project directory, or bare deck name against the schema\")\n .argument(\"<target>\", \"IR JSON file, deck project directory, or bare name under ~/.pptfast/decks\")\n .action(async (target: string) => {\n try {\n console.log(await runValidate(target))\n } catch (e) {\n fail(e)\n }\n })\n\nprogram\n .command(\"audit\")\n .description(\n \"Deterministic geometry audit (overflow, out-of-bounds, low-contrast, overlap, content-truncated, content-dropped), plus an optional --pixels contrast pass — exits 1 when it finds anything\",\n )\n .argument(\"<target>\", \"IR JSON file, deck project directory, or bare name under ~/.pptfast/decks\")\n .option(\"--json\", \"machine-readable output (the full AuditReport)\")\n .option(\"--pixels\", \"also run the optional pixel-contrast pass over image-backed text (requires sharp)\")\n .action(async (target: string, opts: { json?: boolean; pixels?: boolean }) => {\n try {\n const { output, hasFindings } = await runAudit(target, { json: opts.json, pixels: opts.pixels })\n console.log(output)\n if (hasFindings) process.exit(1)\n } catch (e) {\n fail(e)\n }\n })\n\nprogram\n .command(\"schema\")\n .description(\"Print the IR JSON Schema (feed this to a model before it writes IR)\")\n .option(\"--style\", \"print the style-override schema instead\")\n .option(\"--spec\", \"print the deck spec schema instead\")\n .option(\"--plan\", \"removed — use --spec instead\")\n .action((opts: { style?: boolean; spec?: boolean; plan?: boolean }) => {\n // vocabulary-v4 rename (spec §8.2): `--plan` renamed to `--spec`, no\n // long-lived alias — hard-fail pointing at the one new flag rather than\n // silently keep serving the plan schema under its old name.\n if (opts.plan) {\n fail(new Error(\"`pptfast schema --plan` has been renamed to `pptfast schema --spec` — run `pptfast schema --spec` instead\"))\n }\n console.log(runSchema(opts.spec ? \"spec\" : opts.style ? \"style\" : undefined))\n })\n\n// vocabulary-v4 rename (spec §8.2): `pptfast plan validate` renamed to\n// `pptfast spec validate`. The `plan` command group stays registered only so\n// `pptfast plan validate <file>` fails with a message pointing at the new\n// command, rather than commander's own generic \"unknown command\" error.\nconst plan = program.command(\"plan\").description(\"Removed — use `pptfast spec` instead\")\nplan\n .command(\"validate\")\n .description(\"Removed — use `pptfast spec validate` instead\")\n .argument(\"<file>\")\n .action(() => {\n fail(new Error(\"`pptfast plan validate` has been renamed to `pptfast spec validate` — run `pptfast spec validate <file>` instead\"))\n })\n\nconst spec = program.command(\"spec\").description(\"Deck spec commands (spec §6)\")\nspec\n .command(\"validate\")\n .description(\"Validate a deck spec JSON file against the schema and strategy-aware hard gates\")\n .argument(\"<spec.json>\")\n .action(async (specPath: string) => {\n try {\n console.log(await runSpecValidate(specPath))\n } catch (e) {\n fail(e)\n }\n })\n\nprogram\n .command(\"assemble\")\n .description(\"Assemble a deck project directory (deck.spec.json + pages/ + assets/) into an IR JSON file\")\n .argument(\"<dir|name>\", \"deck project directory, or bare name under ~/.pptfast/decks\")\n .option(\"-o, --output <file>\", \"output IR JSON path (default: <dir>/deck.json)\")\n .action(async (target: string, opts: { output?: string }) => {\n try {\n console.log(await runAssemble(target, { output: opts.output }))\n } catch (e) {\n fail(e)\n }\n })\n\nprogram\n .command(\"disassemble\")\n .description(\"Split an IR JSON file into a deck project directory (deck.spec.json + pages/)\")\n .argument(\"<ir.json>\", \"path to the IR file\")\n .requiredOption(\"-o, --output <dir>\", \"output deck project directory\")\n .action(async (irPath: string, opts: { output: string }) => {\n try {\n console.log(await runDisassemble(irPath, opts.output))\n } catch (e) {\n fail(e)\n }\n })\n\nprogram\n .command(\"migrate\")\n .description(\"Convert a v3 IR file to v4, or a deck.plan.json project directory to deck.spec.json — deterministic, no model\")\n .argument(\"<input>\", \"IR v3 JSON file, or a deck project directory containing deck.plan.json\")\n .requiredOption(\"-o, --output <output>\", \"output path — an IR JSON file for a v3 file input, a directory for a deck-project-directory input\")\n .action(async (input: string, opts: { output: string }) => {\n try {\n console.log(await runMigrate(input, opts.output))\n } catch (e) {\n fail(e)\n }\n })\n\nprogram\n .command(\"themes\")\n .description(\"List built-in themes\")\n .option(\"--json\", \"machine-readable output\")\n .action((opts: { json?: boolean }) => console.log(runThemes(Boolean(opts.json))))\n\nprogram\n .command(\"narratives\")\n .description(\"List named narrative presets (strategy/pacing/audience axes + theme recommendations)\")\n .option(\"--json\", \"machine-readable output\")\n .action((opts: { json?: boolean }) => console.log(runNarratives(Boolean(opts.json))))\n\n// vocabulary-v4 rename (spec §8.2): `pptfast scenarios` renamed to\n// `pptfast narratives`, no long-lived alias — hard-fail pointing at the new\n// command name.\nprogram\n .command(\"scenarios\")\n .description(\"Removed — use `pptfast narratives` instead\")\n .action(() => {\n fail(new Error(\"`pptfast scenarios` has been renamed to `pptfast narratives` — run `pptfast narratives` instead\"))\n })\n\nprogram\n .command(\"init\")\n .description(\"Scaffold a pptfast.config.json in the current directory\")\n .action(async () => {\n try {\n console.log(await runInit())\n } catch (e) {\n fail(e)\n }\n })\n\nprogram\n .command(\"preview\")\n .description(\"Render each slide to an SVG file for visual self-check\")\n .argument(\"<target>\", \"IR JSON file, deck project directory, or bare name under ~/.pptfast/decks\")\n .requiredOption(\"-o, --output <dir>\", \"output directory\")\n .option(\"--html\", \"also write a self-contained preview.html (all slides inlined — thumbnail strip, keyboard navigation) for human review\")\n .action(async (target: string, opts: { output: string; html?: boolean }) => {\n try {\n console.log(await runPreview(target, opts.output, { htmlOut: opts.html }))\n } catch (e) {\n fail(e)\n }\n })\n\nprogram\n .command(\"serve\")\n .description(\"Serve a live-reloading HTML preview of an IR JSON file, deck project directory, or bare deck name over HTTP\")\n .argument(\"<target>\", \"IR JSON file, deck project directory, or bare name under ~/.pptfast/decks\")\n .option(\"--port <number>\", `port to listen on (default ${DEFAULT_PORT})`)\n .option(\"--no-open\", \"do not open the URL in a browser after starting\")\n .action(async (target: string, opts: { port?: string; open: boolean }) => {\n try {\n let port: number | undefined\n if (opts.port !== undefined) {\n port = Number(opts.port)\n if (!Number.isInteger(port)) {\n fail(new Error(`invalid --port value \"${opts.port}\" — expected an integer`))\n }\n }\n await runServe(target, { port, open: opts.open })\n } catch (e) {\n fail(e)\n }\n })\n\nprogram\n .command(\"check-update\")\n .description(\"Check npm for a newer pptfast release\")\n .action(async () => {\n const info = await checkForUpdate({ currentVersion: VERSION })\n if (!info.checked) fail(new Error(`update check failed: ${info.error}`))\n console.log(\n info.updateAvailable\n ? `update available: ${info.currentVersion} → ${info.latestVersion} (run \\`pptfast self-update\\`)`\n : `pptfast ${info.currentVersion} is up to date`,\n )\n })\n\nprogram\n .command(\"self-update\")\n .description(\"Update the global pptfast install to the latest release\")\n .action(async () => {\n try {\n const result = await createSelfUpdater()({ currentVersion: VERSION })\n console.log(\n result.updated\n ? `updated: ${result.currentVersion} → ${result.latestVersion}`\n : `already at the latest version (${result.currentVersion})`,\n )\n } catch (e) {\n fail(e)\n }\n })\n\nprogram.parseAsync().catch(fail)\n","import { mkdir, rm, writeFile } from \"node:fs/promises\"\nimport { dirname, join, relative, resolve } from \"node:path\"\nimport {\n formatIssues,\n formatWarnings,\n generatePptx,\n irJsonSchema,\n listThemes,\n renderSlideSvg,\n styleJsonSchema,\n validateIr,\n type ValidationIssue,\n} from \"../api\"\nimport { PptfastError } from \"../errors\"\nimport { StyleOverrideSchema, type PptxIR, type StyleOverride } from \"../ir\"\nimport { PptxIRV3Schema } from \"../ir/legacy-v3\"\nimport { migrateIrV3ToV4 } from \"../ir/migrate\"\nimport { disassembleDeck, type PageContent } from \"../spec/assemble\"\nimport { formatInvalidSpecError, specJsonSchema, resolveSpecThemeId, validateSpec } from \"../spec\"\nimport { migrateDeckPlanToSpec } from \"../spec/migrate\"\nimport { AUDIENCE_VALUES, PACING_BUDGETS, STRATEGY_DEFINITIONS, NARRATIVE_PRESETS, resolveNarrative, type NarrativeProfile } from \"../narrative\"\nimport { auditDeck, type AuditChecks, type AuditFinding, type AuditReport } from \"../svg/audit/deck-audit\"\nimport { getInstalledThemeIds } from \"../themes/definitions\"\nimport { CONFIG_FILENAME, findConfig, findUserConfig } from \"./config\"\nimport {\n assertSafeFileSegment,\n isDeckDirectory,\n pathExists,\n readDeckDir,\n resolveDeckTarget,\n writeDeckAssets,\n PLAN_FILENAME,\n SPEC_FILENAME,\n} from \"./deck-dir\"\nimport { loadIrFile, resolveLocalAssets } from \"./load-ir\"\nimport { buildPreviewHtml } from \"./preview-html\"\n\n/** `findUserConfig()`'s own return shape, named here so it can be threaded as\n * a parameter (`loadDeckTarget`/`applyDeckConfig` below) instead of each\n * callee re-fetching it — see `applyDeckConfig`'s own doc comment for why. */\ntype UserConfigHit = Awaited<ReturnType<typeof findUserConfig>>\n\n/** `findConfig()`'s own return shape — the project-layer counterpart to\n * {@link UserConfigHit}, threaded the same way and for the same reason\n * (W5 task 6: `loadDeckTarget` now needs the project layer too, for\n * `decksDir` — see {@link resolveDecksDirSource}). */\ntype ProjectConfigHit = Awaited<ReturnType<typeof findConfig>>\n\nasync function loadStyleFile(path: string): Promise<StyleOverride> {\n const raw = await loadIrFile(path)\n const r = StyleOverrideSchema.safeParse(raw)\n if (!r.success) {\n const detail = r.error.issues\n .map((i) => `${i.path.join(\".\") || \"(root)\"}: ${i.message}`)\n .join(\"\\n\")\n throw new PptfastError(`invalid style file ${path}:\\n${detail}`)\n }\n return r.data\n}\n\n/** Names which of the four precedence layers the (invalid) resolved `theme`\n * value came from, for {@link applyDeckConfig}'s unknown-theme error — a\n * config-file layer names its own path, `--theme` names itself, and the\n * IR's own default has no path to name at all. */\nfunction describeThemeSource(\n opts: { theme?: string },\n projectHit: { path: string; config: { theme?: string } } | null,\n userHit: UserConfigHit,\n): string {\n if (opts.theme !== undefined) return \"--theme\"\n if (projectHit?.config.theme !== undefined) return projectHit.path\n if (userHit?.config.theme !== undefined) return userHit.path\n return \"the deck's own theme\"\n}\n\n/**\n * The `config` argument `resolveDeckTarget` (`./deck-dir.ts`) and its\n * `decksRoot` (`./home.ts`) expect: an object exposing `decksDir`, resolved\n * against whichever base that value's own layer implies. Project\n * `pptfast.config.json`'s own `decksDir` (spec §7's project-level escape\n * hatch, `ConfigSchema` in `./config.ts`, W5 task 6) wins over the user\n * config's (`UserConfigSchema`) when both are set — same project-beats-user\n * precedence as `theme`/`style` (see `applyDeckConfig` below) — but the two\n * layers resolve against different bases (project against the config file's\n * own directory, user against `pptfastHome()`, `decksRoot`'s one fixed\n * base), so a winning project value is resolved to an absolute path *here*,\n * before being handed down: `decksRoot`'s own\n * `resolve(pptfastHome(), config?.decksDir ?? \"decks\")` then returns that\n * absolute path unchanged (`path.resolve`'s own semantics for an absolute\n * later segment) — the same \"already-absolute short-circuits the base\"\n * behavior `decksRoot({ decksDir: \"/elsewhere/decks\" })` already exercises\n * for the user layer, reused rather than reimplemented. Falls through to\n * `userHit?.config` untouched when the project layer has no `decksDir` of\n * its own — including when there is no project config at all — so the user\n * layer (or, absent that too, `decksRoot`'s own built-in default) keeps\n * working exactly as before this function existed.\n */\nfunction resolveDecksDirSource(\n projectHit: ProjectConfigHit,\n userHit: UserConfigHit,\n): { decksDir?: string } | undefined {\n if (projectHit?.config.decksDir !== undefined) {\n return { decksDir: resolve(dirname(projectHit.path), projectHit.config.decksDir) }\n }\n return userHit?.config\n}\n\n/**\n * Resolve deck defaults onto the raw (pre-validation) IR.\n * Precedence (spec §7's four-layer chain, W5 task 5): CLI flag > project\n * `pptfast.config.json` (walked up from cwd) > user `~/.pptfast/config.json`\n * (`findUserConfig`, no cwd walk-up — a single fixed path, see `./config.ts`)\n * > whatever the artifact itself already carries (an authored IR's own\n * `theme`, or `PptxIRSchema`'s own \"consulting\" default when nothing\n * anywhere sets one — that bottom fallback is `irTheme.id`/`irTheme.style`\n * below, left `undefined` here for the schema to fill in). `--theme` only\n * swaps theme.id — IR-authored style survives.\n *\n * `opts.projectHit`/`opts.userHit` are the caller's own already-fetched\n * `findConfig(cwd)`/`findUserConfig()` results (`undefined` when the caller\n * has not fetched one — this function fetches whichever is missing itself,\n * so it stays usable standalone). Every real caller (`runRender`/\n * `runValidate`/`runPreview` below) fetches both exactly once — `loadDeckTarget`\n * needs the project layer too now, for `decksDir` (W5 task 6,\n * {@link resolveDecksDirSource}) — and passes them to both this function and\n * `loadDeckTarget`, so a command reads either config file at most once per\n * invocation instead of once per helper that happens to need it.\n *\n * The installed-theme check used to run at config *read* time\n * (`readConfigFile`, `./config.ts`) — eagerly, against every layer's value,\n * whether or not it would ever actually apply. It now runs here instead,\n * once, against `theme` (the value that actually wins the four-layer\n * chain): a stale/unknown theme sitting in a config layer that a `--theme`\n * flag (or a higher-precedence config layer) overrides anyway must not\n * hard-fail a command over a value nothing was ever going to use.\n */\nexport async function applyDeckConfig(\n raw: unknown,\n opts: { theme?: string; stylePath?: string; cwd: string; projectHit?: ProjectConfigHit; userHit?: UserConfigHit },\n): Promise<void> {\n if (typeof raw !== \"object\" || raw === null) return // schema error surfaces in validateIr\n const deck = raw as Record<string, unknown>\n const irTheme =\n typeof deck.theme === \"object\" && deck.theme !== null\n ? (deck.theme as Record<string, unknown>)\n : {}\n const [projectHit, userHit] = await Promise.all([\n opts.projectHit !== undefined ? Promise.resolve(opts.projectHit) : findConfig(opts.cwd),\n opts.userHit !== undefined ? Promise.resolve(opts.userHit) : findUserConfig(),\n ])\n const theme = opts.theme ?? projectHit?.config.theme ?? userHit?.config.theme ?? (irTheme.id as string | undefined)\n const style = opts.stylePath\n ? await loadStyleFile(opts.stylePath)\n : (projectHit?.config.style ?? userHit?.config.style ?? irTheme.style)\n if (theme !== undefined) {\n const installedThemeIds = getInstalledThemeIds()\n if (!installedThemeIds.includes(theme)) {\n throw new PptfastError(\n `unknown theme \"${theme}\" (from ${describeThemeSource(opts, projectHit, userHit)}) — available: ${installedThemeIds.join(\", \")} (see \\`pptfast themes\\`)`,\n )\n }\n }\n if (theme === undefined && style === undefined) return\n deck.theme = { ...irTheme, id: theme, ...(style !== undefined ? { style } : {}) }\n}\n\n/**\n * Shared \"turn a CLI target argument into a raw IR-shaped object plus its\n * asset base directory\" step for `runValidate`/`runRender`/`runPreview` (W5\n * task 5) — the one piece of logic those three commands would otherwise\n * triplicate. `arg` is resolved through `resolveDeckTarget` (path vs.\n * bare-name, spec §7) using the effective `decksDir` source — project config\n * when it sets one, else the user config's, else `resolveDeckTarget`'s own\n * built-in default (W5 task 6, {@link resolveDecksDirSource}) — then\n * branches on whether the resolved target is a deck project directory:\n *\n * - directory → `readDeckDir` (assemble in memory — spec + pages/ + assets/,\n * `./deck-dir.ts`), asset paths resolve against the deck directory itself.\n * - file → the pre-existing single-file path, byte-for-byte: `loadIrFile`\n * then the same `dirname(resolve(...))` asset base every caller already\n * used. When `arg` is an explicit path (has a separator, or exists\n * locally — true of every pre-W5 caller, since every existing test passes\n * a full path), `resolveDeckTarget` returns it completely unchanged with\n * no `fs` call at all, so this branch degenerates to exactly the old\n * inline code — single-file behavior stays byte-identical.\n *\n * `isDir` is threaded back so `runValidate` can gate its dir-only placeholder\n * note on it (single-file mode must never grow that note, even for a\n * hand-authored IR that happens to set `placeholder: true` itself).\n *\n * `projectHit`/`userHit` are the caller's own already-fetched\n * `findConfig(cwd)`/`findUserConfig()` results (see `applyDeckConfig`'s doc\n * comment above for why both are threaded rather than fetched here too).\n *\n * `resolvedTarget` (serve wave, task S1) is the absolute path `target` itself\n * resolved to — the deck directory (`isDir: true`) or the single IR file\n * (`isDir: false`). Unused by `runValidate`/`runRender`/`runAudit`, which\n * only ever destructure `raw`/`baseDir`(/`isDir`) — it exists so\n * `buildDeckPreview` below can hand it to `createServeServer` (`./serve.ts`)\n * as the exact path to `fs.watch`, without that module re-deriving the same\n * bare-name/`decksDir` resolution a second time.\n */\nasync function loadDeckTarget(\n arg: string,\n cwd: string,\n projectHit: ProjectConfigHit,\n userHit: UserConfigHit,\n): Promise<{ raw: unknown; baseDir: string; isDir: boolean; resolvedTarget: string }> {\n const target = await resolveDeckTarget(arg, resolveDecksDirSource(projectHit, userHit), cwd)\n if (await isDeckDirectory(target)) {\n const { ir, deckDir } = await readDeckDir(target)\n return { raw: ir, baseDir: deckDir, isDir: true, resolvedTarget: deckDir }\n }\n const raw = await loadIrFile(target)\n const resolvedFile = resolve(target)\n return { raw, baseDir: dirname(resolvedFile), isDir: false, resolvedTarget: resolvedFile }\n}\n\nexport interface RenderOptions {\n output: string\n theme?: string\n stylePath?: string\n cwd?: string\n /** Skip the unfilled-placeholder-pages gate (W5 task 1) — see `generatePptx` in `../api`. */\n draft?: boolean\n}\n\n/**\n * `irPath` accepts a single IR/spec JSON file, a deck project directory, or\n * a bare deck name under `~/.pptfast/decks` (W5 task 5, `loadDeckTarget`\n * above) — directory/bare-name input is assembled in memory first, then\n * follows the exact same validate → resolve-assets → generate pipeline a\n * single file always has. `--draft` threads through unchanged either way\n * (`generatePptx`'s own gate, W5 task 1) — a deck project's own placeholder\n * pages are exactly what that gate exists to catch.\n *\n * Appends the same field-alias {@link normalizedNote} `runValidate` below\n * prints (W5 whole-branch review finding 3 — the README already claimed\n * `render` did this; it never actually threaded `v.normalized` through\n * until now), plus {@link warningsNote} (borrow wave, Task 2) whenever the\n * pre-flight `validateIr` call below returned warn-severity findings —\n * `generatePptx`'s own internal re-validate (`../api.ts`) follows the exact\n * same error-only severity rule, so this pre-flight check and the actual\n * generation it gates in step can never disagree on what counts as blocking.\n */\nexport async function runRender(irPath: string, opts: RenderOptions): Promise<string> {\n const cwd = opts.cwd ?? process.cwd()\n const [projectHit, userHit] = await Promise.all([findConfig(cwd), findUserConfig()])\n const { raw, baseDir } = await loadDeckTarget(irPath, cwd, projectHit, userHit)\n await applyDeckConfig(raw, { theme: opts.theme, stylePath: opts.stylePath, cwd, projectHit, userHit })\n const v = validateIr(raw)\n if (!v.ok) throw new PptfastError(`invalid IR:\\n${formatIssues(v.errors)}`)\n await resolveLocalAssets(v.ir!, baseDir)\n const bytes = await generatePptx(v.ir!, { draft: opts.draft })\n await mkdir(dirname(resolve(opts.output)), { recursive: true })\n await writeFile(opts.output, bytes)\n const ok = `wrote ${opts.output} (${v.ir!.slides.length} slides, ${bytes.length} bytes)`\n const notes = [warningsNote(v.warnings), normalizedNote(v.normalized)].filter((n): n is string => n !== undefined)\n return notes.length > 0 ? `${ok}\\n${notes.join(\"\\n\")}` : ok\n}\n\n/**\n * `\"note: N field alias(es) normalized\\n path: alias → canonical\\n...\"` —\n * the note line every one of `validateIr`'s callers appends after its own\n * success line when `ValidateResult.normalized` (`../api.ts`) is non-empty,\n * i.e. `validateIr` deterministically rewrote at least one synonym field\n * name before parsing (W5 task 4 — kpi `title`→`label` and friends,\n * `../ir/field-aliases.ts`). Extracted so `runRender`/`runPreview` (W5\n * whole-branch review finding 3 — the README already claimed `validate`\n * *and* `render` both printed this note — `render` never actually did, and\n * `preview` is folded in here too for the same reason) can append the exact\n * same note `runValidate` below has always printed, instead of each\n * re-deriving the formatting a second and third time. `undefined` when\n * nothing was normalized, the same \"let the caller skip the line entirely\"\n * shape {@link placeholderNote} below already uses.\n */\nfunction normalizedNote(normalized: string[] | undefined): string | undefined {\n if (!normalized || normalized.length === 0) return undefined\n const n = normalized.length\n return `note: ${n} field alias${n === 1 ? \"\" : \"es\"} normalized\\n${normalized.map((line) => ` ${line}`).join(\"\\n\")}`\n}\n\n/**\n * `\"warning: page N — path: message\"` block, one line per\n * {@link ValidateResult.warnings} entry (`../api.ts`, borrow wave Task 2's\n * dual-threshold severity split) — printed by `runValidate`/`runRender`\n * alongside their own success line whenever `validateIr` returned at least\n * one warn-severity finding. `undefined` when there are none, same\n * \"let the caller skip the line entirely\" shape {@link normalizedNote}\n * above and {@link placeholderNote} below both use. Exit code is\n * unaffected either way — a warning never turns a `runValidate`/`runRender`\n * call into a thrown `PptfastError` (only `!v.ok`, i.e. an error-severity\n * finding, does that). This note is purely additive visibility.\n */\nfunction warningsNote(warnings: ValidationIssue[] | undefined): string | undefined {\n if (!warnings || warnings.length === 0) return undefined\n return formatWarnings(warnings)\n}\n\n/**\n * Dir-mode-only informational note (W5 task 5, `runValidate` below): unlike\n * `generatePptx`'s draft gate (a hard error) or the content-quality gate\n * (which skips a placeholder's content rules entirely, `ir-quality.ts`), a\n * placeholder page is schema-valid and produces no validation issue on its\n * own — without this, a deck project with pages still unfilled would\n * validate silently \"OK\" with no signal anything is left to do. `undefined`\n * when there are none, so the caller can skip the note line entirely rather\n * than test its own string for emptiness.\n */\nfunction placeholderNote(ir: PptxIR): string | undefined {\n const placeholders = ir.slides\n .map((slide, i) => ({ slide, page: i + 1 }))\n .filter(({ slide }) => slide.placeholder)\n if (placeholders.length === 0) return undefined\n const refs = placeholders\n .map(({ slide, page }) => (slide.id ? `${slide.id} (page ${page})` : `page ${page}`))\n .join(\", \")\n return `note: ${placeholders.length} unfilled placeholder page${placeholders.length === 1 ? \"\" : \"s\"}: ${refs}`\n}\n\n/**\n * `irPath` accepts a single IR/spec JSON file, a deck project directory, or\n * a bare deck name (same `loadDeckTarget` resolution `runRender` uses).\n * Directory/bare-name input additionally gets a {@link placeholderNote} —\n * gated on `isDir` specifically so single-file mode (including a\n * hand-authored IR that sets `placeholder: true` itself) never grows one,\n * keeping that path's output byte-identical to before this task.\n *\n * Returns human-readable report. Throws PptfastError when invalid (CLI exit 1).\n * When `validateIr` deterministically rewrote any synonym field names before\n * parsing (W5 task 4 — kpi `title`→`label` and friends, `ir/field-aliases.ts`),\n * appends them as a \"note\" line after the OK summary: visible so the caller\n * knows their input got silently massaged, but never a reason to fail — a\n * fixed alias never makes it into `v.errors`.\n *\n * Borrow wave, Task 2 (dual-threshold severity): also appends\n * {@link warningsNote} whenever `validateIr` returned warn-severity\n * findings — printed as `\"warning: ...\"` lines, exit code 0 either way\n * (only `!v.ok`, above, throws). A deck can print `OK` and still carry\n * warnings — that combination is the point of the split, not a bug.\n *\n * Borrow wave, Task 2 follow-up (review finding, medium): also runs\n * `resolveLocalAssets` on `v.ir!`, same as `runRender`/`runAudit`/\n * `runPreview` already do — `validateIr` itself only sniffs already-inlined\n * `data:` URIs (`checkAssetBytes`, `../api.ts`'s own doc comment on why a\n * local file path is a different, Node-only ingestion form), so without\n * this a deck-dir referencing a corrupt local `.png` printed `OK` here while\n * `render` correctly rejected the exact same input right after — an\n * inconsistency with SKILL.md's Phase 3 contract, which treats `validate`\n * as the authoritative pre-flight check. `resolveLocalAssets` mutating\n * `v.ir!.assets.images[x].src` into a data URI as a side effect is harmless\n * here — nothing this function reads afterward (`slides.length`, `theme.id`,\n * `placeholderNote`) depends on `src` — so there was no reason to write a\n * separate check-only variant; reusing the exact same function guarantees\n * identical rejection semantics with `render` by construction, not by\n * keeping two copies of the same logic in sync by hand.\n */\nexport async function runValidate(irPath: string, cwd = process.cwd()): Promise<string> {\n const [projectHit, userHit] = await Promise.all([findConfig(cwd), findUserConfig()])\n const { raw, baseDir, isDir } = await loadDeckTarget(irPath, cwd, projectHit, userHit)\n await applyDeckConfig(raw, { cwd, projectHit, userHit })\n const v = validateIr(raw)\n if (!v.ok)\n throw new PptfastError(\n `invalid IR (${v.errors.length} issue${v.errors.length === 1 ? \"\" : \"s\"}):\\n${formatIssues(v.errors)}`,\n )\n await resolveLocalAssets(v.ir!, baseDir)\n const ok = `OK — ${v.ir!.slides.length} slides, theme \"${v.ir!.theme.id}\"`\n const notes: string[] = []\n const warnNote = warningsNote(v.warnings)\n if (warnNote) notes.push(warnNote)\n const aliasNote = normalizedNote(v.normalized)\n if (aliasNote) notes.push(aliasNote)\n if (isDir) {\n const note = placeholderNote(v.ir!)\n if (note) notes.push(note)\n }\n return notes.length > 0 ? `${ok}\\n${notes.join(\"\\n\")}` : ok\n}\n\n/**\n * `\"page 3 (p-kpi): [low-contrast] ...\"` — one line per {@link AuditFinding},\n * echoing `formatIssues`' own `\"page N (id) — path: message\"` convention\n * (`../api.ts`) with a bracketed `[code]` standing in for `path` — an\n * `AuditFinding` has no `path` (it is not a schema-location error, see that\n * interface's own doc comment in `../svg/audit/deck-audit.ts`), and `code`\n * is the closest equivalent \"what kind of problem\" tag. The bracket keeps an\n * audit-finding line visually distinct from a validate-error line at a\n * glance, per the plan's own worked example.\n */\nfunction formatAuditFinding(f: AuditFinding): string {\n const idSuffix = f.slideId !== undefined ? ` (${f.slideId})` : \"\"\n return `page ${f.page}${idSuffix}: [${f.code}] ${f.message}`\n}\n\n/**\n * Human-readable `pptfast audit` report (W6 task 2, spec §7 workflow ④):\n * every finding as its own {@link formatAuditFinding} line — already\n * naturally grouped by page, since `auditDeck` pushes findings in slide\n * order (`../svg/audit/deck-audit.ts`) — followed by a trailing summary line\n * in the plan's own literal wording (\"audited N pages, M skipped, K\n * findings\") so an agent can read just the last line to decide whether to\n * keep iterating, instead of counting findings itself. {@link placeholderNote}\n * runs unconditionally (unlike `runValidate`'s dir-mode-only gating on that\n * same helper below) — audit has no pre-existing single-file-mode output to\n * keep byte-identical the way `runValidate` did when that gating was added,\n * so there is no reason to withhold a genuinely useful note from a\n * hand-authored IR that happens to carry placeholders too.\n *\n * `checks.pixels === \"completed\"` (audit-v2 phase B, i.e. `--pixels` was\n * passed) appends one more line — purely additive, gated on that exact\n * value so the far more common no-`--pixels` run stays byte-identical to\n * the wording pinned above (`checks.pixels` is `\"not-requested\"` there,\n * never `\"completed\"`). No line at all for the omitted case rather than an\n * explicit \"not requested\" note: the human already knows whether they\n * passed the flag, and the machine-readable `--json` path (never silent\n * about `checks` either way) is what an agent actually consumes to tell\n * \"not checked\" apart from \"checked and clean\".\n */\nfunction formatAuditReport(report: AuditReport, ir: PptxIR): string {\n const lines = report.findings.map(formatAuditFinding)\n lines.push(\n `audited ${report.pagesAudited} page${report.pagesAudited === 1 ? \"\" : \"s\"}, ${report.pagesSkipped} skipped, ${report.findings.length} finding${report.findings.length === 1 ? \"\" : \"s\"}`,\n )\n if (report.checks.pixels === \"completed\") {\n lines.push(\"pixel-contrast check: completed\")\n }\n const note = placeholderNote(ir)\n if (note) lines.push(note)\n return lines.join(\"\\n\")\n}\n\nexport interface AuditOptions {\n json?: boolean\n cwd?: string\n /** `--pixels` (audit-v2 phase B, spec §4.3/§11.7): also run the optional\n * pixel-contrast pass over image-backed text. Explicit opt-in only — see\n * `auditDeck`'s own overload doc comment for why this is threaded as a\n * ternary with a literal in each arm rather than passed straight through\n * as `{ pixels: opts.pixels }` (a plain `boolean` doesn't match either\n * overload). Missing rasterization capability or a remote asset\n * reference makes this command fail loudly (a rejected `auditDeck`\n * promise propagates straight out of this function, same as the\n * existing invalid-IR `PptfastError` path) rather than silently\n * reporting a clean pixel check that never ran. */\n pixels?: boolean\n}\n\nexport interface AuditCliResult {\n /** Human report ({@link formatAuditReport}) or, with `opts.json`, the raw\n * `JSON.stringify`'d {@link AuditReport} verbatim — the plan's own \"the\n * full AuditReport\" requirement, unmodified by any CLI-side enrichment. */\n output: string\n /** `true` when `report.findings.length > 0`. The CLI (`../cli.ts`) prints\n * `output` either way, then exits 1 on this signal alone — clean exits 0\n * (spec §7 workflow ④: advisory, not a hard gate, but still\n * agent-judgeable purely from the exit code without parsing output). */\n hasFindings: boolean\n}\n\n/**\n * `pptfast audit <target> [--json]` (W6 task 2, spec §7 workflow ④): resolve\n * `target` through the exact same `loadDeckTarget` path `runValidate`/\n * `runRender`/`runPreview` already use (IR file / deck project directory /\n * bare name under `~/.pptfast/decks`), validate first, then hand the\n * validated IR to `auditDeck` (`../svg/audit/deck-audit.ts`, pure, no I/O).\n *\n * An invalid deck fails exactly like `pptfast validate` — same message\n * shape, same `PptfastError` → CLI exit-1 path — and never reaches\n * `auditDeck` at all: the geometry/contrast/overlap checks only mean\n * anything over a schema-valid, already-quality-gated deck (`auditDeck`'s\n * own \"advisory, not a hard gate\" doc comment — `validateIr` is the hard\n * gate this command leans on rather than re-implements).\n *\n * `resolveLocalAssets` runs after validation, same as `runRender`/\n * `runPreview` — a deck referencing local (non-`data:`/non-`http(s)`) image\n * files must have them inlined before `auditDeck`'s internal `renderSlideSvg`\n * calls, otherwise a local asset's `src` would still be its raw relative\n * path when the contrast checker's background-region walk inspects it,\n * auditing a slide shape that doesn't match what `render`/`preview` actually\n * produce for the same deck.\n *\n * No `--theme`/`--style` flags (unlike `runRender`) — the plan's CLI surface\n * for this command is deliberately just `<target> [--json]` — but\n * `applyDeckConfig` still runs (with no CLI-flag overrides) so a project/user\n * config's own theme/style default still applies, the same \"config layers\n * apply even with no flag passed\" behavior `runValidate` already has.\n */\nexport async function runAudit(target: string, opts: AuditOptions = {}): Promise<AuditCliResult> {\n const cwd = opts.cwd ?? process.cwd()\n const [projectHit, userHit] = await Promise.all([findConfig(cwd), findUserConfig()])\n const { raw, baseDir } = await loadDeckTarget(target, cwd, projectHit, userHit)\n await applyDeckConfig(raw, { cwd, projectHit, userHit })\n const v = validateIr(raw)\n if (!v.ok) {\n throw new PptfastError(\n `invalid IR (${v.errors.length} issue${v.errors.length === 1 ? \"\" : \"s\"}):\\n${formatIssues(v.errors)}`,\n )\n }\n await resolveLocalAssets(v.ir!, baseDir)\n const report = opts.pixels ? await auditDeck(v.ir!, { pixels: true }) : auditDeck(v.ir!)\n const hasFindings = report.findings.length > 0\n const output = opts.json ? JSON.stringify(report, null, 2) : formatAuditReport(report, v.ir!)\n return { output, hasFindings }\n}\n\n/**\n * Validate a deck spec JSON file (W5 task 2: `pptfast plan validate`, renamed\n * to `pptfast spec validate` — vocabulary-v4 rename, task 2, spec §8.2).\n * `loadIrFile` is a generic \"read + JSON-parse with a readable failure\n * message\" helper despite its IR-scoped name (`./load-ir.ts`) — reused as-is\n * rather than duplicated, same pattern `runValidate` above uses for IR.\n * Returns human-readable report. Throws PptfastError when invalid (CLI exit 1).\n *\n * Appends the same {@link normalizedNote} `runValidate`/`runRender` print\n * (T0b fix 2 scope extension) whenever `validateSpec` rewrote a top-level\n * `narrative: {id: \"<preset>\"}` shape (`SpecValidateResult.normalized`,\n * `../spec/index.ts`) — the spec-validate channel gets the identical note\n * format the bare-IR path already has, not a second, differently-shaped one.\n */\nexport async function runSpecValidate(specPath: string): Promise<string> {\n const raw = await loadIrFile(specPath, \"spec\")\n const v = validateSpec(raw)\n if (!v.ok) {\n throw new PptfastError(formatInvalidSpecError(v.errors))\n }\n const spec = v.spec!\n // Safe to call unguarded: validateSpec already resolved this same\n // expression successfully as part of its own hard-gate chain.\n const axes = resolveNarrative(spec.narrative as string | Partial<NarrativeProfile> | undefined)\n const ok = `OK — ${spec.pages.length} pages, narrative ${axes.strategy}/${axes.pacing}/${axes.audience}, theme \"${resolveSpecThemeId(spec)}\"`\n const aliasNote = normalizedNote(v.normalized)\n return aliasNote ? `${ok}\\n${aliasNote}` : ok\n}\n\n/** `mode` selects which JSON Schema to print (`pptfast schema [--style|--spec]`,\n * spec §8.2's `schema --plan`→`schema --spec` rename, task 2) — `\"plan\"` was\n * the pre-rename flag value, no longer accepted (`../cli.ts` hard-fails a\n * bare `--plan` before this function is ever called, see that file's own\n * comment). */\nexport function runSchema(mode?: \"style\" | \"spec\"): string {\n const schema = mode === \"style\" ? styleJsonSchema() : mode === \"spec\" ? specJsonSchema() : irJsonSchema()\n return JSON.stringify(schema, null, 2)\n}\n\nexport function runThemes(asJson: boolean): string {\n const themes = listThemes()\n if (asJson) return JSON.stringify(themes, null, 2)\n return themes.map((t) => `${t.id.padEnd(12)} ${t.label}`).join(\"\\n\")\n}\n\n/**\n * List the named narrative presets (spec §5): strategy/pacing/audience axes +\n * soft theme recommendations — never a hard constraint, see\n * `NarrativePreset.themeRecommendations`'s own doc comment in `narrative/index.ts`.\n * `--json` hands back the full machine-readable payload an agent would want\n * before picking a narrative: every preset, plus the raw strategy/pacing/audience\n * tables those presets are built from (`STRATEGY_DEFINITIONS`/`PACING_BUDGETS`\n * carry data this wave doesn't yet consume for selection — W4's job — but are\n * still useful for a caller inspecting what each axis value means).\n *\n * CLI surface renamed this task (spec §8.2's `scenarios`→`narratives`\n * rename, task 2): command name `narratives`, `--json` output field names\n * `strategies`/`pacings` (were `modes`/`deliveries`) — kept in step with the\n * command's own new name rather than leaving a `pptfast narratives --json`\n * caller staring at a `modes` key for what is now the `strategy` axis.\n */\nexport function runNarratives(asJson: boolean): string {\n if (asJson) {\n return JSON.stringify(\n {\n presets: NARRATIVE_PRESETS,\n strategies: STRATEGY_DEFINITIONS,\n pacings: PACING_BUDGETS,\n audiences: AUDIENCE_VALUES,\n },\n null,\n 2,\n )\n }\n const rows = Object.values(NARRATIVE_PRESETS).map((p) => ({\n id: p.id,\n axes: `${p.axes.strategy}/${p.axes.pacing}/${p.axes.audience}`,\n themes: p.themeRecommendations.join(\", \"),\n }))\n const idWidth = Math.max(...rows.map((r) => r.id.length))\n const axesWidth = Math.max(...rows.map((r) => r.axes.length))\n return rows\n .map((r) => `${r.id.padEnd(idWidth + 2)}${r.axes.padEnd(axesWidth + 2)}${r.themes}`)\n .join(\"\\n\")\n}\n\nconst CONFIG_TEMPLATE = {\n theme: \"consulting\",\n style: {\n colors: { primary: \"#0B5FFF\", accent: \"#FF6A00\" },\n },\n} as const\n\n/** Scaffold pptfast.config.json in cwd. Never overwrites. */\nexport async function runInit(cwd = process.cwd()): Promise<string> {\n const target = join(cwd, CONFIG_FILENAME)\n try {\n await writeFile(target, JSON.stringify(CONFIG_TEMPLATE, null, 2) + \"\\n\", { flag: \"wx\" })\n } catch (e) {\n if ((e as NodeJS.ErrnoException).code === \"EEXIST\") {\n throw new PptfastError(`${target} already exists — edit it instead`)\n }\n throw e\n }\n return `wrote ${target} — themes: \\`pptfast themes\\`, style schema: \\`pptfast schema --style\\``\n}\n\nexport interface PreviewOptions {\n cwd?: string\n /** `--html` (v0.3 W7 task 1, spec §7 workflow ⑤): also write a\n * self-contained `preview.html` alongside the per-slide SVG files —\n * every slide's already-rendered SVG inlined into one file (thumbnail\n * filmstrip + keyboard/click navigation, `buildPreviewHtml`,\n * `./preview-html.ts`) for a human (or an agent that can view HTML) to\n * flip through the whole deck at once instead of opening N separate SVG\n * files. Named `htmlOut` rather than `html` so `RenderOptions.draft`-style\n * option objects in this file all read as \"what to produce\", not\n * \"whether this is HTML\" (there is nothing else this bundle could be).\n * Known limitation (see `buildPreviewHtml`'s own doc comment,\n * `./preview-html.ts`): self-containment assumes every image asset is\n * local or already a `data:` URI — a remote `http(s):` asset src passes\n * through `resolveLocalAssets` untouched and lands in the bundle as a\n * live network reference, not an inlined file.\n *\n * Also gates the audit overlay (notes+preview wave, task 2): when set\n * and the deck has no placeholder page, `runPreview` runs `auditDeck`\n * (`../svg/audit/deck-audit.ts`) and embeds its findings and `checks`\n * into `preview.html` (per-page badges + a findings panel + a one-line\n * checks summary, `buildPreviewHtml`). A deck with any placeholder page\n * skips the audit entirely instead of running it partially — see\n * `runPreview`'s own doc comment for why. */\n htmlOut?: boolean\n}\n\n/**\n * Shared \"assemble/validate/render\" half of the preview build pipeline\n * (serve wave, task S1 extraction) — every step `runPreview` always\n * performed regardless of `--html`, factored out so {@link buildDeckPreview}\n * below (and transitively `createServeServer`, `./serve.ts`) can reuse it\n * without re-threading `loadDeckTarget`/`applyDeckConfig`/`validateIr`/\n * `resolveLocalAssets` a second time. Resolves `target` exactly like\n * `runRender`/`runValidate`/`runAudit` (single IR file, deck project\n * directory, or bare deck name — {@link loadDeckTarget} above), then renders\n * every slide to SVG once (`svgs`, index-aligned with `ir.slides`) — the same\n * strings both `runPreview`'s per-slide `.svg` files and\n * {@link buildDeckAuditAndHtml}'s embedded `preview.html` copies come from,\n * so the two stay byte-identical by construction, not just because the\n * renderer is deterministic (the same guarantee `runPreview` documented\n * before this extraction).\n */\ninterface DeckRenderResult {\n ir: PptxIR\n svgs: string[]\n /** The deck directory (`isDir: true`) or the single IR file (`isDir:\n * false`) `target` resolved to — see {@link loadDeckTarget}'s own doc\n * comment on `resolvedTarget` for why this is threaded back. */\n resolvedTarget: string\n isDir: boolean\n normalized?: string[]\n}\n\nasync function renderDeckSlides(target: string, opts: { cwd?: string } = {}): Promise<DeckRenderResult> {\n const cwd = opts.cwd ?? process.cwd()\n const [projectHit, userHit] = await Promise.all([findConfig(cwd), findUserConfig()])\n const { raw, baseDir, isDir, resolvedTarget } = await loadDeckTarget(target, cwd, projectHit, userHit)\n await applyDeckConfig(raw, { cwd, projectHit, userHit })\n const v = validateIr(raw)\n if (!v.ok) throw new PptfastError(`invalid IR:\\n${formatIssues(v.errors)}`)\n await resolveLocalAssets(v.ir!, baseDir)\n const ir = v.ir!\n const svgs = ir.slides.map((_, i) => renderSlideSvg(ir, i))\n return { ir, svgs, resolvedTarget, isDir, normalized: v.normalized }\n}\n\n/**\n * Audit + HTML-build half of the pipeline (serve wave, task S1 extraction —\n * this is `runPreview`'s pre-extraction `opts.htmlOut` branch body, moved\n * here with no behavior change so both `--html` output and\n * `createServeServer`'s cached page are the exact same bytes for the exact\n * same deck state). Runs `auditDeck` (notes+preview wave, task 2) — but only\n * when the deck has no placeholder page. `auditDeck` itself silently skips a\n * placeholder (`AuditReport.pagesSkipped`, nothing to audit on an unfilled\n * page) — running it over a deck that has some would produce a *partial*\n * report that still looks complete (zero findings reads as \"clean\", not\n * \"some pages were never checked\"), which is worse than not running it at\n * all. The plan's contract is the simpler \"any placeholder present → skip\n * the whole overlay, one-line notice instead\" — implemented here as\n * `hasPlaceholder`, and threaded into `buildPreviewHtml` as either\n * `findings` + `checks` (clean run) or `auditNote` (skipped), never both.\n * `checks` (`AuditReport.checks`, `../svg/audit/deck-audit.ts`) rides along\n * with `findings` on every clean run, not just a partial/findings-only one —\n * `buildPreviewHtml` renders it as its own one-line summary regardless of\n * `findings.length`, so a deck that audited clean because nothing was wrong\n * stays visually distinct from one that audited clean because the pixel\n * pass never ran.\n */\nfunction buildDeckAuditAndHtml(\n ir: PptxIR,\n svgs: string[],\n): { html: string; findings: AuditFinding[]; checks?: AuditChecks } {\n const hasPlaceholder = ir.slides.some((slide) => slide.placeholder)\n const auditReport = hasPlaceholder ? undefined : auditDeck(ir)\n const findings = auditReport?.findings ?? []\n const html = buildPreviewHtml({\n title: ir.filename,\n slides: ir.slides.map((slide, i) => ({\n index: i,\n id: slide.id,\n type: slide.type,\n svg: svgs[i]!,\n placeholder: slide.placeholder,\n })),\n findings: findings.map((f) => ({ page: f.page, slideId: f.slideId, code: f.code, message: f.message })),\n auditNote: hasPlaceholder\n ? \"audit overlay skipped — deck has unfilled placeholder pages; fill every page and re-run `pptfast preview --html` to see audit findings\"\n : undefined,\n checks: auditReport?.checks,\n })\n return { html, findings, checks: auditReport?.checks }\n}\n\n/**\n * {@link renderDeckSlides} + {@link buildDeckAuditAndHtml} combined — the\n * full \"target → {html, findings, ...}\" preview build pipeline (serve wave,\n * task S1; spec-plan.md `.issues/2026-07-25-serve/spec-plan.md` §3 design\n * ruling 5: \"buildPreviewHtml 复用现状 ... 禁止 fork 一份 preview 构建逻辑\").\n * Two consumers: `runPreview`'s `opts.htmlOut` branch below (byte-identical\n * output to before this extraction — see that function's own doc comment),\n * and `createServeServer` (`./serve.ts`), which calls this once at startup\n * and again on every debounced `fs.watch` rebuild, caching `.html` in memory\n * for `GET /` and pushing an SSE `reload` once it succeeds. A thrown\n * `PptfastError` (invalid IR, a mid-edit malformed JSON save, ...) propagates\n * straight out of this function either way — it is `createServeServer`'s job\n * to catch the *rebuild* case and turn it into an SSE `error` event instead\n * of letting it kill the server; the *first* call (before serve starts\n * listening) is deliberately allowed to reject the whole command, same\n * \"throw `PptfastError` → CLI exit 1\" contract every other `run*` command\n * already has, since there is no previous-good HTML yet to keep serving.\n */\nexport interface DeckPreviewResult extends DeckRenderResult {\n html: string\n findings: AuditFinding[]\n checks?: AuditChecks\n}\n\nexport async function buildDeckPreview(target: string, opts: { cwd?: string } = {}): Promise<DeckPreviewResult> {\n const rendered = await renderDeckSlides(target, opts)\n const { html, findings, checks } = buildDeckAuditAndHtml(rendered.ir, rendered.svgs)\n return { ...rendered, html, findings, checks }\n}\n\n/**\n * `irPath` accepts a single IR/spec JSON file, a deck project directory, or\n * a bare deck name (same `loadDeckTarget` resolution `runRender` uses).\n * Preview never gates on placeholder pages either way (single-file or\n * dir-mode) — `renderSlideSvg` itself never calls the draft gate, spec §7:\n * preview always lets everything through — an agent iterating on a\n * partially-filled deck needs to see whatever page it just wrote without\n * every other still-empty page blocking it.\n *\n * Appends the same field-alias {@link normalizedNote} `runValidate`/\n * `runRender` print (W5 whole-branch review finding 3).\n *\n * Delegates the assemble/render/audit/HTML-build work to\n * {@link renderDeckSlides}/{@link buildDeckAuditAndHtml} (serve wave, task S1\n * extraction — see {@link buildDeckPreview}'s own doc comment for why); this\n * function's own job is now purely the CLI-facing shell around them —\n * writing each rendered SVG to `outDir`, conditionally writing\n * `preview.html`, and assembling the human-readable summary line. `outDir`\n * is only created once assemble/validate/render has already succeeded\n * (`renderDeckSlides` runs first) — a target that fails to resolve or\n * validate never leaves behind an empty `outDir` it was never able to fill,\n * the same \"don't create output for a call that's about to fail\" posture\n * `runDisassemble`'s own path-traversal guard already established elsewhere\n * in this file.\n */\nexport async function runPreview(irPath: string, outDir: string, opts: PreviewOptions = {}): Promise<string> {\n const { ir, svgs, normalized } = await renderDeckSlides(irPath, { cwd: opts.cwd })\n // After render, not before (S1 review carry) — see this function's own doc comment.\n await mkdir(outDir, { recursive: true })\n for (let i = 0; i < ir.slides.length; i++) {\n const name = `${String(i + 1).padStart(3, \"0\")}-${ir.slides[i]!.type}.svg`\n await writeFile(join(outDir, name), svgs[i]!)\n }\n const ok = `wrote ${ir.slides.length} SVG files to ${outDir}`\n const notes: string[] = []\n const aliasNote = normalizedNote(normalized)\n if (aliasNote) notes.push(aliasNote)\n if (opts.htmlOut) {\n const { html, findings } = buildDeckAuditAndHtml(ir, svgs)\n const htmlPath = join(outDir, \"preview.html\")\n await writeFile(htmlPath, html)\n notes.push(`note: wrote self-contained preview to ${htmlPath}`)\n if (findings.length > 0) {\n notes.push(`note: audit found ${findings.length} finding${findings.length === 1 ? \"\" : \"s\"} — see preview.html`)\n }\n }\n return notes.length > 0 ? `${ok}\\n${notes.join(\"\\n\")}` : ok\n}\n\nexport interface AssembleOptions {\n output?: string\n cwd?: string\n}\n\n/**\n * Rewrites every local (non-`data:`/non-`http(s)`) asset src so it keeps\n * resolving correctly when the assembled IR is written to `outDir`, a\n * different directory than the `deckDir` it was assembled from (`-o`\n * pointing outside the deck project, `runAssemble` below). `readDeckDir`'s\n * asset scan always produces a `deckDir`-relative src (`./deck-dir.ts`'s\n * `scanAssets` — always `assets/<file>`), so writing the IR anywhere else\n * unchanged would leave that src resolving against the *wrong* base the\n * next time this file is loaded (`loadDeckTarget`'s single-file branch\n * resolves relative asset srcs against the IR file's own directory, not\n * where it happened to be assembled from). Rebuilds `assets.images` rather\n * than mutating entries in place — the same \"never mutate a live IR's asset\n * map\" caution `readDeckDir` itself documents (`./deck-dir.ts`).\n */\nfunction withRewrittenAssetPaths(ir: PptxIR, deckDir: string, outDir: string): PptxIR {\n const images = Object.fromEntries(\n Object.entries(ir.assets.images).map(([id, asset]) => {\n if (asset.src.startsWith(\"data:\") || /^https?:\\/\\//.test(asset.src)) return [id, asset] as const\n return [id, { ...asset, src: relative(outDir, join(deckDir, asset.src)) }] as const\n }),\n )\n return { ...ir, assets: { images } }\n}\n\n/**\n * `pptfast assemble <dir|name>` (W5 task 5): resolve `target` (path or bare\n * deck name, `resolveDeckTarget`) → `readDeckDir` (spec + pages/ + assets/ →\n * IR, `./deck-dir.ts`) → write the assembled IR as pretty-printed JSON,\n * default `<deckDir>/deck.json` when `-o` is omitted. Deliberately does\n * *not* call `applyDeckConfig` — `assemble` materializes exactly what the\n * spec says plus each page's own auto-selected `layout` where the page file\n * left it implicit (`assembleDeck`'s own doc comment, W4 design decision\n * 10) — a portable IR file, self-contained down to which archetype each page\n * will render with. Theme/style overrides are `validate`/`render`/\n * `preview`'s job (each already applies the four-layer chain whether given\n * this same directory or the `deck.json` this command just wrote).\n *\n * `target` must resolve to an actual directory: a target that exists but\n * names a file gets a friendly `expected a deck project directory` error\n * right here rather than reaching `readDeckDir` and failing deeper, with a\n * confusing `ENOTDIR` message, trying to read `<file>/deck.spec.json`. A\n * target that does not exist *at all* is deliberately let through to\n * `readDeckDir` unchanged — its own missing-spec-file error already names\n * the expected layout, strictly more helpful than this shorter message.\n *\n * `-o` resolves against `cwd` (the same fix `resolveDeckTarget` already\n * needed — see that function's own doc comment) rather than the real\n * `process.cwd()`, so a caller that threads a custom `cwd` gets the output\n * where it actually asked for it. When the resolved output directory is not\n * `deckDir` itself, every local asset src is rewritten\n * ({@link withRewrittenAssetPaths}) to stay correct from the new location —\n * otherwise `assets/logo.png` (correct relative to `deckDir`) would silently\n * fail to resolve from wherever `-o` actually put the file.\n *\n * When the spec omitted `seed`, `readDeckDir` (via `assembleDeck`) generates\n * one deterministically and reports it as `generatedSeed` — surfaced here as\n * a suggestion to add it back to `deck.spec.json` for revision stability\n * (spec §5's seed-generation semantics). Never written automatically:\n * `assembleDeck` stays a pure function with no fs side effects, and silently\n * rewriting a file the user did not ask this command to touch would be a\n * worse surprise than asking them to paste one line in.\n *\n * `materializedLayoutCount` (also from `assembleDeck`, unset when every page\n * already named its own `layout` or landed on the image-cover bypass) gets\n * its own one-line note the same way, listed after the seed note when both\n * apply — purely informational, telling the caller how many pages just had\n * an auto-pick baked into `deck.json` rather than leaving them to notice by\n * diffing the file. The base summary line's `(N slides, M placeholders)`\n * parenthetical itself stays untouched by either note (`scripts/e2e.mts`\n * checks it by exact substring) — both notes are strictly additional lines.\n */\nexport async function runAssemble(target: string, opts: AssembleOptions = {}): Promise<string> {\n const cwd = opts.cwd ?? process.cwd()\n const [projectHit, userHit] = await Promise.all([findConfig(cwd), findUserConfig()])\n const dir = await resolveDeckTarget(target, resolveDecksDirSource(projectHit, userHit), cwd)\n if ((await pathExists(dir)) && !(await isDeckDirectory(dir))) {\n throw new PptfastError(`expected a deck project directory: ${dir}`)\n }\n const { ir, generatedSeed, materializedLayoutCount, deckDir } = await readDeckDir(dir)\n const outPath = opts.output ? resolve(cwd, opts.output) : join(deckDir, \"deck.json\")\n const outDir = dirname(outPath)\n const outIr = outDir === deckDir ? ir : withRewrittenAssetPaths(ir, deckDir, outDir)\n await mkdir(outDir, { recursive: true })\n await writeFile(outPath, JSON.stringify(outIr, null, 2) + \"\\n\")\n const placeholderCount = outIr.slides.filter((s) => s.placeholder).length\n const summary = `wrote ${outPath} (${outIr.slides.length} slides, ${placeholderCount} placeholder${placeholderCount === 1 ? \"\" : \"s\"})`\n const notes: string[] = []\n if (generatedSeed !== undefined) {\n notes.push(`note: generated seed ${generatedSeed} — add \"seed\": ${generatedSeed} to deck.spec.json for revision stability`)\n }\n if (materializedLayoutCount !== undefined) {\n notes.push(\n `note: ${materializedLayoutCount} layout${materializedLayoutCount === 1 ? \"\" : \"s\"} auto-selected into deck.json — pin \"layout\" in a page file to lock one`,\n )\n }\n return [summary, ...notes].join(\"\\n\")\n}\n\n/**\n * `pptfast disassemble <deck.json> -o <dir>` (W5 task 5): the CLI shell for\n * `disassembleDeck` (`../spec/assemble.ts`) — read + validate an IR file the\n * same way `runRender`/`runValidate` do, then write `deck.spec.json` +\n * `pages/<id>.json` for every non-placeholder page. Pretty-printed. Key\n * order is already stable because `disassembleDeck` builds every object\n * with the same fixed field order on every call, not by iterating the\n * input, so there is no separate \"stable stringify\" step to write. Refuses\n * to overwrite an existing `deck.spec.json` — same `wx`-flag EEXIST guard as\n * `runInit`'s config scaffold — so re-running this command never silently\n * clobbers a deck project someone has since started filling in. Page files\n * are freely (re)written since they only exist because this same command\n * produced them, and written concurrently (`Promise.all`) since each is an\n * independent file.\n *\n * Also materializes `assets/` ({@link writeDeckAssets}, `./deck-dir.ts`) —\n * `disassembleDeck` itself never touches `ir.assets.images` (see that\n * function's own doc comment for the full accounting), so this is the step\n * that actually closes the loop: without it, an image deck disassembles\n * with every `asset_id` reference intact but no bytes behind it, then\n * re-assembles and renders with the image silently missing.\n *\n * The summary never claims to have written a directory it did not create:\n * `pagesDir`/`assetsDir` are only named when at least one page/asset file\n * actually landed there (a spec-only deck with every slide a placeholder,\n * or an assetless deck, leaves either directory unwritten).\n *\n * Every page id is checked with {@link assertSafeFileSegment} (`./deck-dir.ts`)\n * before *any* file is written — not just ahead of `pages/<id>.json` (W5\n * whole-branch review finding 1, CRITICAL — CWE-22), but ahead of\n * `deck.spec.json` too (post-v0.3 W8 fix round, backlog item 8,\n * `.issues/notes/2026-07-18-post-v03-backlog.md` #8 — the check originally\n * ran after the spec write): `slide.id` is an unrestricted string at the\n * schema layer, so a hand-authored IR could otherwise set one to\n * `\"../../../../escape\"` and write outside `outDir`. `writeDeckAssets` below\n * (`./deck-dir.ts`) carries the matching check for asset keys, inside\n * `writeOneAsset` — that check stays per-asset rather than also moving\n * ahead of the spec write, since an unsafe id is only one of several ways\n * `writeOneAsset` can fail (malformed data URI, URL asset, unreadable local\n * file) and the others can't be front-loaded without doing the write itself.\n *\n * Failure rollback (post-v0.3 W8 fix round, backlog item 8): once\n * `deck.spec.json` is written, this call is the sole owner of that file for\n * the rest of its own execution, so any failure in the page/asset writes\n * below deletes it before rethrowing — a failed run never leaves a\n * `deck.spec.json` behind that doesn't match what actually landed in\n * `pages/`/`assets/`. The `wx` no-overwrite guard above still runs first and\n * throws before this rollback scope is ever entered, so a pre-existing\n * `deck.spec.json` this call did not itself create is never at risk of\n * being deleted — deleting only ever targets the file this same invocation\n * just wrote.\n */\nexport async function runDisassemble(irPath: string, outDir: string): Promise<string> {\n const raw = await loadIrFile(irPath)\n const v = validateIr(raw)\n if (!v.ok) throw new PptfastError(`invalid IR:\\n${formatIssues(v.errors)}`)\n const { spec, pages } = disassembleDeck(v.ir!)\n\n // W5 whole-branch review finding 1 (CRITICAL, CWE-22): `id` is `slide.id`\n // off the parsed input IR (`disassembleDeck` passes a bare `slide.id`\n // through unchanged when present, `../spec/assemble.ts`) — unrestricted at\n // the schema layer, so an id like `\"../../../../escape\"` would otherwise\n // write outside `outDir`. Post-v0.3 W8 fix round (backlog item 8): checked\n // here, ahead of every write including `deck.spec.json` itself, so a\n // single unsafe id fails the whole call with nothing written at all,\n // rather than leaving a `deck.spec.json` that then needs rolling back.\n const ids = Object.keys(pages)\n for (const id of ids) assertSafeFileSegment(id, \"slide id\")\n\n const specPath = join(outDir, \"deck.spec.json\")\n await mkdir(outDir, { recursive: true })\n try {\n await writeFile(specPath, JSON.stringify(spec, null, 2) + \"\\n\", { flag: \"wx\" })\n } catch (e) {\n if ((e as NodeJS.ErrnoException).code === \"EEXIST\") {\n throw new PptfastError(`${specPath} already exists — refusing to overwrite an existing deck project`)\n }\n throw e\n }\n\n // From here on `specPath` is a file this call just created (the `wx` flag\n // above guarantees no pre-existing file survived to this point), so it is\n // safe to delete on any failure below — backlog item 8: a mid-way failure\n // used to leave `deck.spec.json` on disk with no matching pages/assets,\n // misrepresenting the deck project as already, successfully disassembled.\n const pagesDir = join(outDir, \"pages\")\n try {\n if (ids.length > 0) {\n await mkdir(pagesDir, { recursive: true })\n await Promise.all(\n ids.map((id) => {\n const content: PageContent = pages[id]!\n return writeFile(join(pagesDir, `${id}.json`), JSON.stringify(content, null, 2) + \"\\n\")\n }),\n )\n }\n\n const { count: assetCount, assetsDir } = await writeDeckAssets(\n v.ir!.assets.images,\n outDir,\n dirname(resolve(irPath)),\n )\n\n const pagesNote =\n ids.length > 0\n ? `${ids.length} page file${ids.length === 1 ? \"\" : \"s\"} to ${pagesDir}`\n : \"no pages (every slide was a placeholder)\"\n const assetsNote = assetCount > 0 ? `, and ${assetCount} asset file${assetCount === 1 ? \"\" : \"s\"} to ${assetsDir}` : \"\"\n return `wrote ${specPath}, ${pagesNote}${assetsNote}`\n } catch (e) {\n // Best-effort cleanup: a failure to delete the spec file must never mask\n // the real failure `e` below, so its own error is swallowed, not thrown.\n await rm(specPath, { force: true }).catch(() => {})\n throw e\n }\n}\n\n// ── migrate ──────────────────────────────────────────────────────────────\n\n/**\n * `pptfast migrate <input> -o <output>` (spec §9.1/§9.2/§9.3, vocabulary-v4\n * rename, task 2): the one deterministic conversion surface for both\n * artifacts this rename touches. Dispatches purely on whether `<input>`\n * resolves to a directory ({@link isDeckDirectory}) — the same signal every\n * other deck-accepting command already uses to branch between single-file\n * and deck-project-directory mode:\n *\n * - a directory containing `deck.plan.json` → {@link runMigrateDeckDir}:\n * rewrites it to `deck.spec.json` per spec §9.2's field mapping\n * ({@link migrateDeckPlanToSpec}, `../spec/migrate.ts`), written to\n * `<output>` (a directory — `<output>/deck.spec.json`).\n * - a file → {@link runMigrateIrFile}: must be an IR v3 document\n * (`version: \"3\"`), wraps {@link migrateIrV3ToV4} (`../ir/migrate.ts`),\n * written to `<output>` (a file). IR v2 is explicitly not accepted here\n * (spec §15.3: \"v2 无真实用户\" — `pptfast migrate` only supports v3→v4,\n * `validateIr`'s own v2 hard-reject message carries the full v2→v4\n * combined mapping for a caller who needs to convert one by hand).\n *\n * Both branches never overwrite `<output>` — a pre-existing file at the\n * resolved output path is a hard `PptfastError`, the same `wx`-flag EEXIST\n * guard `runDisassemble`/`runInit` already use elsewhere in this file (spec\n * §9.2: \"迁移工具必须默认写到新目标,不覆盖原文件\"). Neither branch runs a\n * model or reinterprets content — both are thin CLI shells over an\n * already-pure mapping function, per spec §9.3: \"只做已声明的结构映射,不\n * 运行模型,不重写内容,不重新选择 layout\".\n */\nexport async function runMigrate(input: string, output: string, cwd = process.cwd()): Promise<string> {\n const resolvedInput = resolve(cwd, input)\n if (await isDeckDirectory(resolvedInput)) {\n return runMigrateDeckDir(resolvedInput, output, cwd)\n }\n return runMigrateIrFile(resolvedInput, output, cwd)\n}\n\n/**\n * Deck-project-directory leg of {@link runMigrate}: reads `deck.plan.json`\n * out of `dir` (`loadIrFile`'s generic read-plus-parse, `./load-ir.ts` —\n * same helper `runSpecValidate` above uses, \"plan\" naming its own failure\n * messages), maps it through {@link migrateDeckPlanToSpec}\n * (`../spec/migrate.ts`, spec §9.2's field mapping), and writes the result\n * to `<output>/deck.spec.json`. `pages/*.json` and `assets/*` are untouched\n * — spec §9.2's mapping only touches `deck.plan.json`'s own top-level\n * `scenario` field and each page's `rhythm` field, both entirely absent from\n * the pages/assets directories.\n *\n * Deliberately does not delete or rename the source `deck.plan.json` —\n * spec §9.2: \"不覆盖原文件\" applies to the migration direction generally,\n * and leaving the old file in place is what lets {@link readSpecFile}-style\n * dual-file detection (`../cli/deck-dir.ts`) catch a half-finished migration\n * (both files present) instead of one command silently deciding the old\n * file is now garbage. The success message tells the caller to delete it\n * once they have confirmed the new file is correct.\n *\n * Checks for `deck.plan.json` up front (task 3, routed from task 2's\n * review) instead of letting a missing file fall through to\n * `loadIrFile`'s generic \"cannot read plan file\" — a directory that has\n * already been migrated (a `deck.spec.json` sitting there with no\n * `deck.plan.json` left to convert, the plan file having since been\n * deleted per this function's own success message) gets a dedicated\n * \"already migrated\" error instead of a message that reads like the\n * directory was never a deck project at all. A directory with neither file\n * still reaches `loadIrFile`'s generic error — this function has no more\n * specific diagnosis to offer than that one already gives.\n */\nasync function runMigrateDeckDir(dir: string, output: string, cwd: string): Promise<string> {\n const planPath = join(dir, PLAN_FILENAME)\n const sourceSpecPath = join(dir, SPEC_FILENAME)\n if (!(await pathExists(planPath)) && (await pathExists(sourceSpecPath))) {\n throw new PptfastError(\n `${dir} has ${SPEC_FILENAME} but no ${PLAN_FILENAME} — this deck project is already migrated, nothing to do`,\n )\n }\n const raw = await loadIrFile(planPath, \"plan\")\n const migrated = migrateDeckPlanToSpec(raw)\n const outDir = resolve(cwd, output)\n const specPath = join(outDir, SPEC_FILENAME)\n await mkdir(outDir, { recursive: true })\n try {\n await writeFile(specPath, JSON.stringify(migrated, null, 2) + \"\\n\", { flag: \"wx\" })\n } catch (e) {\n if ((e as NodeJS.ErrnoException).code === \"EEXIST\") {\n throw new PptfastError(`${specPath} already exists — refusing to overwrite, delete it first or choose a different -o`)\n }\n throw e\n }\n return `wrote ${specPath} — run \\`pptfast spec validate ${specPath}\\` to confirm it, then delete ${planPath} (a directory with both files present is rejected)`\n}\n\n/**\n * Single-file leg of {@link runMigrate}: requires an explicit `version: \"3\"`\n * (spec §9.3: \"IR v4 入口遇到 v3 时硬拒绝\" is `validateIr`'s own job — this\n * command instead requires v3 *specifically*, since v3 is the only version\n * it knows how to convert). `version: \"2\"` gets its own message pointing at\n * `validateIr`'s existing combined v2→v4 mapping rather than silently\n * routing it through the v3 vocabulary as a stepping stone (spec §15.3:\n * \"v2 无真实用户\", \"`pptfast migrate` 只支持 v3→v4,不接 v2\"). Any other\n * version (already v4, or missing/malformed entirely) is rejected with a\n * message naming what this command does accept.\n */\nasync function runMigrateIrFile(filePath: string, output: string, cwd: string): Promise<string> {\n const raw = await loadIrFile(filePath)\n const version = typeof raw === \"object\" && raw !== null ? (raw as Record<string, unknown>).version : undefined\n if (version === \"2\") {\n throw new PptfastError(\n \"pptfast migrate does not support IR v2 (spec §15.3: v2 has no real users) — run `pptfast validate` on the v2 file to see the full v2→v4 combined field mapping and rewrite it by hand\",\n )\n }\n if (version !== \"3\") {\n throw new PptfastError(\n `pptfast migrate only converts an IR v3 file (version: \"3\") or a deck project directory containing ${PLAN_FILENAME} — got version ${JSON.stringify(version)} in ${filePath}`,\n )\n }\n const parsed = PptxIRV3Schema.safeParse(raw)\n if (!parsed.success) {\n const detail = parsed.error.issues.map((issue) => `${issue.path.join(\".\") || \"(root)\"}: ${issue.message}`).join(\"\\n\")\n throw new PptfastError(`invalid IR v3 file ${filePath}:\\n${detail}`)\n }\n const migrated = migrateIrV3ToV4(parsed.data)\n const outPath = resolve(cwd, output)\n await mkdir(dirname(outPath), { recursive: true })\n try {\n await writeFile(outPath, JSON.stringify(migrated, null, 2) + \"\\n\", { flag: \"wx\" })\n } catch (e) {\n if ((e as NodeJS.ErrnoException).code === \"EEXIST\") {\n throw new PptfastError(`${outPath} already exists — refusing to overwrite, delete it first or choose a different -o`)\n }\n throw e\n }\n return `wrote ${outPath} (migrated IR v3 → v4)`\n}\n","import { readFile } from \"node:fs/promises\"\nimport { dirname, join, resolve } from \"node:path\"\nimport { z } from \"zod\"\nimport { PptfastError } from \"../errors\"\nimport { StyleOverrideSchema } from \"../ir\"\nimport { userConfigPath } from \"./home\"\n\n/**\n * Project-level deck defaults. Precedence (spec §7's four-layer chain, W5\n * task 5): CLI flag > project config (this schema, cwd walk-up) > user\n * config (`UserConfigSchema` below) > whatever the artifact itself already\n * carries (an authored IR's own `theme`, or the schema's own \"consulting\"\n * default when nothing anywhere sets one) — see `commands.ts`'s\n * `applyDeckConfig` for where all four layers actually get merged. `theme`\n * is kept an open string at this schema layer (mirrors `ThemeSchema` in\n * `ir/index.ts`) on purpose: `readConfigFile` below no longer checks it\n * against the installed set at read time — a config file's theme value\n * might sit behind a CLI flag or another layer that never actually gets\n * used, so rejecting it here would hard-fail a command over a value that\n * was never going to apply. `applyDeckConfig` runs that check once, at\n * resolution time, against whichever layer's value actually wins the chain\n * — the same \"unknown → PptfastError with the available list\" UX as\n * `validateIr`, just applied to the resolved value instead of unconditionally\n * to every layer.\n *\n * `decksDir` (W5 task 6, spec §7: a team that wants deck project\n * directories checked into the repo instead of living under\n * `~/.pptfast/decks` declares it here): a relative value resolves against\n * *this config file's own directory* (wherever `findConfig`'s cwd walk-up\n * found it) — never the CLI's cwd, and never `pptfastHome()`. Wins over the\n * user config's own `decksDir` (`UserConfigSchema` below) when both are\n * set, same project-beats-user precedence as `theme`/`style` above. The two\n * layers resolve against different bases, so this schema alone can't\n * express the final answer — `commands.ts`'s `resolveDecksDirSource`\n * computes the already-resolved absolute path before handing it down to\n * `./deck-dir.ts`'s `resolveDeckTarget` / `./home.ts`'s `decksRoot`, neither\n * of which knows there are two possible bases, only the final one.\n */\nconst ConfigSchema = z\n .object({\n theme: z.string().optional(),\n style: StyleOverrideSchema.optional(),\n decksDir: z.string().optional(),\n })\n .strict()\n\nexport type PptfastConfig = z.infer<typeof ConfigSchema>\n\n/**\n * User-level config schema (spec §7's four-layer chain — the layer between\n * project config and the artifact's own value): the same three deck-default\n * fields as {@link ConfigSchema} (`theme`/`style`/`decksDir`) — `decksDir`\n * is no longer project-config-free as of W5 task 6 (see {@link ConfigSchema}'s\n * own doc comment on that field), but the two layers still resolve it\n * against different bases: this user layer always resolves against\n * `pptfastHome()` (`./home.ts`'s `decksRoot`, this layer's one fixed\n * location), the project layer against the project config file's own\n * directory. Declared as its own flat object literal rather than\n * `ConfigSchema.extend(...)` — a shape this small is not worth taking on\n * zod's extend-then-restrict chaining, and it keeps both schemas readable\n * independently.\n *\n * `decksDir`: a relative value resolves against this config file's own\n * directory (`./home.ts`'s `pptfastHome()` — the only directory a user\n * config can ever live in, see `decksRoot`), never the CLI's cwd. No tilde\n * expansion — a literal `~/decks` is the literal relative path segment\n * `~/decks` under that base, not the home directory. The resulting (almost\n * certainly missing) directory surfaces through whatever downstream error\n * reads it, same as any other bad path.\n */\nconst UserConfigSchema = z\n .object({\n theme: z.string().optional(),\n style: StyleOverrideSchema.optional(),\n decksDir: z.string().optional(),\n })\n .strict()\n\nexport type UserPptfastConfig = z.infer<typeof UserConfigSchema>\n\nexport const CONFIG_FILENAME = \"pptfast.config.json\"\n\n/**\n * Shared read+parse+validate body for both config layers (project and user)\n * — same failure posture either way: a missing file is `null` (\"fine, no\n * config at this level\"), invalid JSON or a failed schema parse is a hard\n * {@link PptfastError} naming `path`. Deliberately does *not* check `theme`\n * against the installed set here — see {@link ConfigSchema}'s own doc\n * comment for why that moved to `applyDeckConfig` (`../cli/commands.ts`) at\n * resolution time instead, applied only to whichever layer's value actually\n * wins the four-layer chain.\n */\nasync function readConfigFile<T>(\n path: string,\n schema: z.ZodType<T>,\n): Promise<{ path: string; config: T } | null> {\n let text: string\n try {\n text = await readFile(path, \"utf8\")\n } catch {\n return null // no config at this level\n }\n let raw: unknown\n try {\n raw = JSON.parse(text) as unknown\n } catch (e) {\n throw new PptfastError(`${path} is not valid JSON: ${(e as Error).message}`)\n }\n const r = schema.safeParse(raw)\n if (!r.success) {\n const detail = r.error.issues\n .map((i) => `${i.path.join(\".\") || \"(root)\"}: ${i.message}`)\n .join(\"\\n\")\n throw new PptfastError(`invalid ${path}:\\n${detail}`)\n }\n return { path, config: r.data }\n}\n\n/** Walk from startDir up to the filesystem root looking for pptfast.config.json.\n * Invalid config is a hard error (with the file path in the message), never silently ignored. */\nexport async function findConfig(\n startDir: string,\n): Promise<{ path: string; config: PptfastConfig } | null> {\n let dir = resolve(startDir)\n for (;;) {\n const hit = await readConfigFile(join(dir, CONFIG_FILENAME), ConfigSchema)\n if (hit) return hit\n const parent = dirname(dir)\n if (parent === dir) return null\n dir = parent\n }\n}\n\n/**\n * User-level config (spec §7's four-layer chain, the layer below project\n * config): a single fixed path (`userConfigPath()`, `./home.ts` —\n * `$PPTFAST_HOME` or `~/.pptfast`), no cwd walk-up — there is exactly one\n * user config, unlike project config which can live at any ancestor of cwd.\n * Same missing/invalid posture as {@link findConfig}: missing file is fine\n * (`null`), invalid JSON or schema is a hard {@link PptfastError} with the\n * path.\n */\nexport async function findUserConfig(): Promise<{ path: string; config: UserPptfastConfig } | null> {\n return readConfigFile(userConfigPath(), UserConfigSchema)\n}\n","import { homedir } from \"node:os\"\nimport { join, resolve } from \"node:path\"\n\n/**\n * Root directory for pptfast's user-level state — deck project defaults\n * (`decksRoot`) and the user config file (`userConfigPath`), spec §7's\n * storage-policy decision. `PPTFAST_HOME` overrides it wholesale (CI /\n * containers). Otherwise a single predictable dotdir under the user's home,\n * the same posture as `.ssh`/`.npmrc`/`.aws`/`~/.claude` — deliberately\n * *not* the per-OS XDG/AppData split an `env-paths`-style helper would give:\n * deck project directories are large working files an agent produces, not\n * roaming-synced app config, and this tool's users (developers and agents)\n * benefit more from one predictable path than from OS-idiomatic placement.\n * Read fresh on every call (never cached) — `PPTFAST_HOME` is meant to be\n * redirectable per-process (tests set it via `process.env` before calling).\n */\nexport function pptfastHome(): string {\n return process.env.PPTFAST_HOME ?? join(homedir(), \".pptfast\")\n}\n\n/**\n * Default parent directory for bare-name deck resolution\n * (`$PPTFAST_HOME/decks/<name>/`, `./deck-dir.ts`'s `resolveDeckTarget`).\n * `config` is deliberately a minimal structural shape (`{ decksDir?: string\n * }`), not `UserPptfastConfig` itself — `./config.ts` already imports\n * `userConfigPath` from this module, so importing its type back here would\n * be circular. Redirecting `decksDir` is a user-identity concern (spec §7:\n * user-identity-class config belongs to the user layer) — a team that wants\n * deck projects tracked inside a repo instead reaches for project-level\n * `pptfast.config.json`, a separate, unrelated mechanism.\n *\n * A relative `decksDir` resolves against `pptfastHome()` itself — the only\n * directory a user config file can ever live in (see `userConfigPath`\n * below) — never the CLI's cwd. An absolute value passes through unchanged\n * (`path.resolve`'s own semantics handle both in one call, no separate\n * `isAbsolute` branch needed). No tilde expansion: a literal `~/decks` is\n * one relative path segment, not shorthand for the home directory — see\n * `./config.ts`'s `UserConfigSchema` doc comment.\n */\nexport function decksRoot(config?: { decksDir?: string }): string {\n return resolve(pptfastHome(), config?.decksDir ?? \"decks\")\n}\n\n/** Path to the user-level config file (theme/style defaults + `decksDir` redirect, spec §7's four-layer chain). */\nexport function userConfigPath(): string {\n return join(pptfastHome(), \"config.json\")\n}\n","/**\n * Deck project directory fs shell (spec §7's \"deck project directory\"\n * scheme, W5 task 5). Everything here touches disk — the pure half\n * (locked-field injection, placeholder/orphan semantics) lives in\n * `../spec/assemble.ts`'s `assembleDeck`, zero-fs by design (`AGENTS.md`'s\n * layout rule: this module is the *only* place that reads `deck.spec.json`\n * / `pages/*.json` / `assets/*` off disk and calls straight through to it,\n * the same posture `./load-ir.ts` already holds for a single IR file). It\n * is also the only place that *writes* `assets/*`, on the disassemble side\n * ({@link writeDeckAssets}) — the mirror image of {@link scanAssets} below,\n * and the CLI-shell half of `disassembleDeck`'s otherwise-lossy asset\n * handling (see that function's own doc comment in `../spec/assemble.ts`).\n *\n * Directory layout (spec §6/§7 — the locked artifact renamed from\n * `deck.plan.json` to `deck.spec.json`, vocabulary-v4 rename, task 2):\n * ```\n * my-deck/\n * deck.spec.json the locked spec — page order's sole source of truth\n * pages/<page-id>.json one file per filled page, content only (no type/heading)\n * assets/ local images, auto-registered by filename\n * ```\n *\n * A directory carrying the pre-rename `deck.plan.json` only (no\n * `deck.spec.json` yet) is no longer read directly — `pptfast migrate\n * <dir> -o <dir>` (`./commands.ts`'s `runMigrate`) converts it in place per\n * spec §9.2's field mapping. A directory carrying *both* files at once is a\n * hard error ({@link readSpecFile} below) — spec §9.2: \"目录中同时出现\n * `deck.plan.json` 和 `deck.spec.json` 时应硬报错,不能猜测优先级\".\n */\nimport { copyFile, mkdir, readFile, readdir, stat, writeFile } from \"node:fs/promises\"\nimport { basename, extname, isAbsolute, join, relative, resolve } from \"node:path\"\nimport { PptfastError } from \"../errors\"\nimport { assembleDeck, type AssembleResult, type PageContent } from \"../spec/assemble\"\nimport { decksRoot } from \"./home\"\nimport { EXT_BY_MIME, loadIrFile } from \"./load-ir\"\n\n/** The pre-rename artifact name (vocabulary-v4 rename, spec §6/§9.2) — no\n * longer read directly by {@link readSpecFile}, but still needed to (a)\n * detect the dual-file hard-error case and (b) as the migrate command's own\n * read source (`./commands.ts`'s `runMigrate`). Both exported for that\n * second reason — `runMigrate` needs the exact same two filenames, and\n * duplicating the literal strings there would risk the two modules drifting\n * on spelling. */\nexport const PLAN_FILENAME = \"deck.plan.json\"\nexport const SPEC_FILENAME = \"deck.spec.json\"\n// Exported (serve wave, task S1) so `./serve.ts` can build its fs.watch\n// roots from the exact same directory names this module already treats as\n// the deck-project layout's source of truth, instead of a second hardcoded\n// \"pages\"/\"assets\" literal that could drift from these.\nexport const PAGES_DIRNAME = \"pages\"\nexport const ASSETS_DIRNAME = \"assets\"\n\n// ── path-traversal safety (CWE-22 defense) ──────────────────────────────\n\n/**\n * Rejects an `id` that is unsafe to join into a page/asset file path (W5\n * whole-branch review finding 1, CRITICAL — reproduced by the reviewer\n * against both call sites below). `slide.id` and `assets.images` keys are\n * both open, unrestricted `z.string()` at the schema layer (`../ir/index.ts`\n * — no format rule there by design, cross-slide/id rules are `validateIr`'s\n * job, see `SlideSchema.id`'s own doc comment), so a hand-authored IR can set\n * either to anything, including `\"../../../../escape\"` — and both\n * {@link writeOneAsset} below and `runDisassemble`'s page write\n * (`./commands.ts`) join that value straight into a write path with no\n * check of their own before this task. Call this before building any path\n * from an id sourced off a parsed IR.\n *\n * A value `join()`'d as (a possibly-suffixed) single trailing path segment\n * can only ever escape `base` if it is itself absolute, contains a `/` or\n * `\\` separator (smuggling in extra segments, e.g. `\"../../../escape\"`), or\n * is exactly `\"..\"` (the one separator-free value that is still a traversal\n * on its own, e.g. when a sink appends an empty suffix) — those lexical\n * checks alone already make every call site in this file safe regardless of\n * what it joins `id` under. `relative(base, resolve(base, id))` escaping\n * `base` (starts with `\"..\"`, or is itself absolute) is checked too, as\n * defense-in-depth on top of the lexical checks, not a substitute for them —\n * `base` here is a fixed stand-in directory rather than either real sink's\n * actual `assetsDir`/`pagesDir`: the property under test (\"can this id ever\n * resolve outside whatever directory it's joined under\") is a function of\n * `id` alone once the lexical checks above hold, true for any base, so a\n * real caller-supplied base would add no extra precision — see this\n * function's own test suite for the two attack shapes this closes.\n *\n * `context` names the offending id's role (`\"slide id\"`, `\"asset id\"`) so\n * the thrown message points at which field was unsafe.\n */\nexport function assertSafeFileSegment(id: string, context: string): void {\n const safeBase = resolve(\"/pptfast-safe-base\")\n const rel = relative(safeBase, resolve(safeBase, id))\n const safe =\n !isAbsolute(id) && !id.includes(\"/\") && !id.includes(\"\\\\\") && id !== \"..\" && !rel.startsWith(\"..\") && !isAbsolute(rel)\n if (!safe) {\n throw new PptfastError(\n `${context} \"${id}\" is not a safe file name — ids used as page/asset file names must not contain path separators or \"..\"`,\n )\n }\n}\n\n// ── bare-name / path resolution ─────────────────────────────────────────\n\n/**\n * Returns true when `stat(path)` succeeds and names a directory — the\n * single source of truth every deck-accepting CLI command (`assemble`,\n * `disassemble`'s input is always a file so it never calls this, `validate`/\n * `render`/`preview`) uses to branch between single-file IR and deck-project\n * directory mode. A missing path (`ENOENT`) reads as \"not a directory\"\n * rather than propagating — the caller's next step (`loadIrFile` for the\n * single-file branch) already has its own readable \"cannot read\" error for\n * a path that turns out not to exist at all, and re-deriving that\n * distinction here would just duplicate it. Any *other* `stat` failure\n * (`EACCES`, `ENOTDIR` via a non-directory path segment, ...) rethrows\n * wrapped in {@link PptfastError} instead — silently reading a real\n * permission or filesystem problem as \"not a directory, try it as a single\n * IR file\" produces a strictly more confusing downstream error than\n * surfacing the actual failure here.\n */\nexport async function isDeckDirectory(path: string): Promise<boolean> {\n try {\n return (await stat(path)).isDirectory()\n } catch (e) {\n if ((e as NodeJS.ErrnoException).code === \"ENOENT\") return false\n throw new PptfastError(`cannot check ${path}: ${(e as Error).message}`)\n }\n}\n\n/**\n * true when `stat(path)` succeeds (file or directory, no distinction) — the\n * same ENOENT-vs-everything-else posture as {@link isDeckDirectory} just\n * above, factored out because `resolveDeckTarget` below needs plain\n * existence (a candidate can legitimately be a file *or* a directory) at\n * two different points, and `runAssemble` (`../cli/commands.ts`) needs it\n * once more, to tell \"target does not exist at all\" (the existing, detailed\n * `readDeckDir` error, expected-layout hint included) apart from \"target\n * exists but is not a directory\" (a friendlier, immediate error — see that\n * function).\n */\nexport async function pathExists(path: string): Promise<boolean> {\n try {\n await stat(path)\n return true\n } catch (e) {\n if ((e as NodeJS.ErrnoException).code === \"ENOENT\") return false\n throw new PptfastError(`cannot check path ${path}: ${(e as Error).message}`)\n }\n}\n\n/**\n * Path-vs-bare-name resolution (spec §7's CLI bare-name resolution): an\n * `arg` that contains a path separator, or that exists locally relative to\n * `cwd` (file *or* directory — a same-directory `deck.json` has no\n * separator but must still resolve as the obviously-intended local file,\n * not get redirected to the deck home), resolves against `cwd` and comes\n * back as a fully-resolved path in both cases — an explicit or\n * locally-resolvable path always wins over the bare-name interpretation,\n * but is never handed back unresolved: every downstream fs call (Node's\n * `readFile` et al.) always resolves a relative path against the process's\n * *real* `process.cwd()`, which only coincides with this function's `cwd`\n * parameter in production (`commands.ts` never passes one explicitly, so it\n * defaults to the real thing) — a test that exercises a different `cwd`\n * without an actual `process.chdir()` needs the already-resolved path back,\n * not the bare `arg`, or the caller's next fs call would silently resolve\n * against a completely different, real cwd. An absolute `arg` is unaffected\n * either way — `path.resolve` returns an absolute later segment as-is.\n *\n * Otherwise `arg` is treated as a deck name under `decksRoot(config)`\n * (`./home.ts` — `$PPTFAST_HOME/decks/<name>`, or a `decksDir` override) —\n * but only when that candidate actually exists. When *neither* the local\n * path nor the deck-home candidate exists, this returns the local\n * (cwd-resolved) path rather than the deck-home guess: a bare arg that was\n * actually a typo'd local filename (`pptfast validate typo.json`) should\n * have its eventual \"cannot read\" error name the file the user typed, not\n * an unrelated `~/.pptfast/decks/typo.json` path they never meant. `config`\n * is not either config layer's raw shape — it is the already-resolved\n * effective `decksDir` source (project `pptfast.config.json`'s own value,\n * spec §7's project-level escape hatch, W5 task 6, when it sets one, else\n * the user config's, `./config.ts`'s `UserPptfastConfig`) computed once by\n * the caller (`commands.ts`'s `resolveDecksDirSource`) and passed in here\n * already resolved to an absolute path when it came from the project layer\n * — this function itself has no reason to know there were ever two\n * possible layers or bases, only the final answer. The caller fetches both\n * config files at most once per command and passes them in rather than\n * this function reaching for either itself, so a command that already\n * needs one of them for other reasons (theme/style resolution) never reads\n * the same file twice.\n *\n * An empty or whitespace-only `arg` (W5 whole-branch review finding 4) is\n * rejected up front rather than silently resolving to `cwd` itself — without\n * this guard, `resolve(cwd, \"\")` returns `cwd` unchanged and `pathExists`\n * always finds it (a directory always exists), so the empty string would\n * otherwise quietly pass through as \"the target is cwd\", surfacing later as\n * a confusing missing-`deck.spec.json` error instead of naming the actual\n * problem (an empty target argument) up front.\n */\nexport async function resolveDeckTarget(\n arg: string,\n config?: { decksDir?: string },\n cwd: string = process.cwd(),\n): Promise<string> {\n if (arg.trim() === \"\") throw new PptfastError(\"deck target must not be empty\")\n if (arg.includes(\"/\") || arg.includes(\"\\\\\")) return resolve(cwd, arg)\n const local = resolve(cwd, arg)\n if (await pathExists(local)) return local\n const fallback = join(decksRoot(config), arg)\n return (await pathExists(fallback)) ? fallback : local\n}\n\n// ── deck.spec.json ──────────────────────────────────────────────────────\n\n/** The expected-layout block of {@link readSpecFile}'s missing-file error —\n * `padEnd`-aligned programmatically (not hand-counted spaces in a template\n * literal) so the three column widths can't silently drift out of line\n * when one of the three filename/`*_DIRNAME` constants above changes. */\nfunction expectedLayoutHint(): string {\n const rows: [string, string][] = [\n [SPEC_FILENAME, \"the locked spec (see `pptfast spec validate`)\"],\n [`${PAGES_DIRNAME}/<page-id>.json`, \"one file per filled page (missing pages become placeholders)\"],\n [`${ASSETS_DIRNAME}/`, \"optional local images\"],\n ]\n const width = Math.max(...rows.map(([name]) => name.length)) + 2\n return rows.map(([name, desc]) => ` ${name.padEnd(width)}${desc}`).join(\"\\n\")\n}\n\n/**\n * Reads `deck.spec.json` out of `dir` (vocabulary-v4 rename, task 2 —\n * this function used to read the pre-rename `deck.plan.json` directly; it\n * no longer does). Three failure shapes, each with its own message:\n *\n * - both `deck.plan.json` and `deck.spec.json` present — a hard error, spec\n * §9.2: \"目录中同时出现 `deck.plan.json` 和 `deck.spec.json` 时应硬报错,\n * 不能猜测优先级\" (\"hard error, never guess which one wins\"). Checked\n * before the missing-file branch below so a caller that just ran\n * `pptfast migrate <dir> -o <dir>` (which writes `deck.spec.json`\n * *alongside* the pre-existing `deck.plan.json`, never deleting it) gets\n * pointed at deleting the old file, not a generic \"not a deck project\"\n * message.\n * - only `deck.plan.json` present (no `deck.spec.json` yet) — this\n * directory predates the rename and is no longer read directly; the\n * message points at `pptfast migrate` instead of the generic missing-file\n * hint, since the fix here is a one-command conversion, not authoring a\n * fresh file from scratch.\n * - neither file present — the pre-existing \"friendlier message over\n * `loadIrFile`'s generic \"cannot read\"\" this function has always had: the\n * one failure a deck-directory caller is most likely to hit by typo or by\n * pointing at a directory that was never a deck project in the first\n * place, so the error spells out the expected layout and points at\n * `pptfast spec validate` rather than leaving the caller to guess.\n */\nasync function readSpecFile(dir: string): Promise<unknown> {\n const specPath = join(dir, SPEC_FILENAME)\n const planPath = join(dir, PLAN_FILENAME)\n const [specExists, planExists] = await Promise.all([pathExists(specPath), pathExists(planPath)])\n if (specExists && planExists) {\n throw new PptfastError(\n `both ${SPEC_FILENAME} and ${PLAN_FILENAME} exist in ${dir} — ambiguous, refusing to guess which one wins. Delete ${PLAN_FILENAME} once you have confirmed ${SPEC_FILENAME} is correct (\\`pptfast migrate\\` never deletes the source file it read)`,\n )\n }\n if (!specExists && planExists) {\n throw new PptfastError(\n `${dir} has ${PLAN_FILENAME} but no ${SPEC_FILENAME} — deck project directories now use ${SPEC_FILENAME}. Run \\`pptfast migrate ${dir} -o ${dir}\\` to convert it`,\n )\n }\n let text: string\n try {\n text = await readFile(specPath, \"utf8\")\n } catch (e) {\n if ((e as NodeJS.ErrnoException).code === \"ENOENT\") {\n throw new PptfastError(\n `no ${SPEC_FILENAME} in ${dir} — expected a deck project directory:\\n${expectedLayoutHint()}`,\n )\n }\n throw new PptfastError(`cannot read spec file: ${specPath}`)\n }\n try {\n return JSON.parse(text) as unknown\n } catch (e) {\n throw new PptfastError(`spec file ${specPath} is not valid JSON: ${(e as Error).message}`)\n }\n}\n\n// ── pages/<id>.json ──────────────────────────────────────────────────────\n\n/**\n * Reads every `pages/<id>.json` file into a `{ id: parsedContent }` record —\n * `id` is the filename sans `.json` (spec §7: one file per page, named by a\n * stable id). A missing `pages/` directory (`ENOENT`) is not an error, just\n * an empty record (a brand-new deck project with a spec and no filled pages\n * yet is exactly `assembleDeck`'s \"every page becomes a placeholder\" case)\n * — but `pages/` existing as something that cannot be read as a directory\n * (e.g. a file sitting where a directory was expected, `ENOTDIR`) is a real\n * problem and throws {@link PptfastError} naming the path, not silently\n * \"zero pages\" (same ENOENT-vs-everything-else posture as\n * {@link isDeckDirectory} above). Non-`.json` entries (a stray `.DS_Store`,\n * an editor swap file, a subdirectory) are silently skipped rather than fed\n * to `JSON.parse` — `.json` is the only declared file shape for this\n * directory, so anything else was never a page file to begin with, not a\n * malformed one. `pages` is deliberately typed `Record<string, unknown>`\n * here (not `Record<string, PageContent>`) — each value's actual shape is\n * checked by `assembleDeck` itself, the same `unknown`-until-validated\n * boundary its own doc comment describes. Entries are read concurrently\n * (`Promise.all`) — independent files, each writing its own `pages[id]` key,\n * nothing to race on.\n */\nasync function readPages(dir: string): Promise<Record<string, unknown>> {\n const pagesDir = join(dir, PAGES_DIRNAME)\n let entries: string[]\n try {\n entries = (await readdir(pagesDir, { withFileTypes: true }))\n .filter((entry) => entry.isFile() && extname(entry.name) === \".json\")\n .map((entry) => entry.name)\n } catch (e) {\n if ((e as NodeJS.ErrnoException).code === \"ENOENT\") return {}\n throw new PptfastError(`cannot read ${PAGES_DIRNAME}/ directory ${pagesDir}: ${(e as Error).message}`)\n }\n const pages: Record<string, unknown> = {}\n await Promise.all(\n entries.map(async (entry) => {\n const id = basename(entry, \".json\")\n pages[id] = await loadIrFile(join(pagesDir, entry), `page \"${id}\"`)\n }),\n )\n return pages\n}\n\n// ── assets/ ──────────────────────────────────────────────────────────────\n\n/**\n * Scans `assets/` and maps each file to an `assets.images` entry (spec §7's\n * assets-mapping rule): `id` is the filename sans extension, `src` is the\n * `assets/<filename>` path *relative to the deck directory* — resolved to\n * actual bytes later by the existing `resolveLocalAssets` (`./load-ir.ts`),\n * called with the deck directory as its base (see `commands.ts`). A missing\n * `assets/` directory (`ENOENT`) is zero assets, same as a missing `pages/`\n * above — anything else (`ENOTDIR`, a permission error, ...) throws\n * {@link PptfastError} naming the path rather than silently reading as \"no\n * assets here\" (see {@link readPages}'s own note on this). Dotfiles\n * (`.DS_Store` and friends — `extname` returns `\"\"` for these, so their\n * \"id\" would otherwise be the whole filename) are skipped: they are never a\n * legitimate image, and `resolveLocalAssets` inlines *every* registered\n * entry unconditionally, so a stray metadata file left registered would\n * fail the whole render with a confusing \"unsupported image format\" error\n * for an asset nothing in the deck ever references. Two files that\n * normalize to the same id (`logo.png` and `logo.jpg`) is a genuine\n * authoring ambiguity — same \"structural mismatch always errors\" posture as\n * `assembleDeck`'s orphan-page check — reported with both filenames so the\n * fix (rename one) is obvious.\n */\nasync function scanAssets(dir: string): Promise<Record<string, { src: string }>> {\n const assetsDir = join(dir, ASSETS_DIRNAME)\n let entries: string[]\n try {\n entries = (await readdir(assetsDir, { withFileTypes: true }))\n .filter((entry) => entry.isFile() && !entry.name.startsWith(\".\"))\n .map((entry) => entry.name)\n } catch (e) {\n if ((e as NodeJS.ErrnoException).code === \"ENOENT\") return {}\n throw new PptfastError(`cannot read ${ASSETS_DIRNAME}/ directory ${assetsDir}: ${(e as Error).message}`)\n }\n const images: Record<string, { src: string }> = {}\n const sourceFile = new Map<string, string>()\n for (const entry of entries) {\n const id = basename(entry, extname(entry))\n const previous = sourceFile.get(id)\n if (previous !== undefined) {\n throw new PptfastError(\n `${ASSETS_DIRNAME}/${previous} and ${ASSETS_DIRNAME}/${entry} both register image id \"${id}\" — rename one of the files`,\n )\n }\n sourceFile.set(id, entry)\n images[id] = { src: `${ASSETS_DIRNAME}/${entry}` }\n }\n return images\n}\n\n// ── readDeckDir ──────────────────────────────────────────────────────────\n\nexport interface DeckDirResult extends AssembleResult {\n /** Absolute path to the deck directory — the base `resolveLocalAssets`\n * should resolve this IR's (relative) asset paths against. */\n deckDir: string\n}\n\n/**\n * Reads a deck project directory end to end: spec + pages → `assembleDeck`\n * (locked-field injection, placeholder/orphan semantics — see that\n * function's own doc comment) → assets/ scan merged into the assembled IR's\n * `assets.images` (assemble first, then inject — a deck spec has no `assets`\n * field of its own, so there is never a pre-existing id for a scanned asset\n * to collide with, only the intra-`assets/`-directory collision\n * {@link scanAssets} itself guards against).\n *\n * The merge rebuilds `ir.assets` as a fresh object (`{ images: { ...,\n * ...images } }`) rather than assigning into `ir.assets.images` in place —\n * deliberately, not just style: `PptxIRSchema`'s `assets` field defaults to\n * a *static* object literal (`AssetsSchema.default({ images: {} })`,\n * `../ir/index.ts`), and a deck spec never sets its own `assets` (assembleDeck's\n * raw object omits the key entirely, same as every other field it lets the\n * schema default), so *every* assembled deck's `ir.assets.images` starts out\n * as that one schema-default object — zod does not deep-clone a static\n * default per parse, only the immediately-defaulted field itself, so nested\n * defaults below it (`images: {}` inside `AssetsSchema`'s own default) keep\n * one shared identity across unrelated parses. Mutating that shared object\n * in place (`ir.assets.images[id] = asset`, the first version of this\n * function) would silently register one deck project's local images onto\n * every other deck assembled in the same process — confirmed with a\n * standalone repro against this exact schema shape before writing this\n * comment. Rebuilding the object sidesteps the shared reference without\n * needing to touch the schema itself.\n *\n * The merged result is spliced in via a shallow clone of the whole `ir`\n * object (`{ ...ir, assets: ... }`), not a `ir.assets = ...` reassignment\n * onto the object `assembleDeck` returned (post-v0.3 W8 fix round, backlog\n * item 4, `.issues/notes/2026-07-18-post-v03-backlog.md` #4): the earlier\n * version mutated `assembleDeck`'s own return value in place, which is\n * harmless *today* only because `variety.ts`'s `deckSeedCache` and\n * `layout-selection.ts`'s `deckEffectiveLayoutIdsCache` — the only two\n * consumers that key a `WeakMap` off an `ir` object's identity — never read\n * `.assets` (confirmed by reading both cache-populating functions: they only\n * touch `seed`/`filename`/`theme.id`/`theme.style`/`narrative`/\n * `slides[].heading`/`.id`/`.type`/`.layout`/`.background`). Cloning\n * instead of mutating means the object\n * `assembleDeck` returned is never touched, and this function's own return\n * value is a distinct identity no earlier reference could have already\n * cached against — correct-by-construction regardless of what a future\n * cache keys on, not just correct because of what today's two caches happen\n * to skip.\n */\nexport async function readDeckDir(dir: string): Promise<DeckDirResult> {\n const deckDir = resolve(dir)\n const spec = await readSpecFile(deckDir)\n const pages = await readPages(deckDir)\n const { ir, generatedSeed, materializedLayoutCount } = assembleDeck(spec, pages as Record<string, PageContent>)\n const images = await scanAssets(deckDir)\n const merged = { ...ir, assets: { images: { ...ir.assets.images, ...images } } }\n return { ir: merged, generatedSeed, materializedLayoutCount, deckDir }\n}\n\n// ── assets/ (write direction — disassemble) ─────────────────────────────\n\nexport interface WriteDeckAssetsResult {\n /** Number of `ir.assets.images` entries materialized into `assets/`. */\n count: number\n /** Absolute path to the `assets/` directory written into — never created\n * (and this path never exists) when `count` is 0. */\n assetsDir: string\n}\n\n/**\n * Write direction of the assets/ concept — the mirror image of\n * {@link scanAssets} above, and the CLI-shell half of `disassembleDeck`'s\n * documented-lossy `assets` handling (`../spec/assemble.ts`'s own doc\n * comment on that function): that pure function never touches\n * `ir.assets.images` at all (its `{ spec, pages }` return has no `assets`\n * field), so without this step a disassembled directory would carry\n * `asset_id` references inside `pages/*.json` with nothing under `assets/`\n * backing them — exactly the \"image deck round-trips to a missing image\"\n * bug this function exists to close. Called by `runDisassemble`\n * (`../cli/commands.ts`) with the source IR's own `assets.images` map and\n * `sourceBaseDir` (the *input* IR file's own directory — the same base\n * `resolveLocalAssets`, `./load-ir.ts`, would resolve a relative local src\n * against at render time).\n *\n * Three source shapes, three outcomes (per entry, all independent —\n * written concurrently via `Promise.all`):\n * - `data:<mime>;base64,<payload>` → decoded and written to\n * `assets/<id><ext>`, `ext` looked up from `mime` via `EXT_BY_MIME`\n * (`./load-ir.ts`). An unrecognized mime or a non-base64 data URI is a\n * hard {@link PptfastError} naming the asset, not a silent skip.\n * - a local file path (relative resolves against `sourceBaseDir`, the same\n * `isAbsolute(src) ? src : resolve(base, src)` rule `resolveLocalAssets`\n * itself uses) → copied byte-for-byte into `assets/<id><origExt>`. An\n * unreadable source (moved/deleted/permission-denied since the IR was\n * generated) is a hard {@link PptfastError} naming the asset and the path\n * that could not be read.\n * - `http(s)://` → always a hard {@link PptfastError} — a URL asset has no\n * local bytes to write at all. The fix is on the deck author's side\n * (inline it as a data URI, or download it first), not something this\n * function can paper over.\n *\n * Written entries need no spec or page record of their own: `readDeckDir`'s\n * own {@link scanAssets} re-registers every file under `assets/` purely by\n * scanning the directory, the same way it would for a hand-added image —\n * this function's only job is making sure the bytes are there.\n */\nexport async function writeDeckAssets(\n images: Record<string, { src: string }>,\n outDir: string,\n sourceBaseDir: string,\n): Promise<WriteDeckAssetsResult> {\n const entries = Object.entries(images)\n const assetsDir = join(outDir, ASSETS_DIRNAME)\n if (entries.length === 0) return { count: 0, assetsDir }\n await mkdir(assetsDir, { recursive: true })\n await Promise.all(entries.map(([id, asset]) => writeOneAsset(id, asset.src, assetsDir, sourceBaseDir)))\n return { count: entries.length, assetsDir }\n}\n\n/** `data:<mime>;base64,<payload>` — the only data-URI shape any producer in\n * this codebase ever writes (`resolveLocalAssets`, `./load-ir.ts`, and the\n * sharp/canvas recode paths in `../platform/`) — matched strictly rather\n * than handling arbitrary charset params or non-base64 payloads nothing\n * here produces. */\nconst DATA_URI_RE = /^data:([^;,]+);base64,(.*)$/s\n\nasync function writeOneAsset(id: string, src: string, assetsDir: string, sourceBaseDir: string): Promise<void> {\n // W5 whole-branch review finding 1: one guard at the top covers both\n // write branches below (data-URI and local-file-copy) — `id` is the same\n // value regardless of which branch runs, so there is nothing branch-\n // specific about the check itself, only about what gets appended after it.\n assertSafeFileSegment(id, \"asset id\")\n if (src.startsWith(\"data:\")) {\n const match = DATA_URI_RE.exec(src)\n if (!match) {\n throw new PptfastError(`asset \"${id}\": only base64-encoded data URIs can be disassembled (malformed data URI)`)\n }\n const mime = match[1]\n const payload = match[2]\n const ext = EXT_BY_MIME[mime]\n if (!ext) {\n throw new PptfastError(\n `asset \"${id}\": cannot disassemble a data URI with mime \"${mime}\" — expected one of ${Object.keys(EXT_BY_MIME).join(\", \")}`,\n )\n }\n await writeFile(join(assetsDir, `${id}${ext}`), Buffer.from(payload, \"base64\"))\n return\n }\n if (/^https?:\\/\\//.test(src)) {\n throw new PptfastError(\n `asset \"${id}\": URL assets cannot be disassembled into a deck directory — inline it as a data URI or download it first`,\n )\n }\n const abs = isAbsolute(src) ? src : resolve(sourceBaseDir, src)\n try {\n await copyFile(abs, join(assetsDir, `${id}${extname(abs)}`))\n } catch {\n throw new PptfastError(`asset \"${id}\": cannot read source image ${abs} (from src \"${src}\") — cannot disassemble`)\n }\n}\n","import { readFile } from \"node:fs/promises\"\nimport { extname, isAbsolute, resolve } from \"node:path\"\nimport { PptfastError } from \"../errors\"\nimport type { PptxIR } from \"../ir\"\nimport { FORMAT_BY_MIME, MIME_BY_SNIFFED_FORMAT, sniffImageFormat } from \"../ir/asset-sniff\"\nimport { getPlatform } from \"../platform/registry\"\n\nconst MIME_BY_EXT: Record<string, string> = {\n \".png\": \"image/png\",\n \".jpg\": \"image/jpeg\",\n \".jpeg\": \"image/jpeg\",\n \".gif\": \"image/gif\",\n}\n\n/**\n * Mime → extension (with leading dot), one canonical extension per mime\n * (\"image/jpeg\" → \".jpg\", not \".jpeg\") — the reverse direction of\n * {@link MIME_BY_EXT} above, plus `image/webp`, which that table\n * deliberately omits: a local `.webp` file must keep taking the sharp\n * recode-to-png path in {@link resolveLocalAssets} below (see the e2e\n * \"webp asset regression leg\", `scripts/e2e.mts` — adding webp to\n * `MIME_BY_EXT` would silently skip that path for local files). This table\n * only serves the opposite direction — naming a file already decoded from a\n * `data:` URI (`writeDeckAssets`, `./deck-dir.ts`'s disassemble asset\n * materialization) — where a webp *payload* is a real possibility (e.g. an\n * upload-derived asset already embedded as webp) with no local file or\n * recode step involved at all.\n */\nexport const EXT_BY_MIME: Record<string, string> = {\n \"image/png\": \".png\",\n \"image/jpeg\": \".jpg\",\n \"image/gif\": \".gif\",\n \"image/webp\": \".webp\",\n}\n\n/** Read and JSON-parse a file with readable failure messages. `kind` names\n * what the file is expected to hold (e.g. \"spec\") for both failure\n * messages — defaults to \"IR\" for this function's original, still most\n * common caller (`runRender`/`runValidate`/`runPreview`, `./commands.ts`);\n * `runSpecValidate` passes \"spec\" so its own errors read correctly instead\n * of borrowing IR's wording for a file that was never one. */\nexport async function loadIrFile(irPath: string, kind = \"IR\"): Promise<unknown> {\n let text: string\n try {\n text = await readFile(irPath, \"utf8\")\n } catch {\n throw new PptfastError(`cannot read ${kind} file: ${irPath}`)\n }\n try {\n return JSON.parse(text) as unknown\n } catch (e) {\n throw new PptfastError(`${kind} file ${irPath} is not valid JSON: ${(e as Error).message}`)\n }\n}\n\n/**\n * Rewrite local file paths in assets.images to data URIs (CLI-only concern).\n * data: and http(s): sources pass through — the export pipeline inlines URLs\n * itself.\n *\n * Byte-level validation (borrow wave, Task 2 — D3, the local-file half of\n * `api.ts`'s `checkAssetBytes` doc comment — read that one for the full\n * rationale, this is its Node-only counterpart for the ingestion form\n * `validateIr` itself can't reach): every file's bytes are read once here\n * regardless of extension, then a zero-byte file is rejected loud\n * immediately (`dr/d-robustness.md`'s zero-byte-PNG probe — previously this\n * silently produced a 0-byte media part in the exported .pptx). For the four\n * extensions {@link MIME_BY_EXT} recognizes, the bytes are additionally\n * magic-byte-sniffed ({@link sniffImageFormat}) and checked against what the\n * extension claims: a corrupt/unrecognized header is rejected\n * (garbage-bytes-PNG probe), and so is a mismatch — a real PNG saved as\n * `.jpg` (the third D3 probe) — same reject-not-silently-relabel disposition\n * `checkAssetBytes` documents, for the same reason: an extension/content\n * mismatch would otherwise land in the exported package as a media part\n * whose declared type and actual bytes disagree, which `package-audit.ts`'s\n * structural rules never check. A file whose extension isn't one of those\n * four (webp and friends) skips the sniff/mismatch check and keeps taking\n * the `recodeImageToPng` path below unconditionally — sharp decodes by\n * content, not extension, so a mislabeled-but-decodable file there is\n * already harmless, and a genuinely corrupt one surfaces as sharp's own\n * decode error.\n */\nexport async function resolveLocalAssets(ir: PptxIR, baseDir: string): Promise<void> {\n for (const [name, asset] of Object.entries(ir.assets.images)) {\n const src = asset.src\n if (src.startsWith(\"data:\") || /^https?:\\/\\//.test(src)) continue\n const abs = isAbsolute(src) ? src : resolve(baseDir, src)\n let bytes: Buffer\n try {\n bytes = await readFile(abs)\n } catch {\n throw new PptfastError(`asset \"${name}\": cannot read image file ${abs} (from src \"${src}\")`)\n }\n if (bytes.length === 0) {\n throw new PptfastError(`asset \"${name}\": image file ${abs} is zero bytes — re-export or re-select the file`)\n }\n const ext = extname(abs).toLowerCase()\n const mime = MIME_BY_EXT[ext]\n if (mime) {\n const sniffed = sniffImageFormat(bytes)\n if (sniffed === null) {\n throw new PptfastError(\n `asset \"${name}\": image file ${abs} has a corrupt or unrecognized header (extension claims ${mime}) — re-export or re-select the file`,\n )\n }\n const expected = FORMAT_BY_MIME[mime]\n if (expected && sniffed !== expected) {\n throw new PptfastError(\n `asset \"${name}\": image file ${abs} is named \"${ext}\" but its bytes are actually ${MIME_BY_SNIFFED_FORMAT[sniffed]} — rename the file to match its real format, or re-export/re-save it as a genuine ${mime}`,\n )\n }\n asset.src = `data:${mime};base64,${bytes.toString(\"base64\")}`\n continue\n }\n const recode = getPlatform().recodeImageToPng\n if (!recode) {\n throw new PptfastError(\n `asset \"${name}\": unsupported image format \"${extname(abs)}\" — install sharp or convert to png/jpeg/gif`\n )\n }\n asset.src = await recode(`data:application/octet-stream;base64,${bytes.toString(\"base64\")}`)\n }\n}\n","/**\n * Pure string builder for `pptfast preview --html`'s self-contained review\n * bundle (v0.3 W7 task 1, spec §7 workflow ⑤): one `preview.html` with every\n * slide's already-rendered SVG (`renderSlideSvg`, `../api.ts`) inlined\n * directly into the markup, a bottom thumbnail filmstrip, keyboard (←/→) and\n * click navigation, and a page counter. No `fs` here on purpose — this\n * module only assembles a string; `runPreview` (`./commands.ts`) is the only\n * caller and the only place that touches disk, which keeps this file\n * trivially unit-testable (feed it slide data, assert on the returned\n * string) despite living under the Node-only `src/cli*` tree (AGENTS.md's\n * \"no Node-only deps\" layout rule is about `src/index.ts`'s own dependency\n * closure, not every file under `src/cli` — this one just happens to need\n * none anyway).\n *\n * Self-containment (the plan's hard requirement): every slide's SVG is\n * embedded as raw markup — never `<img src>`, never any other reference to\n * an external file — and the only CSS/JS in the document is inlined in\n * `<style>`/`<script>`. Local image assets are already `data:` URIs by the\n * time `runPreview` calls `renderSlideSvg` (`resolveLocalAssets`'s job,\n * `./load-ir.ts` — this module never touches assets itself) — *assuming*\n * every image asset the deck references is local or already a `data:` URI.\n * Known limitation: `resolveLocalAssets` deliberately passes a remote\n * `http(s):` asset `src` through untouched (the export pipeline inlines\n * those itself), so that src is left un-inlined and lands verbatim in this\n * bundle's embedded SVG as a live network reference, not a namespace URI —\n * breaking the zero-network-request guarantee for that one slide. Barring\n * that case, the only `http(s)` substrings that can appear anywhere in the\n * output are SVG namespace URIs (`xmlns=\"http://www.w3.org/2000/svg\"`,\n * emitted by `../svg/serialize.ts` on every slide) — XML namespace\n * identifiers, not network requests.\n *\n * Embed strategy (one `<svg>` per slide, not two): the thumbnail filmstrip\n * and the large \"stage\" view share the exact same DOM node per slide rather\n * than each holding its own copy — duplicating every slide's SVG (including\n * any inlined `data:` image payloads) would double the file's byte size for\n * an image-heavy deck for zero benefit, since only one size is ever on\n * screen for a given slide at a time. The one node that exists for a slide\n * lives in exactly one of two homes: `#pf-stage` (the slide currently being\n * viewed large) or its own `.pf-thumb-slot` (every other slide, shown small\n * in the filmstrip) — `<script>`'s `activate()` moves it between the two\n * with a plain `appendChild` (which detaches a node from its previous parent\n * automatically) when the viewer clicks a thumbnail or presses ←/→. Because\n * an inactive slide's badge travels with its node, and the active slide's\n * thumbnail button carries its own always-present badge, a placeholder page\n * still shows its \"unfilled\" mark in both places even though only one copy\n * of the slide's markup ever exists.\n *\n * Audit overlay + annotations (notes+preview wave, task 2): `buildPreviewHtml`\n * is still a pure renderer — `findings` (`../svg/audit/deck-audit.ts`'s\n * `AuditFinding`, reshaped locally as {@link PreviewHtmlFinding} so this file\n * still has no `../ir`/`../svg` import) and the placeholder-skip\n * {@link PreviewHtmlInput.auditNote} both arrive as plain input, the same way\n * `slides` already does; the caller (`runPreview`, `./commands.ts`) decides\n * *whether* to run `auditDeck` at all (skipped whenever the deck has any\n * placeholder page — a placeholder has nothing to audit and `auditDeck`\n * itself silently skips it, so surfacing a half-audited deck as if it were\n * clean would be misleading; the plan's contract is \"any placeholder present\n * → skip the whole overlay, one-line notice instead\"). Per-page finding\n * counts (thumbnail/stage badges) and the findings panel are rendered as\n * static markup at build time, not computed by client-side JS from the\n * embedded JSON — `findings` is known up front here, so there is nothing for\n * the browser to compute; the embedded `<script type=\"application/json\"\n * id=\"pf-audit-findings\">` blob exists only so a saved `preview.html` still\n * carries the structured findings for later tooling, not to drive the UI.\n * User content still flows through {@link escapeHtml} everywhere it lands in\n * HTML (a finding's `message` embeds a truncated quote of the offending\n * slide's own text) — the JSON blob instead goes through {@link embedJson},\n * which additionally neutralizes any literal `</script` sequence a slide's\n * text could contain (escaping every `<` to its unicode escape — valid\n * inside a JSON string, and the only character the HTML tokenizer would\n * otherwise use to end the `<script>` element early), the standard technique\n * for safely inlining untrusted JSON into a script tag.\n *\n * Annotations are a pure client-side, in-memory feature (no `fs`, no network\n * — this module builds a static page, `<script>`'s own closure holds the\n * state) keyed by each slide's 0-based array index, not by its `pageId` —\n * the id/index duality only matters at *export* time (`pageIdFor()` in\n * `JS` below derives `slide.id` when the active `.pf-slide` node carries a\n * `data-id`, else falls back to the 1-based page number — matching\n * `AuditFinding.page`'s own established \"1-based page number when there is\n * no slide id\" convention, `../svg/audit/deck-audit.ts`, rather than the\n * 0-based `data-index` this file otherwise uses internally). \"Export\n * revision requests\" reads the deck title back out of `#pf-title`'s already-\n * escaped `textContent` (browser-decoded HTML entities, exactly the original\n * `title` string) instead of embedding a second JS string literal for it —\n * one fewer thing that needs its own escaping discipline. The exported file\n * never touches disk or a server: `URL.createObjectURL` on an in-memory\n * `Blob` plus a synthetic `<a download>` click, the standard zero-backend\n * browser download pattern — keeping the self-containment invariant this\n * whole module exists to protect (no `fetch`, no `XMLHttpRequest`, no form\n * `action`).\n *\n * `buildExportBlob()` (serve wave, task S2 rework) factors the payload/Blob\n * construction half of that click handler into its own named function,\n * still private to the same `<script>` closure (it still reads `annotations`\n * and calls `pageIdFor()` directly) — and additionally assigns a reference\n * to it onto `window.__pptfastBuildExportBlob`. That one global function\n * handle is the sanctioned seam `pptfast serve` (`../cli/serve.ts`) calls to\n * reuse this exact serialization for its own `POST /revision-request`\n * submit flow, instead of forking a second copy of it (spec-plan.md design\n * ruling 5) — this module itself stays exactly as ignorant of\n * networking/serve as ever (still no `fetch` call anywhere in this file,\n * still a pure download feature); only the caller on the other side of that\n * global reaches for `fetch`.\n */\n\nexport interface PreviewHtmlSlideInput {\n /** Authoritative page number (1-based labels derive from this, not array\n * position, so a caller that filters/reorders `slides` still gets\n * correct output). */\n index: number\n /** Stable slide id (`slide.id`, `../ir/index.ts`'s `SlideSchema`) when the\n * deck sets one. User content — HTML-escaped wherever it is shown. */\n id?: string\n /** `slide.type` — `cover`/`chapter`/`content`/`ending` in practice, kept\n * as a plain `string` here so this module has no dependency on `../ir`. */\n type: string\n /** Already-rendered standalone SVG markup for this slide (`renderSlideSvg`,\n * `../api.ts`) — embedded verbatim: trusted, self-produced markup, never\n * escaped (escaping it would corrupt the SVG/XML syntax itself). */\n svg: string\n /** `slide.placeholder` — an unfilled page (assemble's stand-in for content\n * nobody has written yet, W5 task 1). Renders a visible \"unfilled\" badge:\n * this bundle exists for a human/agent visual review, so an unfilled page\n * must never look indistinguishable from a finished one. */\n placeholder?: boolean\n}\n\n/**\n * One `auditDeck` finding (`AuditFinding`, `../svg/audit/deck-audit.ts`),\n * reshaped to this module's own minimal fields only — dropping `detail`\n * (never shown) keeps this file dependency-free of `../svg` the same way it\n * is already dependency-free of `../ir` (see the module doc comment). `page`\n * is 1-based, matching `PreviewHtmlSlideInput.index + 1` for the slide it\n * belongs to (both ultimately trace back to the same `ir.slides` array\n * position in `runPreview`, `./commands.ts`). User content — `message`\n * embeds a truncated quote of the offending slide's own text — HTML-escaped\n * wherever it is shown, same as every other user-content field in this file.\n */\nexport interface PreviewHtmlFinding {\n page: number\n slideId?: string\n code: string\n message: string\n}\n\n/**\n * `AuditChecks` (`../svg/audit/deck-audit.ts`), reshaped locally the same\n * way `findings` is reshaped to {@link PreviewHtmlFinding} — keeps this file\n * free of a `../svg` import (see the module doc comment). Literal state\n * words only, mirroring the source type exactly: this wave's soul\n * constraint is \"not checked must never read as passed\", so `pixels` being\n * `\"not-requested\"` has to survive unchanged all the way into the rendered\n * line — no checkmark, no \"passed\"/\"ok\" substitute that could be misread as\n * a completed pixel pass.\n */\nexport interface PreviewHtmlChecks {\n svg: \"completed\"\n pixels: \"not-requested\" | \"completed\"\n}\n\nexport interface PreviewHtmlInput {\n /** Deck title (`ir.filename`) — shown in the `<title>` tag and the header.\n * User content — HTML-escaped wherever it is shown. */\n title: string\n slides: PreviewHtmlSlideInput[]\n /** `auditDeck(ir).findings` (`../svg/audit/deck-audit.ts`), reshaped to\n * {@link PreviewHtmlFinding} — omit or pass `[]` when the caller skipped\n * the audit (no findings to show at all, e.g. the deck has a placeholder\n * page, see {@link auditNote}) or the deck audited clean. Drives the\n * thumbnail/stage finding-count badges and the findings panel — see the\n * module doc comment for why those are rendered as static markup here\n * rather than computed by client-side JS from the embedded JSON blob. */\n findings?: PreviewHtmlFinding[]\n /** One-line notice shown in the header in place of any findings UI — the\n * plan's placeholder-skip contract: `runPreview` sets this (and passes no\n * `findings`) whenever the deck has any placeholder page, since\n * `auditDeck` itself silently skips a placeholder (nothing to audit) and\n * showing a placeholder-heavy deck as audit-clean would be misleading.\n * User content only in the sense that it is caller-supplied prose, not\n * deck content — HTML-escaped like everything else in this file\n * regardless. */\n auditNote?: string\n /** `auditDeck(...).checks` (`../svg/audit/deck-audit.ts`), reshaped to\n * {@link PreviewHtmlChecks} — omit whenever the caller skipped the audit\n * (the same condition {@link auditNote}/{@link findings} already use: a\n * deck with a placeholder page never calls `auditDeck` at all, so there\n * is nothing to report here either). Rendered as one line, independent of\n * `findings.length` — a clean 0-finding report and a report where\n * `pixels` never ran would otherwise look identical once the findings\n * panel is empty in both cases, and the whole point of surfacing `checks`\n * here is to keep that distinction visible even then. */\n checks?: PreviewHtmlChecks\n}\n\nfunction escapeHtml(s: string): string {\n return s\n .replace(/&/g, \"&amp;\")\n .replace(/</g, \"&lt;\")\n .replace(/>/g, \"&gt;\")\n .replace(/\"/g, \"&quot;\")\n .replace(/'/g, \"&#39;\")\n}\n\n/**\n * `JSON.stringify(value)`, with every `<` escaped to `<` — safe to\n * inline inside a `<script>` element's text content (this file's\n * `#pf-audit-findings` data blob): the HTML tokenizer looks only for the\n * literal byte sequence `</script` to end a script element, regardless of\n * the script's `type` or of what its content actually parses as, so a\n * finding `message` that happens to contain that substring (it embeds a\n * truncated quote of the offending slide's own text — user content) could\n * otherwise truncate the document early. `<` never appears in JSON outside a\n * string value (the syntax has no structural use for it), so this blanket\n * replace only ever touches characters that were already inside string\n * content — the standard technique for embedding untrusted JSON in a page.\n */\nfunction embedJson(value: unknown): string {\n return JSON.stringify(value).replace(/</g, \"\\\\u003c\")\n}\n\n/** `\"3\"` (a finding-count badge) or `\"\"` when the slide has no findings —\n * shared by {@link slideNode} (the moving node's own badge) and\n * {@link thumbButton} (the thumbnail's always-present one), same\n * \"one class for the node's own copy, one for the thumbnail button's\"\n * split the existing `pf-badge`/`pf-thumb-badge` pair already uses. In\n * practice a slide never carries both this badge and the \"unfilled\" one —\n * `runPreview` only ever passes non-empty `findings` when *no* slide in the\n * deck is a placeholder (the plan's skip-the-whole-overlay contract, see\n * `PreviewHtmlInput.auditNote`'s doc comment) — but nothing here assumes\n * that invariant: the two badges use different classes and corners (this\n * one top-left, \"unfilled\" top-right) specifically so a caller that did\n * pass both for one slide would still render two distinct, non-overlapping\n * marks rather than a garbled stack. */\nfunction findingBadge(count: number, className: string): string {\n if (count === 0) return \"\"\n return `<div class=\"${className}\" aria-hidden=\"true\">${count}</div>`\n}\n\n/** The one `.pf-slide` node this slide will ever have — moved between\n * `#pf-stage` and its `.pf-thumb-slot` by `<script>` at runtime, never\n * duplicated (see this module's own doc comment). Carries its own\n * \"unfilled\" badge so the badge travels with it wherever it currently is,\n * plus (independently) a finding-count badge when `findingCount > 0`. */\nfunction slideNode(slide: PreviewHtmlSlideInput, findingCount: number): string {\n const idAttr = slide.id !== undefined ? ` data-id=\"${escapeHtml(slide.id)}\"` : \"\"\n const badge = slide.placeholder ? `<div class=\"pf-badge\" aria-hidden=\"true\">unfilled</div>` : \"\"\n const fBadge = findingBadge(findingCount, \"pf-finding-badge\")\n return `<div class=\"pf-slide\" id=\"pf-slide-${slide.index}\" data-index=\"${slide.index}\"${idAttr}>${badge}${fBadge}${slide.svg}</div>`\n}\n\n/** `\"slide 3 · content · p-body · unfilled\"` — shared by the thumbnail\n * button's `title`/`aria-label` (raw pieces joined, then escaped once —\n * escaping each piece separately and joining after would be equally\n * correct, but this reads simpler and is exactly as safe). */\nfunction thumbDescription(slide: PreviewHtmlSlideInput): string {\n const parts = [`slide ${slide.index + 1}`, slide.type]\n if (slide.id !== undefined) parts.push(slide.id)\n if (slide.placeholder) parts.push(\"unfilled\")\n return escapeHtml(parts.join(\" · \"))\n}\n\n/** `\"3 · p-body\"` (or just `\"3\"` without an id) — the thumbnail's small\n * printed label, and (via {@link counterText}) the page counter's format. */\nfunction positionLabel(slide: PreviewHtmlSlideInput): string {\n const idPart = slide.id !== undefined ? ` · ${slide.id}` : \"\"\n return escapeHtml(`${slide.index + 1}${idPart}`)\n}\n\n/** `\"1 / 8 · p-cover\"` — the page counter's initial text (rendered directly\n * into the static markup so it is correct even before `<script>` runs;\n * `<script>`'s own `updateCounter()` keeps it in sync after that). */\nfunction counterText(slide: PreviewHtmlSlideInput, total: number): string {\n const idPart = slide.id !== undefined ? ` · ${slide.id}` : \"\"\n return escapeHtml(`${slide.index + 1} / ${total}${idPart}`)\n}\n\n/** One always-present thumbnail button. `slotContent` is the slide's own\n * {@link slideNode} markup when this slide starts inactive (every slide but\n * the first), or `\"\"` when it starts active (the first slide — its node\n * lives in `#pf-stage` instead, see {@link buildPreviewHtml}). Either way\n * the button itself, its label, its own \"unfilled\" badge (if the slide is a\n * placeholder), and its own finding-count badge (if `findingCount > 0`) are\n * always rendered — only the slot's content moves. */\nfunction thumbButton(slide: PreviewHtmlSlideInput, isActive: boolean, slotContent: string, findingCount: number): string {\n const description = thumbDescription(slide)\n const badge = slide.placeholder ? `<span class=\"pf-thumb-badge\" aria-hidden=\"true\">unfilled</span>` : \"\"\n const fBadge = findingBadge(findingCount, \"pf-thumb-finding-badge\")\n return (\n `<button type=\"button\" class=\"pf-thumb${isActive ? \" pf-thumb-active\" : \"\"}\" id=\"pf-thumb-${slide.index}\" ` +\n `data-index=\"${slide.index}\" title=\"${description}\" aria-label=\"${description}\">` +\n `<span class=\"pf-thumb-slot\" id=\"pf-slot-${slide.index}\">${slotContent}</span>` +\n `<span class=\"pf-thumb-label\">${positionLabel(slide)}</span>` +\n `${badge}${fBadge}</button>`\n )\n}\n\n/** One row in the audit findings panel — `data-page-index` is the finding's\n * owning slide's 0-based array index (`f.page - 1`, `PreviewHtmlFinding.page`\n * is 1-based), the same identity `<script>`'s existing `activate(i)` already\n * navigates by, so a click just calls the same function every thumbnail\n * click already does. */\nfunction findingPanelEntry(f: PreviewHtmlFinding): string {\n const idPart = f.slideId !== undefined ? ` · ${escapeHtml(f.slideId)}` : \"\"\n return (\n `<button type=\"button\" class=\"pf-finding\" data-page-index=\"${f.page - 1}\">` +\n `<span class=\"pf-finding-loc\">page ${f.page}${idPart}</span>` +\n `<span class=\"pf-finding-code\">[${escapeHtml(f.code)}]</span> ` +\n `<span class=\"pf-finding-msg\">${escapeHtml(f.message)}</span>` +\n `</button>`\n )\n}\n\nconst CSS = `\n:root{color-scheme:light}\n*{box-sizing:border-box}\nhtml,body{height:100%;margin:0}\nbody{display:flex;flex-direction:column;font-family:-apple-system,BlinkMacSystemFont,\"Segoe UI\",Helvetica,Arial,sans-serif;background:#f4f4f4;color:#1a1a1a}\nheader{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:10px 16px;background:#fff;border-bottom:1px solid #ddd;font-size:14px;flex:0 0 auto}\n#pf-title{font-weight:600;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}\n#pf-counter{font-variant-numeric:tabular-nums;color:#555;white-space:nowrap}\n#pf-audit-note{color:#b45309;font-size:12px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}\n#pf-export-btn{font:inherit;font-size:13px;padding:6px 10px;border:1px solid #2563eb;background:#2563eb;color:#fff;border-radius:4px;cursor:pointer;white-space:nowrap;flex:0 0 auto}\n#pf-export-btn:hover{background:#1d4ed8}\n#pf-stage-wrap{flex:1 1 auto;min-height:0;display:flex;align-items:center;justify-content:center;gap:16px;padding:16px}\n#pf-stage{position:relative;background:#000;box-shadow:0 2px 16px rgba(0,0,0,.15);aspect-ratio:16/9;width:min(100%,calc((100vh - 190px) * 16 / 9));max-height:100%}\n#pf-stage,.pf-thumb-slot{position:relative}\n.pf-slide{position:absolute;inset:0}\n.pf-slide svg{display:block;width:100%;height:100%}\n#pf-side{flex:0 0 260px;align-self:stretch;overflow-y:auto;background:#fff;border:1px solid #ddd;border-radius:6px;padding:12px;font-size:13px}\n#pf-side h2{margin:0 0 8px;font-size:11px;text-transform:uppercase;letter-spacing:.04em;color:#666}\n#pf-side section+section{margin-top:16px;padding-top:16px;border-top:1px solid #eee}\n#pf-audit-checks{margin:0 0 12px;font-size:12px;color:#555}\n.pf-finding{display:block;width:100%;text-align:left;background:#fff;border:1px solid #eee;border-radius:4px;padding:6px 8px;margin-bottom:6px;cursor:pointer;font:inherit}\n.pf-finding:hover{border-color:#93c5fd}\n.pf-finding-loc{display:block;font-size:11px;color:#888}\n.pf-finding-code{display:inline-block;font-size:11px;font-weight:700;color:#b91c1c}\n.pf-finding-msg{font-size:12px;color:#333}\n#pf-annotate-current-label{font-size:11px;color:#888;margin-bottom:6px}\n#pf-annotate-list{list-style:none;margin:0 0 8px;padding:0}\n.pf-annotate-item{display:flex;justify-content:space-between;gap:6px;align-items:flex-start;padding:4px 0;border-bottom:1px solid #f0f0f0;font-size:12px}\n.pf-annotate-remove{border:none;background:none;color:#999;cursor:pointer;font-size:14px;line-height:1;padding:0 2px}\n.pf-annotate-remove:hover{color:#dc2626}\n#pf-annotate-input{width:100%;box-sizing:border-box;font:inherit;font-size:12px;padding:6px;border:1px solid #ddd;border-radius:4px;resize:vertical}\n#pf-annotate-add{font:inherit;font-size:12px;margin-top:6px;width:100%;padding:6px 10px;border:1px solid #2563eb;background:#2563eb;color:#fff;border-radius:4px;cursor:pointer}\n#pf-annotate-add:hover{background:#1d4ed8}\n#pf-filmstrip{display:flex;gap:8px;padding:10px 16px;overflow-x:auto;background:#fff;border-top:1px solid #ddd;flex:0 0 auto}\n.pf-thumb{flex:0 0 auto;width:160px;padding:0;margin:0;border:2px solid transparent;background:#eee;cursor:pointer;border-radius:6px;overflow:hidden;position:relative;font:inherit;text-align:left}\n.pf-thumb:hover{border-color:#93c5fd}\n.pf-thumb-active,.pf-thumb-active:hover{border-color:#2563eb;background:#dbeafe}\n.pf-thumb-slot{display:block;width:100%;aspect-ratio:16/9;background:#ddd}\n.pf-thumb-label{display:block;font-size:11px;line-height:1.4;padding:3px 6px;color:#444;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}\n.pf-badge,.pf-thumb-badge{position:absolute;top:4px;right:4px;background:#d97706;color:#fff;font-size:10px;font-weight:700;letter-spacing:.03em;padding:2px 6px;border-radius:3px;text-transform:uppercase;z-index:2;pointer-events:none}\n.pf-finding-badge,.pf-thumb-finding-badge{position:absolute;top:4px;left:4px;background:#dc2626;color:#fff;font-size:10px;font-weight:700;padding:2px 6px;border-radius:3px;z-index:2;pointer-events:none}\n`.trim()\n\nconst JS = `\n(function () {\n var stage = document.getElementById('pf-stage')\n var counter = document.getElementById('pf-counter')\n var thumbs = Array.prototype.slice.call(document.querySelectorAll('.pf-thumb'))\n var total = thumbs.length\n if (total === 0) return\n var current = parseInt(thumbs[0].getAttribute('data-index'), 10)\n\n function slideEl(i) { return document.getElementById('pf-slide-' + i) }\n function slotEl(i) { return document.getElementById('pf-slot-' + i) }\n function thumbEl(i) { return document.getElementById('pf-thumb-' + i) }\n function thumbPos(i) {\n for (var t = 0; t < thumbs.length; t++) {\n if (parseInt(thumbs[t].getAttribute('data-index'), 10) === i) return t\n }\n return -1\n }\n\n function updateCounter(i) {\n var el = slideEl(i)\n if (!el) return\n var text = (thumbPos(i) + 1) + ' / ' + total\n var id = el.getAttribute('data-id')\n if (id) text += ' · ' + id\n counter.textContent = text\n }\n\n function activate(i) {\n if (i === current) return\n var nextSlide = slideEl(i)\n var nextThumb = thumbEl(i)\n if (!nextSlide || !nextThumb) return\n var prevSlide = slideEl(current)\n var prevSlot = slotEl(current)\n if (prevSlide && prevSlot) prevSlot.appendChild(prevSlide)\n var prevThumb = thumbEl(current)\n if (prevThumb) prevThumb.classList.remove('pf-thumb-active')\n stage.appendChild(nextSlide)\n nextThumb.classList.add('pf-thumb-active')\n current = i\n updateCounter(i)\n renderAnnotations()\n }\n\n thumbs.forEach(function (t) {\n t.addEventListener('click', function () {\n activate(parseInt(t.getAttribute('data-index'), 10))\n })\n })\n\n document.addEventListener('keydown', function (e) {\n var pos = thumbPos(current)\n if (e.key === 'ArrowRight' && pos < total - 1) {\n activate(parseInt(thumbs[pos + 1].getAttribute('data-index'), 10))\n } else if (e.key === 'ArrowLeft' && pos > 0) {\n activate(parseInt(thumbs[pos - 1].getAttribute('data-index'), 10))\n }\n })\n\n // ---- audit findings panel: click a finding, jump to its page (same\n // activate() every thumbnail click already uses) ----\n Array.prototype.slice.call(document.querySelectorAll('.pf-finding')).forEach(function (b) {\n b.addEventListener('click', function () {\n activate(parseInt(b.getAttribute('data-page-index'), 10))\n })\n })\n\n // ---- annotations: in-memory only, keyed by the slide's 0-based array\n // index (current) — never by pageId, since an object key coerces every\n // key to a string and would silently collide a numeric page-fallback\n // pageId with a same-looking slide id (or lose the numeric/string type\n // distinction the exported JSON needs); pageIdFor() below resolves the\n // real pageId fresh from the DOM only at render/export time. ----\n var annotations = {}\n var annotateList = document.getElementById('pf-annotate-list')\n var annotateInput = document.getElementById('pf-annotate-input')\n var annotateLabel = document.getElementById('pf-annotate-current-label')\n var annotateAdd = document.getElementById('pf-annotate-add')\n var exportBtn = document.getElementById('pf-export-btn')\n\n // Slide id when the active slide has one, else its 1-based page number —\n // mirrors AuditFinding.page's own \"1-based page number when there is no\n // slide id\" convention (../svg/audit/deck-audit.ts) rather than this\n // file's internal 0-based data-index, so a revision-request.json's\n // pageId lines up with what pptfast audit/validate already print.\n function pageIdFor(i) {\n var el = slideEl(i)\n var id = el ? el.getAttribute('data-id') : null\n return id !== null ? id : thumbPos(i) + 1\n }\n\n function renderAnnotations() {\n var pid = pageIdFor(current)\n annotateLabel.textContent = 'page ' + (thumbPos(current) + 1) + (typeof pid === 'string' ? ' · ' + pid : '')\n var list = annotations[current] || []\n annotateList.innerHTML = ''\n list.forEach(function (text, idx) {\n var li = document.createElement('li')\n li.className = 'pf-annotate-item'\n var span = document.createElement('span')\n span.textContent = text\n var rm = document.createElement('button')\n rm.type = 'button'\n rm.className = 'pf-annotate-remove'\n rm.setAttribute('aria-label', 'remove annotation')\n rm.textContent = '\\\\u00d7'\n rm.addEventListener('click', function () {\n list.splice(idx, 1)\n renderAnnotations()\n })\n li.appendChild(span)\n li.appendChild(rm)\n annotateList.appendChild(li)\n })\n }\n\n annotateAdd.addEventListener('click', function () {\n var text = annotateInput.value.trim()\n if (!text) return\n if (!annotations[current]) annotations[current] = []\n annotations[current].push(text)\n annotateInput.value = ''\n renderAnnotations()\n })\n\n // \"Export revision requests\": preview.html stays read-only end to end —\n // this never writes back into the deck itself, only produces a JSON file\n // of requests for an agent/human to route through pages/*.json (see\n // skills/pptfast/SKILL.md's phase-6 revision-request handling). Zero\n // network/storage — an in-memory Blob + a synthetic <a download> click,\n // the standard browser-only download pattern.\n // pptfast serve (src/cli/serve.ts) depends on this function's\n // callable-and-synchronous contract and return type — think before\n // changing.\n function buildExportBlob() {\n var requests = []\n Object.keys(annotations).forEach(function (key) {\n var i = parseInt(key, 10)\n var pid = pageIdFor(i)\n annotations[i].forEach(function (text) {\n requests.push({ pageId: pid, annotation: text, createdAt: new Date().toISOString() })\n })\n })\n var deckTitle = document.getElementById('pf-title').textContent\n var payload = { version: '1', deck: deckTitle, requests: requests }\n return new Blob([JSON.stringify(payload, null, 2)], { type: 'application/json' })\n }\n // The one sanctioned cross-module seam this file exposes (see this\n // module's own doc comment) — a plain function reference, not data, so\n // the caller always gets this build's live annotation state, never a\n // stale snapshot.\n window.__pptfastBuildExportBlob = buildExportBlob\n\n exportBtn.addEventListener('click', function () {\n var blob = buildExportBlob()\n var url = URL.createObjectURL(blob)\n var a = document.createElement('a')\n a.href = url\n a.download = 'revision-request.json'\n document.body.appendChild(a)\n a.click()\n document.body.removeChild(a)\n URL.revokeObjectURL(url)\n })\n\n renderAnnotations()\n})()\n`.trim()\n\n/** Always-present \"Annotations\" side panel — static markup, no per-build\n * data (the annotation list itself lives only in `<script>`'s in-memory\n * `annotations` state, populated and re-rendered at runtime, see `JS`\n * above's `renderAnnotations()`). Unlike the audit findings panel, this one\n * is never conditionally omitted — annotating is available regardless of\n * whether the audit ran, found anything, or was skipped for placeholders. */\nconst ANNOTATE_PANEL = `<section id=\"pf-annotate-panel\">\n<h2>Annotations</h2>\n<div id=\"pf-annotate-current-label\"></div>\n<ul id=\"pf-annotate-list\"></ul>\n<textarea id=\"pf-annotate-input\" rows=\"3\" placeholder=\"Add a note for this page…\"></textarea>\n<button type=\"button\" id=\"pf-annotate-add\">Add annotation</button>\n</section>`\n\n/**\n * Build the self-contained `preview.html` bundle. Pure — no `fs`, safe to\n * unit-test directly (`./preview-html.test.ts`). See this module's own doc\n * comment for the self-containment and single-embed-per-slide design notes.\n */\nexport function buildPreviewHtml(input: PreviewHtmlInput): string {\n const { title, slides, findings = [], auditNote, checks } = input\n const total = slides.length\n const escapedTitle = escapeHtml(title)\n\n // Group findings by the 1-based page number they belong to, so each\n // slide's badge count is a single map lookup rather than an O(findings)\n // scan per slide.\n const findingsByPage = new Map<number, PreviewHtmlFinding[]>()\n for (const f of findings) {\n const list = findingsByPage.get(f.page)\n if (list) list.push(f)\n else findingsByPage.set(f.page, [f])\n }\n const countFor = (slide: PreviewHtmlSlideInput) => findingsByPage.get(slide.index + 1)?.length ?? 0\n\n const stageSlide = total > 0 ? slideNode(slides[0]!, countFor(slides[0]!)) : \"\"\n const thumbs = slides\n .map((s, i) => thumbButton(s, i === 0, i === 0 ? \"\" : slideNode(s, countFor(s)), countFor(s)))\n .join(\"\")\n const initialCounter = total > 0 ? counterText(slides[0]!, total) : \"0 / 0\"\n\n // Findings panel + embedded JSON blob (see this module's own doc comment\n // for why the panel is static markup, not client-computed from the blob)\n // — both entirely omitted when there is nothing to show, so a clean or\n // audit-skipped deck's preview.html carries no trace of either (matches\n // the pre-existing \"never shows an 'unfilled' badge when no slide is a\n // placeholder\" precedent for the badge markup itself).\n const auditPanel =\n findings.length > 0\n ? `<section id=\"pf-audit-panel\"><h2>Audit findings (${findings.length})</h2><div id=\"pf-audit-list\">${findings.map(findingPanelEntry).join(\"\")}</div></section>`\n : \"\"\n const findingsDataScript =\n findings.length > 0\n ? `<script type=\"application/json\" id=\"pf-audit-findings\">${embedJson(findings)}</script>`\n : \"\"\n const auditNoteHtml = auditNote !== undefined ? `<span id=\"pf-audit-note\">${escapeHtml(auditNote)}</span>` : \"\"\n\n // One-line \"which check families actually ran\" summary (fix round,\n // Important-1: the task brief's own scope for this wave, missed in the\n // first pass) — independent of `auditPanel`'s `findings.length > 0` gate\n // on purpose, see `PreviewHtmlInput.checks`'s own doc comment for why.\n // Renders the literal state words straight out of `PreviewHtmlChecks`\n // (`\"completed\"` / `\"not-requested\"`) with no checkmark/tick substitute —\n // the soul constraint this whole wave is built on is \"not checked must\n // never read as passed\", and a glyph here would be exactly that misread\n // for a `pixels: \"not-requested\"` report.\n const checksLine =\n checks !== undefined\n ? `<div id=\"pf-audit-checks\">audit checks: svg ${checks.svg} · pixels ${checks.pixels}</div>`\n : \"\"\n\n return `<!doctype html>\n<html lang=\"en\">\n<head>\n<meta charset=\"utf-8\">\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n<title>${escapedTitle} — pptfast preview</title>\n<style>${CSS}</style>\n</head>\n<body>\n<header>\n<span id=\"pf-title\">${escapedTitle}</span>\n<span id=\"pf-counter\">${initialCounter}</span>\n${auditNoteHtml}\n<button type=\"button\" id=\"pf-export-btn\">Export revision requests</button>\n</header>\n<div id=\"pf-stage-wrap\"><div id=\"pf-stage\">${stageSlide}</div><aside id=\"pf-side\">${checksLine}${auditPanel}${ANNOTATE_PANEL}</aside></div>\n<nav id=\"pf-filmstrip\" aria-label=\"slides\">${thumbs}</nav>\n${findingsDataScript}\n<script>${JS}</script>\n</body>\n</html>\n`\n}\n","/**\n * `pptfast serve <target>` (serve wave, task S1, spec-plan.md\n * `.issues/2026-07-25-serve/spec-plan.md`): a live-reloading HTTP preview of\n * the exact same `preview.html` bundle `pptfast preview --html` writes to\n * disk (`buildDeckPreview`, `./commands.ts`) — this module never builds its\n * own HTML, only serves and refreshes what that shared pipeline produces\n * (design ruling 5: \"buildPreviewHtml 复用现状 ... 禁止 fork 一份 preview 构建\n * 逻辑\").\n *\n * Two layers:\n * - {@link createServeServer}: the testable factory — a plain `node:http`\n * server (design ruling 1: zero new dependencies, no express/ws/chokidar)\n * bound hard to `127.0.0.1` (design ruling 6: no remote bind, no auth — a\n * local dev tool), an `fs.watch`-based rebuild loop, and an SSE channel for\n * push (design ruling 2: v1 is a whole-page `location.reload()` over SSE,\n * no partial DOM patching). No process-level side effects (no `SIGINT`\n * handler, no browser launch) — a caller (tests, or {@link runServe} below)\n * owns that, which is what keeps this factory usable outside the CLI.\n * - {@link runServe}: the CLI-facing wrapper — prints the URL, opens a\n * browser unless `--no-open`, wires `SIGINT` to a clean shutdown.\n *\n * Routes: `GET /` returns the in-memory cached HTML — after {@link injectServeClient}\n * has spliced this module's own `<script>` into it (task S2; see that\n * function's own doc comment) — rebuilt on change, never per-request (a\n * request never blocks on a render). `GET /events` is the SSE stream: a\n * `retry:` hint on connect, a `: heartbeat` comment frame every 30s (keeps\n * the connection alive through an idle-timeout proxy — pure SSE comment\n * syntax, invisible to `EventSource`), an `event: reload` frame after every\n * successful rebuild, an `event: error` frame with a JSON `{message}` body\n * after a failed one. `POST /revision-request` (task S2) accepts the\n * injected client's submit and atomically writes it to disk — see\n * {@link atomicWriteFile}'s and `createServeServer`'s own `revisionRequestPath`\n * doc comments for the write path and body-handling detail. Everything else\n * 404s.\n *\n * Watch roots (design ruling 3) come straight from {@link buildDeckPreview}'s\n * own `resolvedTarget`/`isDir` — the exact path `loadDeckTarget`\n * (`./commands.ts`) already resolved `target` to — rather than this module\n * re-deriving the bare-name/`decksDir` resolution a second time: a deck\n * project directory watches `deck.spec.json` + `pages/` + `assets/`\n * (non-recursive `fs.watch` on each — `{recursive: true}` is macOS/Windows\n * only below Node 20 (ENOSYS on Linux otherwise), and this repo's floor is\n * Node 18, `package.json#engines` — three flat, non-nested directories cover\n * the whole deck-project layout anyway, `docs/deck-projects.md`); a bare IR\n * target watches that one file. Multiple `fs.watch` events firing for a\n * single logical save (editors that write via a temp file + rename, or\n * saving several page files in one \"save all\") are coalesced by a 200ms\n * debounce into one rebuild.\n *\n * Resilience (design ruling 3's other half): a rebuild that throws — a\n * mid-edit malformed JSON save is the common case — never crashes the server\n * or throws out of the watch handler. It's caught, turned into an `error` SSE\n * event, and the previous good `html` stays cached and keeps serving `GET /`\n * until a later rebuild succeeds. Only the *first* build (at\n * `createServeServer` call time, before the server starts listening) is\n * allowed to reject the whole call — same \"throw `PptfastError` → CLI exit 1\"\n * contract every other `run*` command already has (`./commands.ts`), since\n * there is no previous-good HTML yet to fall back to.\n */\nimport { spawn } from \"node:child_process\"\nimport { randomUUID } from \"node:crypto\"\nimport { type FSWatcher, watch } from \"node:fs\"\nimport { rename, unlink, writeFile } from \"node:fs/promises\"\nimport { createServer, type IncomingMessage, type Server, type ServerResponse } from \"node:http\"\nimport { platform as osPlatform } from \"node:os\"\nimport { dirname, join } from \"node:path\"\nimport { PptfastError } from \"../errors\"\nimport { buildDeckPreview } from \"./commands\"\nimport { ASSETS_DIRNAME, PAGES_DIRNAME, SPEC_FILENAME } from \"./deck-dir\"\n\n/** `pptfast serve`'s own default (spec-plan.md §2's worked example,\n * `pptfast serve <target> [--port 4400] [--no-open]`) — never\n * auto-incremented on conflict (design ruling 7: \"不自动递增——agent 要可\n * 预测的 URL\"), so a busy port is a hard error naming `--port` as the way\n * out, never a silent fallback to some other port the caller didn't ask\n * for. */\nexport const DEFAULT_PORT = 4400\n\nconst DEBOUNCE_MS = 200\nconst HEARTBEAT_MS = 30_000\n\n/** `<deck-dir>/revision-request.json`, or alongside the IR file for a\n * bare-IR target (design ruling 4) — see `createServeServer`'s own\n * `revisionRequestPath` derivation below. */\nconst REVISION_REQUEST_FILENAME = \"revision-request.json\"\n\n/** `POST /revision-request`'s body cap (design ruling 6: \"POST 限\n * /revision-request 单路径 + 1MB body 上限\") — 1 MiB, enforced against bytes\n * actually received while streaming the body in, never trusted off a\n * `Content-Length` header a client could misreport. Exported so a test can\n * assert the 413 boundary without duplicating the literal. */\nexport const MAX_REVISION_REQUEST_BYTES = 1024 * 1024\n\nexport interface ServeOptions {\n /** Same target shape every deck-accepting command accepts: an IR JSON\n * file, a deck project directory, or a bare name under\n * `~/.pptfast/decks` (`buildDeckPreview`/`loadDeckTarget`, `./commands.ts`). */\n target: string\n /** Default {@link DEFAULT_PORT}. `0` binds an OS-assigned ephemeral port\n * (tests only — `pptfast serve` itself always resolves a fixed port, see\n * {@link DEFAULT_PORT}'s own doc comment on why this command never\n * auto-increments). */\n port?: number\n cwd?: string\n}\n\nexport interface ServeHandle {\n server: Server\n /** Re-run the build pipeline immediately and push the result over SSE\n * (`reload` on success, `error` on failure) — never throws, same\n * catch-and-broadcast contract the `fs.watch` path uses internally.\n * Exposed so a caller (or a test) can force a synchronous rebuild without\n * waiting on the 200ms debounce. */\n rebuild: () => Promise<void>\n /** Stops watching, closes every open SSE connection, and closes the HTTP\n * server. Safe to call more than once. */\n close: () => Promise<void>\n /** `http://127.0.0.1:<port>` — the actual bound port, resolved even when\n * `options.port` was `0`. */\n url: string\n port: number\n}\n\n/** The concrete paths `createServeServer` should `fs.watch` for `target`,\n * given `buildDeckPreview`'s own `resolvedTarget`/`isDir` for it — see this\n * module's own doc comment for why these three (deck-dir mode) or this one\n * (bare-IR mode) are the whole watch surface. */\nfunction watchRoots(resolvedTarget: string, isDir: boolean): string[] {\n if (!isDir) return [resolvedTarget]\n return [join(resolvedTarget, SPEC_FILENAME), join(resolvedTarget, PAGES_DIRNAME), join(resolvedTarget, ASSETS_DIRNAME)]\n}\n\n/** Marker on the injected `<script>` element (task S2: \"serve 模式检测(注入的\n * 脚本自带标记)\", spec-plan.md §4) — lets a test (`serve.test.ts`) or later\n * tooling confirm a served page carries this module's client wiring\n * without parsing or executing it, and gives {@link injectServeClient} a\n * fixed string to check for (a defensive double-injection guard —\n * `createServeServer` only ever calls it on a fresh `buildDeckPreview`\n * result, which never already contains it, but the check costs nothing). */\nexport const SERVE_CLIENT_SCRIPT_ID = \"pptfast-serve-client\"\n\n/**\n * The serve-mode client (task S2), spliced into every served page by\n * {@link injectServeClient} — never seen by the non-serve `pptfast preview\n * --html` download path. Two jobs:\n *\n * 1. Live reload: opens `EventSource('/events')`, reloads the whole page on\n * `reload` (design ruling 2), shows a fixed top banner on `error`. The\n * server's own custom `event: error` frame and `EventSource`'s *built-in*\n * connection-failure event share the same DOM event name on this one\n * object — a real connection hiccup is a plain `Event` with no `data`\n * (EventSource auto-reconnects itself off the server's `retry:` hint,\n * nothing for this page to do); the server's frame is a `MessageEvent`\n * whose `data` is a JSON `{message}` string. Checking for `.data` first\n * tells the two apart. No explicit \"clear the banner\" path either: every\n * successful rebuild's `reload` does a full `location.reload()`, wiping\n * the banner along with the rest of the DOM — a separate hide-on-success\n * branch would be dead code a reload always beats to it.\n *\n * 2. Revision-request submit: rewires the existing export/download button\n * (`#pf-export-btn`, `buildPreviewHtml`/`./preview-html.ts`) to POST\n * instead of only downloading. The exact serialized payload comes from\n * `window.__pptfastBuildExportBlob` — a plain function reference that\n * file's own `<script>` closure assigns onto `window` specifically as\n * this module's seam (see that file's own doc comment for the full\n * rationale; design ruling 5 forbids a second copy of any part of the\n * preview-build logic, and calling back into the original closure's own\n * function is how this reuses it instead of re-deriving the\n * `{version, deck, requests}` shape here). Called through\n * `Promise.resolve(...).then(...)` rather than invoked and trusted\n * directly — cheap insurance that both a synchronous throw *and* a\n * rejected/async return from `buildExportBlob()` land in the same\n * `.catch` as a network failure, all surfaced as the same inline\n * status-line feedback, never a silent no-op. (An earlier version of\n * this file took a different approach here — briefly monkey-patching\n * `URL.createObjectURL`/`HTMLAnchorElement.prototype.click` around a\n * programmatic click on the original button, to capture the `Blob` it\n * built without a seam existing yet. Reviewed out: it only worked\n * because that handler happened to be perfectly synchronous start to\n * finish, an assumption a later change to it — one `await` — could\n * silently break with zero user-visible error, on the one feature this\n * whole command exists to make possible.) The rewired button (a\n * `cloneNode` swapped in for the original — `cloneNode` never copies\n * `addEventListener` listeners, so the original element, though detached\n * from the document, keeps `buildPreviewHtml`'s own listener intact and\n * still runnable via `.click()`) shows success/failure inline; a small\n * secondary link next to it just calls `originalBtn.click()` — the\n * untouched, real download path — so a manual copy is always still one\n * click away regardless of whether the POST succeeds.\n *\n * Exported (S3, S2 re-review's named test carry) purely so\n * `serve-client.test.ts` can execute this exact string under jsdom instead of\n * only grepping it as markup — this file has no other export consumer, isn't\n * re-exported from anywhere `pptfast --help` or the SDK's public surface ever\n * reads, and stays exactly as inert to import as before: `src/cli/serve.ts`\n * is already Node-only (AGENTS.md's layout rule), never reachable from\n * `src/index.ts`'s browser-safe closure regardless of what it exports.\n */\nexport const SERVE_CLIENT_JS = `\n(function () {\n // Each of the two jobs below is independently wrapped (own function, own\n // try/catch at its call site at the bottom) so a failure in one — an\n // EventSource construction that throws in some unusual embedding, a\n // future buildPreviewHtml markup change that drops #pf-export-btn — can\n // never take the other down with it; a single un-isolated top-level\n // throw would otherwise abort the rest of this IIFE.\n\n function setUpLiveReload() {\n var es = new EventSource('/events')\n es.addEventListener('reload', function () { location.reload() })\n\n var banner = document.createElement('div')\n banner.id = 'pptfast-serve-error-banner'\n banner.setAttribute('role', 'alert')\n banner.style.cssText =\n 'display:none;position:fixed;top:0;left:0;right:0;z-index:2147483647;' +\n 'background:#dc2626;color:#fff;font:13px/1.4 -apple-system,BlinkMacSystemFont,\"Segoe UI\",Helvetica,Arial,sans-serif;' +\n 'padding:8px 16px;text-align:center'\n document.body.appendChild(banner)\n\n function showBanner(message) {\n banner.textContent = 'pptfast serve: ' + message\n banner.style.display = 'block'\n }\n\n es.addEventListener('error', function (e) {\n if (!e || typeof e.data !== 'string') return // a real connection hiccup, not the server's own rebuild-failed frame\n var message = 'rebuild failed'\n try {\n var parsed = JSON.parse(e.data)\n if (parsed && typeof parsed.message === 'string') message = parsed.message\n } catch (err) {}\n showBanner(message)\n })\n }\n\n function setUpRevisionRequestSubmit() {\n var originalBtn = document.getElementById('pf-export-btn')\n if (!originalBtn) return\n\n var submitBtn = originalBtn.cloneNode(true)\n submitBtn.textContent = 'Submit revision request'\n originalBtn.replaceWith(submitBtn)\n\n var status = document.createElement('span')\n status.id = 'pptfast-serve-submit-status'\n status.setAttribute('aria-live', 'polite')\n status.style.cssText = 'margin-left:8px;font-size:12px'\n\n var downloadLink = document.createElement('a')\n downloadLink.id = 'pptfast-serve-download-fallback'\n downloadLink.href = '#'\n downloadLink.textContent = 'download a copy instead'\n downloadLink.style.cssText = 'margin-left:8px;font-size:12px;color:#2563eb'\n\n submitBtn.insertAdjacentElement('afterend', status)\n status.insertAdjacentElement('afterend', downloadLink)\n\n submitBtn.addEventListener('click', function () {\n if (typeof window.__pptfastBuildExportBlob !== 'function') {\n status.style.color = '#dc2626'\n status.textContent = 'failed to submit revision request (export function unavailable — try reloading the page)'\n return\n }\n status.style.color = ''\n status.textContent = 'submitting…'\n // The extra Promise.resolve().then(...) wrapper (rather than calling\n // window.__pptfastBuildExportBlob() directly and chaining off its\n // result) means a synchronous throw from that call lands in the same\n // .catch below as an async rejection or a network failure — every\n // failure mode this chain can hit surfaces as the same inline\n // status-line feedback, none of them silent.\n Promise.resolve()\n .then(function () {\n return window.__pptfastBuildExportBlob()\n })\n .then(function (blob) {\n return blob.text()\n })\n .then(function (text) {\n return fetch('/revision-request', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: text,\n })\n })\n .then(function (res) {\n if (!res.ok) throw new Error('server responded ' + res.status)\n status.style.color = '#16a34a'\n status.textContent = 'Revision request saved to the deck directory'\n })\n .catch(function (err) {\n status.style.color = '#dc2626'\n status.textContent = 'failed to submit revision request' + (err && err.message ? ' (' + err.message + ')' : '')\n })\n })\n\n downloadLink.addEventListener('click', function (e) {\n e.preventDefault()\n originalBtn.click()\n })\n }\n\n try {\n setUpLiveReload()\n } catch (e) {\n console.error('pptfast serve: failed to set up live reload', e)\n }\n try {\n setUpRevisionRequestSubmit()\n } catch (e) {\n console.error('pptfast serve: failed to set up revision-request submit', e)\n }\n})()\n`.trim()\n\n/** Wraps {@link SERVE_CLIENT_JS} in its own `<script>` tag, marked with\n * {@link SERVE_CLIENT_SCRIPT_ID}. */\nfunction buildServeClientScriptTag(): string {\n return `<script id=\"${SERVE_CLIENT_SCRIPT_ID}\">${SERVE_CLIENT_JS}</script>`\n}\n\n/**\n * Post-processing HTML injection (design ruling 5: `buildPreviewHtml`\n * (`./preview-html.ts`) has no seam of its own for extra script content,\n * and forking a second copy of its build logic is forbidden — so this\n * rewrites the *string* `buildDeckPreview` already returned instead,\n * leaving that module — and every byte it produces for the non-serve\n * `pptfast preview --html` download path — completely untouched).\n * `createServeServer` is the only caller, on every fresh\n * `buildDeckPreview` result (initial build and every rebuild alike).\n * Inserted right before the document's one `</body>`: by the time it runs,\n * every element the injected script itself touches (`#pf-export-btn`, ...)\n * already exists, the same reasoning `buildPreviewHtml` already places its\n * own `<script>` there for.\n */\nexport function injectServeClient(html: string): string {\n if (html.includes(SERVE_CLIENT_SCRIPT_ID)) return html\n return html.replace(\"</body>\", `${buildServeClientScriptTag()}\\n</body>`)\n}\n\n/**\n * tmp-file + rename (design ruling 4: \"原子写\"). A plain `writeFile` can\n * leave a half-written file visible to a concurrent reader (an agent\n * polling for `revision-request.json`) if the process dies mid-write;\n * writing to a throwaway sibling path first and `rename`-ing it into place\n * only once the write has fully landed makes the file's *appearance*\n * atomic to any other process (`rename` within the same directory/\n * filesystem is POSIX-atomic — the tmp path is always a sibling of\n * `targetPath`, guaranteeing the same directory). The random suffix\n * (`randomUUID`, not a fixed `.tmp` name) means two overlapping POSTs never\n * collide on the same tmp file, even though they'd still race on whose\n * `rename` lands last — acceptable for a single local user submitting from\n * one browser tab (design ruling 6: no auth, a local dev tool).\n *\n * A `rename` that itself fails (permissions, a full disk, ...) would\n * otherwise leave the tmp file behind forever — nothing else in this\n * module ever revisits it. On that path this now best-effort `unlink`s the\n * orphan before rethrowing the original error (the caller,\n * `handleRevisionRequestPost` below, turns that rethrow into a 500) — the\n * cleanup's own failure is swallowed (`.catch(() => {})`) since it must\n * never mask the real error, but a permissions problem that blocks\n * `rename` typically blocks `unlink` the same way, so this is best-effort,\n * not a guarantee.\n */\nasync function atomicWriteFile(targetPath: string, data: string): Promise<void> {\n const tmpPath = `${targetPath}.${randomUUID()}.tmp`\n await writeFile(tmpPath, data, \"utf8\")\n try {\n await rename(tmpPath, targetPath)\n } catch (e) {\n await unlink(tmpPath).catch(() => {})\n throw e\n }\n}\n\n/**\n * The testable factory (serve wave, task S1). Builds once up front — a\n * failure here rejects the whole call, see this module's own doc comment —\n * then starts listening and watching. Every fs/network resource this\n * function opens (the watchers, the heartbeat timer, the HTTP server) is\n * torn down by the returned {@link ServeHandle.close} and by nothing else:\n * this function has no other side effect a caller would need to separately\n * clean up, which is what makes it safe to call directly from a test without\n * going through the CLI at all.\n */\nexport async function createServeServer(options: ServeOptions): Promise<ServeHandle> {\n const cwd = options.cwd ?? process.cwd()\n const requestedPort = options.port ?? DEFAULT_PORT\n if (!Number.isInteger(requestedPort) || requestedPort < 0 || requestedPort > 65535) {\n throw new PptfastError(`invalid port ${requestedPort} — expected an integer between 0 and 65535`)\n }\n\n // First build happens before the server ever starts listening — deliberate:\n // there is no previous-good HTML to fall back to yet, so an invalid target\n // must fail this call outright (CLI exit 1, same as every other command)\n // rather than start a server with nothing to show at `GET /`.\n const initial = await buildDeckPreview(options.target, { cwd })\n let cachedHtml = injectServeClient(initial.html)\n const sseClients = new Set<ServerResponse>()\n // <deck-dir>/revision-request.json, or — bare-IR target — alongside the\n // IR file (design ruling 4). Derived once, here, off `initial`'s own\n // `resolvedTarget`/`isDir` — the exact path `loadDeckTarget`\n // (`./commands.ts`) already resolved `options.target` to, the same value\n // `watchRoots` above is built from — rather than re-deriving the\n // bare-name/`decksDir` resolution a second time. Path-traversal is a\n // non-issue by construction, not by validation: the filename is a fixed\n // literal ({@link REVISION_REQUEST_FILENAME}) and this directory never\n // changes for the server's lifetime, so nothing about *where* this writes\n // is ever influenced by a request's URL or body.\n const revisionRequestPath = initial.isDir\n ? join(initial.resolvedTarget, REVISION_REQUEST_FILENAME)\n : join(dirname(initial.resolvedTarget), REVISION_REQUEST_FILENAME)\n\n function writeToAll(chunk: string): void {\n for (const res of sseClients) {\n try {\n res.write(chunk)\n } catch {\n // A client that disconnected mid-broadcast — its own `close`/`error`\n // listener (registered where it's added to `sseClients` below)\n // removes it; one dead client must never stop the rest from hearing\n // about this rebuild.\n }\n }\n }\n\n function broadcast(event: string, data: unknown): void {\n writeToAll(`event: ${event}\\ndata: ${JSON.stringify(data)}\\n\\n`)\n }\n\n async function rebuild(): Promise<void> {\n try {\n const result = await buildDeckPreview(options.target, { cwd })\n cachedHtml = injectServeClient(result.html)\n broadcast(\"reload\", {})\n } catch (e) {\n broadcast(\"error\", { message: e instanceof Error ? e.message : String(e) })\n }\n }\n\n /**\n * `POST /revision-request` (task S2, design ruling 4). Streams the body\n * in (never buffers past {@link MAX_REVISION_REQUEST_BYTES} — rejecting\n * on actual bytes received, not a client-reported `Content-Length`),\n * requires it to parse as JSON, then {@link atomicWriteFile}s the raw\n * body — verbatim, not a re-serialization of the parsed value — to\n * `revisionRequestPath`, so the file on disk is byte-for-byte what the\n * client posted (which is, by construction, byte-for-byte what\n * `buildPreviewHtml`'s own download flow would have produced — see\n * {@link SERVE_CLIENT_JS}'s own doc comment). The caller\n * (`createServer`'s request handler below) has already confirmed\n * `req.method === \"POST\"` before calling this.\n */\n async function handleRevisionRequestPost(req: IncomingMessage, res: ServerResponse): Promise<void> {\n const chunks: Buffer[] = []\n let total = 0\n try {\n for await (const chunk of req as AsyncIterable<Buffer>) {\n total += chunk.length\n if (total > MAX_REVISION_REQUEST_BYTES) {\n res.writeHead(413, { \"Content-Type\": \"text/plain; charset=utf-8\" })\n res.end(`revision request body exceeds the ${MAX_REVISION_REQUEST_BYTES}-byte limit`)\n req.destroy()\n return\n }\n chunks.push(chunk)\n }\n } catch {\n // The client aborted mid-upload (or `req.destroy()` above already\n // ended it) — the connection is gone, nothing left to respond to.\n return\n }\n const body = Buffer.concat(chunks).toString(\"utf8\")\n try {\n JSON.parse(body)\n } catch {\n res.writeHead(400, { \"Content-Type\": \"text/plain; charset=utf-8\" })\n res.end(\"invalid JSON body\")\n return\n }\n try {\n await atomicWriteFile(revisionRequestPath, body)\n } catch (e) {\n res.writeHead(500, { \"Content-Type\": \"text/plain; charset=utf-8\" })\n res.end(`failed to write ${REVISION_REQUEST_FILENAME}: ${e instanceof Error ? e.message : String(e)}`)\n return\n }\n res.writeHead(200, { \"Content-Type\": \"application/json; charset=utf-8\" })\n res.end(JSON.stringify({ ok: true }))\n }\n\n const server = createServer((req, res) => {\n const pathname = (req.url ?? \"/\").split(\"?\")[0]\n if (req.method === \"GET\" && pathname === \"/\") {\n res.writeHead(200, { \"Content-Type\": \"text/html; charset=utf-8\" })\n res.end(cachedHtml)\n return\n }\n if (req.method === \"GET\" && pathname === \"/events\") {\n res.writeHead(200, {\n \"Content-Type\": \"text/event-stream\",\n \"Cache-Control\": \"no-cache, no-transform\",\n Connection: \"keep-alive\",\n })\n res.write(\"retry: 2000\\n\\n\")\n sseClients.add(res)\n res.on(\"close\", () => sseClients.delete(res))\n res.on(\"error\", () => sseClients.delete(res))\n return\n }\n if (pathname === \"/revision-request\") {\n if (req.method !== \"POST\") {\n res.writeHead(405, { \"Content-Type\": \"text/plain; charset=utf-8\", Allow: \"POST\" })\n res.end(\"method not allowed — only POST is accepted on /revision-request\")\n return\n }\n void handleRevisionRequestPost(req, res)\n return\n }\n res.writeHead(404, { \"Content-Type\": \"text/plain; charset=utf-8\" })\n res.end(\"not found\")\n })\n\n const heartbeat = setInterval(() => writeToAll(\": heartbeat\\n\\n\"), HEARTBEAT_MS)\n\n let debounceTimer: NodeJS.Timeout | undefined\n function scheduleRebuild(): void {\n if (debounceTimer) clearTimeout(debounceTimer)\n debounceTimer = setTimeout(() => {\n debounceTimer = undefined\n void rebuild()\n }, DEBOUNCE_MS)\n }\n\n const watchers: FSWatcher[] = []\n for (const path of watchRoots(initial.resolvedTarget, initial.isDir)) {\n try {\n watchers.push(watch(path, () => scheduleRebuild()))\n } catch (e) {\n // `pages/`/`assets/` may not exist yet for a brand-new deck project\n // (nothing filled in, no local images) — nothing to watch there until\n // it's created, not a reason to fail serve startup. Anything other\n // than \"doesn't exist yet\" (permissions, ...) is a real problem.\n // Consequence (S1 review carry): this watch-setup pass only ever runs\n // once, at `createServeServer` call time — a directory that gets\n // created *later* in the same session (e.g. the first local image\n // asset is added, materializing `assets/` mid-edit) is never picked\n // up, since nothing here re-scans for newly-appeared watch roots\n // afterward. Changes under such a directory go unnoticed until the\n // user restarts `pptfast serve`.\n if ((e as NodeJS.ErrnoException).code !== \"ENOENT\") throw e\n }\n }\n\n function teardownWatchersAndTimers(): void {\n clearInterval(heartbeat)\n if (debounceTimer) clearTimeout(debounceTimer)\n for (const w of watchers) w.close()\n }\n\n try {\n await new Promise<void>((resolveListen, rejectListen) => {\n const onError = (err: NodeJS.ErrnoException) => {\n server.removeListener(\"listening\", onListening)\n rejectListen(err)\n }\n const onListening = () => {\n server.removeListener(\"error\", onError)\n resolveListen()\n }\n server.once(\"error\", onError)\n server.once(\"listening\", onListening)\n server.listen(requestedPort, \"127.0.0.1\")\n })\n } catch (e) {\n teardownWatchersAndTimers()\n if ((e as NodeJS.ErrnoException).code === \"EADDRINUSE\") {\n throw new PptfastError(`port ${requestedPort} is already in use — pick a different one with --port`)\n }\n throw e\n }\n\n const address = server.address()\n const actualPort = typeof address === \"object\" && address !== null ? address.port : requestedPort\n\n let closed = false\n async function close(): Promise<void> {\n if (closed) return\n closed = true\n teardownWatchersAndTimers()\n for (const res of sseClients) res.end()\n sseClients.clear()\n // `res.end()` above finishes each SSE response, but the socket behind a\n // `Connection: keep-alive` response (`GET /events`'s own header) is not\n // guaranteed to be released the instant the response ends —\n // `server.close()`'s callback only fires once every socket the server\n // ever accepted has actually closed, so a lingering keep-alive socket\n // can otherwise leave it hanging indefinitely (S1 review carry).\n // `closeIdleConnections`/`closeAllConnections` (\"http: added connection\n // closing methods\", nodejs/node#42812) exist since Node 18.2.0 — this\n // repo's actual floor is 18.0.0 (package.json#engines: \">=18\"), hence\n // the `typeof` guard rather than a direct call. Calling both\n // unconditionally on every Node 18+ patch is deliberate, not redundant\n // belt-and-suspenders: Node's own `close()` briefly, accidentally\n // auto-invoked `closeIdleConnections()` internally on some Node 18.x\n // releases — a v19-only behavior mistakenly backported and later\n // reverted (nodejs/node#52336, landed 18.20.3: \"closeIdleConnections\n // should not be called while server.close in node v18. This behavior is\n // for node v19 and above\") — so whether `close()` alone is already\n // sufficient varies by exact patch and cannot be assumed; calling both\n // methods explicitly here is correct on every patch, a harmless no-op\n // wherever `close()` already handled it.\n if (typeof server.closeIdleConnections === \"function\") server.closeIdleConnections()\n if (typeof server.closeAllConnections === \"function\") server.closeAllConnections()\n await new Promise<void>((resolveClose, rejectClose) => {\n server.close((err) => (err ? rejectClose(err) : resolveClose()))\n })\n }\n\n return { server, rebuild, close, url: `http://127.0.0.1:${actualPort}`, port: actualPort }\n}\n\n/**\n * Best-effort browser launch (spec-plan.md S1: \"--no-open: 默认行为打开浏览器\n * ... 若无则用 child_process spawn open (darwin) / xdg-open (linux)\"). Nothing\n * in this repo already opens URLs (`./update.ts`'s `execFile` runs `npm`, not\n * a GUI app) — this is the one place that does. Never throws and never\n * rejects a caller's own flow: a headless box, a sandboxed CI runner, or a\n * missing `xdg-open` binary all fail silently — the URL `runServe` already\n * printed to the terminal is the fallback, so a failed launch here degrades\n * to \"the user copies the URL themselves\", not a broken `pptfast serve`.\n * Windows is out of scope (this repo's own dev-machine assumption is\n * macOS/Linux, spec-plan.md design ruling 1) — falls through to the\n * `xdg-open` branch, which simply fails to spawn (caught below) rather than\n * crashing.\n */\nexport function openBrowser(url: string): void {\n const command = osPlatform() === \"darwin\" ? \"open\" : \"xdg-open\"\n try {\n const child = spawn(command, [url], { stdio: \"ignore\", detached: true })\n child.on(\"error\", () => {})\n child.unref()\n } catch {\n // spawn() itself can throw synchronously (e.g. EMFILE) — equally non-fatal.\n }\n}\n\nexport interface RunServeOptions {\n port?: number\n /** `false` suppresses the browser launch (`--no-open`). Default `true`. */\n open?: boolean\n cwd?: string\n}\n\n/**\n * `pptfast serve <target>` (`../cli.ts`'s CLI wiring). Resolving does not\n * mean the command is finished — unlike every other `run*` (`./commands.ts`),\n * which does its one unit of work and returns, this one starts a long-lived\n * server and returns almost immediately after; the open listening socket\n * `createServeServer` set up is what keeps the CLI process alive from here\n * (the standard long-running-dev-server shape — same reason `vite dev`'s own\n * process doesn't exit right after printing its URL), not this function\n * blocking on anything.\n */\nexport async function runServe(target: string, opts: RunServeOptions = {}): Promise<void> {\n const handle = await createServeServer({ target, port: opts.port, cwd: opts.cwd })\n console.log(`pptfast serve: ${handle.url} (Ctrl+C to stop)`)\n if (opts.open !== false) openBrowser(handle.url)\n process.on(\"SIGINT\", () => {\n void handle.close().then(\n () => process.exit(0),\n () => process.exit(1),\n )\n })\n}\n","// Ported from markpress src/update.ts (same author), minus its playwright\n// post-install step — pptfast has no browser dependency to refresh.\nimport { execFile } from \"node:child_process\"\n\nexport const PACKAGE_NAME = \"@liustack/pptfast\"\n\nexport interface UpdateInfo {\n packageName: string\n currentVersion: string\n latestVersion: string | null\n updateAvailable: boolean\n checked: boolean\n error?: string\n}\n\nexport interface SelfUpdateResult extends UpdateInfo {\n updated: boolean\n}\n\nexport type CommandRunner = (command: string, args: string[]) => Promise<string>\n\nfunction normalizeVersion(version: string): string {\n const normalized = version.trim().replace(/^v/i, \"\").split(\"-\")[0]\n if (!normalized) throw new Error(`invalid version: ${version}`)\n return normalized\n}\n\nfunction parseVersion(version: string): number[] {\n return normalizeVersion(version)\n .split(\".\")\n .map((segment) => {\n const value = Number.parseInt(segment, 10)\n if (!Number.isFinite(value)) throw new Error(`invalid version segment: ${segment}`)\n return value\n })\n}\n\nexport function compareVersions(left: string, right: string): number {\n const l = parseVersion(left)\n const r = parseVersion(right)\n for (let i = 0; i < Math.max(l.length, r.length); i++) {\n const a = l[i] ?? 0\n const b = r[i] ?? 0\n if (a !== b) return a < b ? -1 : 1\n }\n return 0\n}\n\nexport const runCommand: CommandRunner = (command, args) =>\n new Promise((resolve, reject) => {\n execFile(command, args, { encoding: \"utf8\" }, (error, stdout, stderr) => {\n if (error) {\n reject(new Error(stderr.trim() || error.message))\n return\n }\n resolve(stdout.trim())\n })\n })\n\nexport interface CheckForUpdateOptions {\n currentVersion: string\n packageName?: string\n run?: CommandRunner\n}\n\n/** Never throws — an unreachable registry reports { checked: false, error }. */\nexport async function checkForUpdate({\n currentVersion,\n packageName = PACKAGE_NAME,\n run = runCommand,\n}: CheckForUpdateOptions): Promise<UpdateInfo> {\n const current = normalizeVersion(currentVersion)\n try {\n const latest = normalizeVersion(await run(\"npm\", [\"view\", packageName, \"version\"]))\n return {\n packageName,\n currentVersion: current,\n latestVersion: latest,\n updateAvailable: compareVersions(current, latest) < 0,\n checked: true,\n }\n } catch (error) {\n return {\n packageName,\n currentVersion: current,\n latestVersion: null,\n updateAvailable: false,\n checked: false,\n error: error instanceof Error ? error.message : String(error),\n }\n }\n}\n\nexport function createSelfUpdater(run: CommandRunner = runCommand) {\n return async ({\n currentVersion,\n packageName = PACKAGE_NAME,\n }: {\n currentVersion: string\n packageName?: string\n }): Promise<SelfUpdateResult> => {\n const info = await checkForUpdate({ currentVersion, packageName, run })\n if (!info.checked) throw new Error(`unable to check for updates: ${info.error ?? \"unknown error\"}`)\n if (!info.updateAvailable) return { ...info, updated: false }\n await run(\"npm\", [\"install\", \"-g\", `${packageName}@latest`])\n return { ...info, updated: true }\n }\n}\n"],"mappings":"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","names":["mkdir","writeFile","dirname","join","relative","resolve","join","resolve","resolve","join","readFile","extname","isAbsolute","join","resolve","readFile","resolve","readFile","resolve","resolve","isAbsolute","join","readFile","extname","spec","resolve","dirname","mkdir","writeFile","spec","join","relative","writeFile","dirname","join","join","writeFile","dirname","resolve"]}