@liustack/pptwise 0.22.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.
Files changed (46) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +127 -0
  3. package/README.zh-CN.md +136 -0
  4. package/cordis.patch.yml +5 -0
  5. package/dist/chunk-3ZUKISTY.js +114 -0
  6. package/dist/chunk-3ZUKISTY.js.map +1 -0
  7. package/dist/chunk-M35M4QUC.js +1167 -0
  8. package/dist/chunk-M35M4QUC.js.map +1 -0
  9. package/dist/chunk-VUOLBHD7.js +19 -0
  10. package/dist/chunk-VUOLBHD7.js.map +1 -0
  11. package/dist/chunk-WL5KWYKS.js +49762 -0
  12. package/dist/chunk-WL5KWYKS.js.map +1 -0
  13. package/dist/cli.js +4753 -0
  14. package/dist/cli.js.map +1 -0
  15. package/dist/index.d.ts +4224 -0
  16. package/dist/index.js +99 -0
  17. package/dist/index.js.map +1 -0
  18. package/dist/node.d.ts +7 -0
  19. package/dist/node.js +11 -0
  20. package/dist/node.js.map +1 -0
  21. package/dist/pixel-audit-H5K6JK3X.js +218 -0
  22. package/dist/pixel-audit-H5K6JK3X.js.map +1 -0
  23. package/dist/registry-C0GJH7ZT.d.ts +46 -0
  24. package/dsh/client.js +1398 -0
  25. package/dsh/index.js +141 -0
  26. package/dsh/preview-tool.js +1931 -0
  27. package/dsh/spawnHidden.js +109 -0
  28. package/package.json +113 -0
  29. package/skills/pptwise/SKILL.md +100 -0
  30. package/skills/pptwise/SKILL.zh-CN.md +102 -0
  31. package/skills/pptwise/references/branding.md +18 -0
  32. package/skills/pptwise/references/branding.zh-CN.md +21 -0
  33. package/skills/pptwise/references/components.md +35 -0
  34. package/skills/pptwise/references/components.zh-CN.md +40 -0
  35. package/skills/pptwise/references/density.md +17 -0
  36. package/skills/pptwise/references/density.zh-CN.md +22 -0
  37. package/skills/pptwise/references/images.md +42 -0
  38. package/skills/pptwise/references/images.zh-CN.md +47 -0
  39. package/skills/pptwise/references/layouts.md +37 -0
  40. package/skills/pptwise/references/layouts.zh-CN.md +42 -0
  41. package/skills/pptwise/references/spec.md +107 -0
  42. package/skills/pptwise/references/spec.zh-CN.md +112 -0
  43. package/skills/pptwise/references/validate.md +82 -0
  44. package/skills/pptwise/references/validate.zh-CN.md +87 -0
  45. package/skills/pptwise/scripts/run.ps1 +192 -0
  46. package/skills/pptwise/scripts/run.sh +229 -0
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/version.ts","../src/ir/legacy-v3.ts","../src/ir/migrate.ts","../src/themes/brand-extract.ts","../src/themes/brand-theme-file.ts","../src/spec/index.ts","../src/spec/assemble.ts","../src/spec/migrate.ts","../src/svg/asset-brief.ts"],"sourcesContent":["export const VERSION = \"0.22.0\"\n","import { z } from \"zod\"\nimport {\n AssetsSchema,\n BrandSchema,\n DeckBrandingSchema,\n MetaSchema,\n NarrativeProfileInputSchema,\n SlideSchema,\n ThemeSchema,\n} from \"./index\"\n\n/**\n * The frozen IR v3 top-level shape (vocabulary-v4 rename, task 1 — spec\n * §9.3: \"v3 已冻结... 顶层字段和枚举改名必须进入新的 IR 版本,不能在 v3\n * 内静默改变含义\"). `./index.ts`'s `PptxIRSchema` is v4 now — this module\n * exists only so a genuinely v3-shaped document still has somewhere to parse\n * against: `migrateIrV3ToV4`'s input type (`./migrate.ts`), and the\n * v3-hard-reject path's own tests (constructing a *valid* v3 IR to prove the\n * reject fires on version alone, not on some other schema defect).\n *\n * `validateIr` (`src/api.ts`) never calls this schema itself — an incoming\n * v3 document (`version === \"3\"`) is hard-rejected before any schema parse\n * runs at all (spec §9.3), full stop. This schema is a migration-tooling and\n * test fixture, not a second accepted input shape.\n *\n * Every field but `version` and `scenario` is byte-identical to `./index.ts`'s\n * v4 `PptxIRSchema` (spec §9.1: \"其余 IR 字段保持不变\") — reuses the exact\n * same `ThemeSchema`/`MetaSchema`/`AssetsSchema`/`BrandSchema`/`SlideSchema`\n * instances rather than redefining them, so there is no way for this frozen\n * shape to silently drift from the fields it shares with v4.\n */\nexport const PptxIRV3Schema = z\n .object({\n version: z.literal(\"3\").default(\"3\"),\n filename: z.string().default(\"presentation\"),\n // Pre-rename field name and axis vocabulary (mode/delivery/audience) —\n // frozen as of the 0.3.0 release, spec §9.3. Same open-schema/closed-\n // semantic split `NarrativeProfileInputSchema` documents: the actual\n // mode/delivery/audience enum closure happened at `resolveScenario`\n // runtime, not here, even before this rename.\n scenario: z.union([z.string(), NarrativeProfileInputSchema]).optional(),\n theme: ThemeSchema.default({ id: \"consulting\" }),\n meta: MetaSchema.default({}),\n assets: AssetsSchema.default({ images: {} }),\n brand: BrandSchema.optional(),\n // Optional so a v3 file that already carried the (then-undocumented)\n // chrome key still parses. migrateIrV3ToV4 rewrites it to branding.\n chrome: DeckBrandingSchema.optional(),\n seed: z.number().int().optional(),\n slides: z.array(SlideSchema),\n })\n .strict()\n\nexport type PptxIRV3 = z.infer<typeof PptxIRV3Schema>\n","import { PptwiseError } from \"../errors\"\nimport type { PptxIR } from \"./index\"\nimport type { PptxIRV3 } from \"./legacy-v3\"\n\n/**\n * Rewrite a raw deck object's root `chrome` key to `branding`. Dual-source\n * (both keys present) is a hard error, not a silent pick. Identity when\n * `chrome` is absent: omitted stays omitted, no default is materialized.\n * Never mutates `raw`. Non-object / null / array input is returned as-is.\n */\nexport function migrateChromeToBranding(raw: unknown): unknown {\n if (typeof raw !== \"object\" || raw === null || Array.isArray(raw)) return raw\n const obj = raw as Record<string, unknown>\n const hasChrome = Object.hasOwn(obj, \"chrome\")\n const hasBranding = Object.hasOwn(obj, \"branding\")\n if (hasChrome && hasBranding) {\n throw new PptwiseError('cannot migrate: both \"chrome\" and \"branding\" are present')\n }\n if (!hasChrome) return raw\n const next: Record<string, unknown> = { ...obj, branding: obj.chrome }\n delete next.chrome\n return next\n}\n\n/**\n * One-shot relocation of the removed `bloom` theme id onto `classroom`.\n * This is not a long-term alias. After this lands, bloom is not a\n * registered theme. Never mutates `raw`. Non-object / null / array input\n * is returned as-is. Identity when there is no bloom theme id: the same\n * `raw` reference, like chrome when chrome is absent.\n */\nexport function migrateBloomToClassroom(raw: unknown): unknown {\n if (typeof raw !== \"object\" || raw === null || Array.isArray(raw)) return raw\n const obj = raw as Record<string, unknown>\n const theme = obj.theme\n if (theme === \"bloom\") {\n return { ...obj, theme: \"classroom\" }\n }\n if (typeof theme === \"object\" && theme !== null && !Array.isArray(theme)) {\n const themeObj = theme as Record<string, unknown>\n if (themeObj.id === \"bloom\") {\n return { ...obj, theme: { ...themeObj, id: \"classroom\" } }\n }\n }\n return raw\n}\n\nfunction isPlainRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === \"object\" && value !== null && !Array.isArray(value)\n}\n\nfunction isLogoWallComponent(value: unknown): value is Record<string, unknown> {\n return isPlainRecord(value) && value.type === \"logo_wall\"\n}\n\nfunction arrayHasLogoWall(components: unknown): boolean {\n return Array.isArray(components) && components.some(isLogoWallComponent)\n}\n\n/**\n * Map one leftover `logo_wall` item onto an `image_grid` item. `asset_id`\n * copies as-is. `label` becomes `caption` when present. Every other key is\n * dropped. Non-object items pass through unchanged (mechanical, not\n * validating).\n */\nfunction rewriteLogoWallItem(item: unknown): unknown {\n if (!isPlainRecord(item)) return item\n const next: Record<string, unknown> = { asset_id: item.asset_id }\n if (Object.hasOwn(item, \"label\") && item.label !== undefined) next.caption = item.label\n return next\n}\n\n/**\n * One leftover `logo_wall` component → one `image_grid`. Extra logos past 4\n * are dropped because image_grid's own ceiling is 4. A 4-item wall maps 1:1.\n * `title` and every other leftover key are dropped. Non-array `items` stay\n * as-is (mechanical, not validating).\n */\nfunction rewriteLogoWallComponent(component: Record<string, unknown>): Record<string, unknown> {\n const items = component.items\n if (!Array.isArray(items)) {\n return { type: \"image_grid\", items }\n }\n return { type: \"image_grid\", items: items.slice(0, 4).map(rewriteLogoWallItem) }\n}\n\nfunction rewriteComponentsArray(components: unknown[]): unknown[] {\n return components.map((component) => (isLogoWallComponent(component) ? rewriteLogoWallComponent(component) : component))\n}\n\n/**\n * One-shot relocation of the removed `logo_wall` component onto\n * `image_grid`. This is not a long-term alias. After this lands, logo_wall\n * is not a registered component. Never mutates `raw`. Non-object / null /\n * array input is returned as-is. Identity when there is no `type:\n * \"logo_wall\"` component: the same `raw` reference, like bloom when bloom\n * is absent.\n *\n * Walks IR `slides[].components[]` and a page-shaped top-level\n * `components[]`. Each leftover wall becomes `{ type: \"image_grid\", items }`\n * with `asset_id` copied and `label` renamed to `caption`. Extra items past\n * 4 are dropped because image_grid's own ceiling is 4. A 4-item wall maps\n * 1:1.\n */\nexport function migrateLogoWallToImageGrid(raw: unknown): unknown {\n if (!isPlainRecord(raw)) return raw\n let next: Record<string, unknown> | undefined\n const take = (): Record<string, unknown> => {\n if (!next) next = { ...raw }\n return next\n }\n\n if (arrayHasLogoWall(raw.components)) {\n take().components = rewriteComponentsArray(raw.components as unknown[])\n }\n\n if (Array.isArray(raw.slides)) {\n let slidesChanged = false\n const slides = raw.slides.map((slide) => {\n if (!isPlainRecord(slide) || !arrayHasLogoWall(slide.components)) return slide\n slidesChanged = true\n return { ...slide, components: rewriteComponentsArray(slide.components as unknown[]) }\n })\n if (slidesChanged) take().slides = slides\n }\n\n return next ?? raw\n}\n\nfunction rewriteBannerHeadingField(obj: Record<string, unknown>, key: \"layout\" | \"focus\"): Record<string, unknown> | undefined {\n if (obj[key] !== \"banner-heading\") return undefined\n return { ...obj, [key]: \"two-column\" }\n}\n\n/**\n * One-shot relocation of the removed `banner-heading` content layout onto\n * `two-column`. This is not a long-term alias. After this lands,\n * banner-heading is not a registered layout. Never mutates `raw`.\n * Non-object / null / array input is returned as-is. Identity when there\n * is no `layout: \"banner-heading\"` (or spec `focus: \"banner-heading\"`):\n * the same `raw` reference, like logo_wall when logo_wall is absent.\n *\n * Walks IR `slides[].layout`, a spec `pages[].layout` / `pages[].focus`,\n * and a page-shaped top-level `layout` / `focus`. Each leftover pin\n * becomes `\"two-column\"` (`two-column` is always in the auto content\n * pool). Heading treatments keep the title face.\n */\nexport function migrateBannerHeadingToTwoColumn(raw: unknown): unknown {\n if (!isPlainRecord(raw)) return raw\n let next: Record<string, unknown> | undefined\n const take = (): Record<string, unknown> => {\n if (!next) next = { ...raw }\n return next\n }\n\n const topLayout = rewriteBannerHeadingField(raw, \"layout\")\n const topFocus = rewriteBannerHeadingField(topLayout ?? raw, \"focus\")\n if (topLayout || topFocus) {\n const rewritten = topFocus ?? topLayout!\n Object.assign(take(), rewritten)\n }\n\n if (Array.isArray(raw.slides)) {\n let slidesChanged = false\n const slides = raw.slides.map((slide) => {\n if (!isPlainRecord(slide)) return slide\n const rewritten = rewriteBannerHeadingField(slide, \"layout\")\n if (!rewritten) return slide\n slidesChanged = true\n return rewritten\n })\n if (slidesChanged) take().slides = slides\n }\n\n if (Array.isArray(raw.pages)) {\n let pagesChanged = false\n const pages = raw.pages.map((page) => {\n if (!isPlainRecord(page)) return page\n const layoutHit = rewriteBannerHeadingField(page, \"layout\")\n const focusHit = rewriteBannerHeadingField(layoutHit ?? page, \"focus\")\n const rewritten = focusHit ?? layoutHit\n if (!rewritten) return page\n pagesChanged = true\n return rewritten\n })\n if (pagesChanged) take().pages = pages\n }\n\n return next ?? raw\n}\n\n/**\n * `scenario.mode` → `narrative.strategy` value map (spec §9.1): only the\n * `\"narrative\"` mode value renames (the abstraction/instance collision spec\n * §1 flags), every other mode value (`pyramid`/`instructional`/`showcase`/\n * `briefing`) carries straight across unchanged.\n */\nconst STRATEGY_VALUE_MIGRATION: Readonly<Record<string, string>> = { narrative: \"storytelling\" }\n\n/**\n * `scenario.delivery` → `narrative.pacing` value map (spec §9.1): `text` →\n * `dense`, `presentation` → `spacious`. `balanced` is not listed because it\n * maps to itself — the fallback branch below handles it (and any other\n * value this map doesn't know about) as an identity mapping.\n */\nconst PACING_VALUE_MIGRATION: Readonly<Record<string, string>> = { text: \"dense\", presentation: \"spacious\" }\n\n/**\n * Map one v3 `scenario` input (`PptxIRV3Schema`'s open `string |\n * Record<string, unknown>` shape) to its v4 `narrative` equivalent, per spec\n * §9.1's field/value table:\n *\n * ```text\n * scenario → narrative\n * scenario.mode → narrative.strategy\n * scenario.mode: \"narrative\" → narrative.strategy: \"storytelling\"\n * scenario.delivery → narrative.pacing\n * scenario.delivery: \"text\" → narrative.pacing: \"dense\"\n * scenario.delivery: \"balanced\" → narrative.pacing: \"balanced\"\n * scenario.delivery: \"presentation\" → narrative.pacing: \"spacious\"\n * scenario.audience → narrative.audience\n * ```\n *\n * A preset-id string (e.g. `\"annual-review\"`) carries straight across\n * unchanged — spec §5: preset ids are not renamed, only the axes and values\n * a preset resolves to internally. An `undefined` input stays `undefined` —\n * both `resolveScenario` and `resolveNarrative` fall back to the exact same\n * `general` preset for an omitted axis input, so omitting is itself already\n * the equivalence-preserving choice (no need to materialize the default).\n *\n * Deliberately mechanical, not validating: an unrecognized key (already\n * invalid under v3 too) or an unrecognized `mode`/`delivery` value passes\n * through unchanged rather than throwing — `migrateIrV3ToV4` is a pure\n * structural mapping (spec §9.3: \"只做已声明的结构映射,不运行模型,不重写\n * 内容\"), not a second copy of `resolveScenario`'s own runtime validation.\n * Any input that was already invalid under v3's own semantics stays exactly\n * as invalid under v4's — `resolveNarrative` reports it as such the same way\n * `resolveScenario` would have.\n */\nfunction migrateNarrativeInput(\n scenario: string | Record<string, unknown> | undefined,\n): string | Record<string, unknown> | undefined {\n if (scenario === undefined || typeof scenario === \"string\") return scenario\n\n const narrative: Record<string, unknown> = {}\n for (const [key, value] of Object.entries(scenario)) {\n if (key === \"mode\") {\n narrative.strategy = typeof value === \"string\" ? (STRATEGY_VALUE_MIGRATION[value] ?? value) : value\n } else if (key === \"delivery\") {\n narrative.pacing = typeof value === \"string\" ? (PACING_VALUE_MIGRATION[value] ?? value) : value\n } else if (key === \"audience\") {\n narrative.audience = value\n } else {\n // Unknown key on an already-open record — not one of v3's own\n // documented axis keys. Carried across as-is (see this function's own\n // docstring on why this stays mechanical, not validating).\n narrative[key] = value\n }\n }\n return narrative\n}\n\n/**\n * Deterministic, pure IR v3 → v4 migration (spec §9.1). Field-for-field,\n * value-for-value per the mapping in {@link migrateNarrativeInput}'s\n * docstring — every field this function doesn't touch (`filename`, `theme`,\n * `meta`, `assets`, `brand`, `seed`, `slides`) carries across by the exact\n * same reference it came in with, unchanged (spec §9.1: \"其余 IR 字段保持不\n * 变\"; spec §10: no weight/budget/selection/render change is in scope for\n * this migration, ever).\n *\n * Exported from the SDK surface (`src/index.ts`) as the deterministic\n * migration primitive the `pptwise migrate` CLI command (task 2) wraps —\n * this function itself does no I/O and never runs a model, per spec §9.3's\n * \"只做已声明的结构映射,不运行模型,不重写内容,不重新选择 layout\".\n *\n * Takes an already-parsed `PptxIRV3` (i.e. `PptxIRV3Schema.parse(...)`'s\n * output, defaults already applied) rather than raw `unknown` JSON — schema\n * validation of the v3 input is the caller's job (the CLI parses-then-\n * migrates; `validateIr`'s own v3 path hard-rejects before ever reaching\n * this function, spec §9.3, so `validateIr` itself never calls this).\n */\nexport function migrateIrV3ToV4(v3: PptxIRV3): PptxIR {\n const narrative = migrateNarrativeInput(v3.scenario as string | Record<string, unknown> | undefined)\n const v4 = {\n version: \"4\" as const,\n filename: v3.filename,\n ...(narrative !== undefined ? { narrative } : {}),\n theme: v3.theme,\n meta: v3.meta,\n assets: v3.assets,\n ...(v3.brand !== undefined ? { brand: v3.brand } : {}),\n ...(v3.chrome !== undefined ? { chrome: v3.chrome } : {}),\n ...(v3.seed !== undefined ? { seed: v3.seed } : {}),\n slides: v3.slides,\n }\n return migrateBannerHeadingToTwoColumn(\n migrateLogoWallToImageGrid(migrateBloomToClassroom(migrateChromeToBranding(v4))),\n ) as PptxIR\n}\n","/**\n * Local brand-color/font extraction from a user's own `.thmx`/`.potx`/`.pptx`\n * OOXML theme part (brand-extract wave, roadmap §2.0.1: the free\n * adoption-friction remover — \"the output doesn't look like our company\" is\n * pptwise's #1 enterprise-adoption blocker, and every user who hits it\n * already has a company template on disk). Everything in this module runs\n * against zip bytes only (`jszip`, already a browser-safe dependency) —\n * `src/index.ts`'s dependency closure stays free of Node-only deps\n * (`AGENTS.md`'s layout rule), so this can run in a browser as readily as\n * the CLI's `pptwise brand extract` wraps it for.\n *\n * Rewritten from `.issues/notes/brand-extraction-probe.py` (the 39/39\n * feasibility reference: every macOS Office `.thmx` extracted cleanly) —\n * read that file for the reference regex shapes, not embedded here as\n * Python. This is a TS reimplementation, not a port with a Python shim.\n *\n * ── Slot → token mapping (roadmap §2.0.1's table, 裁定 2) ──────────────\n *\n * dk1/lt1 (by measured lightness) → text / bg — see below, not a fixed\n * dk1→text/lt1→bg assignment\n * lt2 → surface (falls back to bg when absent)\n * accent1 (or dk2) → primary/accent\n * accent1-6 → chartPalette (OOXML's 6 accent slots map 1:1 onto\n * pptwise's chart palette — no reshaping needed)\n * — → muted, derived (see {@link deriveMuted})\n *\n * ── bg/text assignment: by measured lightness, not slot name ────────────\n *\n * OOXML's `dk1`/`lt1` are slot *names*, not a guarantee about which one is\n * actually darker. Empirically verified against all 39 real `.thmx` files\n * shipped with a local macOS Office install (`.issues/notes/brand-extraction-probe.py`'s\n * own corpus) before writing this: every single one — including\n * \"Dark Gradient.thmx\", the one visually dark-background theme in that\n * set — declares the conventional `dk1=#000000`/`lt1=#FFFFFF` unchanged;\n * six declare a near-black-but-not-pure `dk1` (e.g. `#131313`), never a\n * *light* `dk1`. A theme's actual dark/light background comes from the\n * slide master's own background fill (`<p:bg>`, often referencing `dk2` or a\n * gradient) — a part this extractor deliberately never reads (theme part\n * only, 裁定 1) — not from which literal color sits in `dk1` vs `lt1`. So\n * this extractor makes no claim about detecting \"is this a dark theme\" at\n * all; it only guards against the (real, if rare outside this local corpus)\n * case of a producer that swapped which raw color occupies which *named*\n * slot: {@link resolveBgText} assigns `text` to whichever of `dk1`/`lt1`\n * measures darker and `bg` to whichever measures lighter, regardless of\n * which slot name it came from — always a legible dark-ink-on-light-bg (or,\n * for a genuinely inverted producer, the reverse) pairing, never a\n * name-literal assignment that could land dark-on-dark. `isDark` (below)\n * measures \"darker\" with nothing but the already-exported `contrastRatio`\n * (`../svg/ink`) against pure black/white — no new luminance formula, per\n * this wave's \"reuse existing ratio/mix utilities\" discipline.\n */\nimport JSZip from \"jszip\"\nimport { PptwiseError } from \"../errors\"\nimport { contrastRatio } from \"../svg/ink\"\nimport { mixHex } from \"../svg/components/color-mix\"\nimport type { BrandConfig } from \"@/ir\"\nimport type { StyleTokens } from \"./tokens\"\n\n/** The theme-file JSON shape written by `pptwise brand extract` / SDK\n * {@link extractBrandTheme}, and read back by `--theme-file` /\n * `brand-theme-file.ts`'s `registerBrandThemeFile`. Pure data (裁定 3): no\n * `layouts`/`motif`/`layoutTendencies` — those default to the full set,\n * same as any other `registerTheme` caller that omits them. `label` is\n * informational only (a human-readable name for the CLI/UI to show); the\n * render chain never reads it — the theme's own `id` is what everything\n * else keys off. */\nexport interface BrandThemeFile {\n id: string\n label: string\n style: StyleTokens\n brand: BrandConfig\n tags: string[]\n}\n\nexport interface ExtractBrandThemeOptions {\n /** Theme id — defaults to a slug of {@link ExtractBrandThemeOptions.label},\n * or (when that's also absent) a slug of the source theme part's own\n * `<a:clrScheme name=\"…\">`. The CLI (`pptwise brand extract`) always\n * passes one explicitly (`--id`, or a slug of the `-o` filename — 裁定\n * 4) — this default only ever fires for a bare SDK caller. */\n id?: string\n /** Human-readable label — defaults to the source theme part's own\n * `<a:clrScheme name=\"…\">`. */\n label?: string\n}\n\n/** Every OOXML color-scheme slot this parser reads. `hlink`/`folHlink` are\n * parsed (mirroring probe.py) but never mapped onto a pptwise token — no\n * hyperlink-color concept exists in `StyleColors` today. */\ntype ClrSlot = \"dk1\" | \"lt1\" | \"dk2\" | \"lt2\" | \"accent1\" | \"accent2\" | \"accent3\" | \"accent4\" | \"accent5\" | \"accent6\" | \"hlink\" | \"folHlink\"\ntype ClrSlots = Partial<Record<ClrSlot, string>>\n\nconst ACCENT_SLOTS: readonly ClrSlot[] = [\"accent1\", \"accent2\", \"accent3\", \"accent4\", \"accent5\", \"accent6\"]\n\n// Theme part location (probe.py's own comment): `.thmx` puts it at\n// `theme/theme/theme1.xml`, `.pptx`/`.potx` at `ppt/theme/theme1.xml` — this\n// regex matches either, and any numbered variant (`theme2.xml`, …). A\n// multi-variant `.thmx` also carries per-variant copies under\n// `theme/themeVariants/variant<N>/theme/theme1.xml`; those are excluded by\n// path, and among the survivors the *shortest* path wins (probe.py's own\n// `sorted(cands, key=len)[0]`) — the top-level part is always the shortest.\nconst THEME_PART_RE = /theme\\d*\\.xml$/\nconst THEME_VARIANTS_SEGMENT = \"themeVariants\"\n\nconst CLR_SCHEME_RE = /<a:clrScheme name=\"([^\"]*)\">([\\s\\S]*?)<\\/a:clrScheme>/\nconst CLR_SLOT_RE =\n /<a:(dk1|lt1|dk2|lt2|accent[1-6]|hlink|folHlink)>\\s*<a:(?:srgbClr val=\"([0-9A-Fa-f]{6})\"|sysClr[^>]*lastClr=\"([0-9A-Fa-f]{6})\")/g\nconst FONT_SCHEME_RE =\n /<a:fontScheme name=\"([^\"]*)\">[\\s\\S]*?<a:majorFont>\\s*<a:latin typeface=\"([^\"]*)\"[\\s\\S]*?<a:minorFont>\\s*<a:latin typeface=\"([^\"]*)\"/\n\ninterface ThemePart {\n path: string\n xml: string\n}\n\nasync function findThemePart(zip: JSZip): Promise<ThemePart | undefined> {\n const candidates = Object.keys(zip.files)\n .filter((name) => !zip.files[name]!.dir && THEME_PART_RE.test(name) && !name.includes(THEME_VARIANTS_SEGMENT))\n .sort((a, b) => a.length - b.length)\n const path = candidates[0]\n if (path === undefined) return undefined\n const xml = await zip.files[path]!.async(\"string\")\n return { path, xml }\n}\n\nfunction parseColors(xml: string): { schemeName: string | undefined; slots: ClrSlots } {\n const match = CLR_SCHEME_RE.exec(xml)\n if (!match) return { schemeName: undefined, slots: {} }\n const slots: ClrSlots = {}\n for (const m of match[2]!.matchAll(CLR_SLOT_RE)) {\n const slot = m[1] as ClrSlot\n const hex = m[2] ?? m[3]\n if (hex) slots[slot] = `#${hex.toUpperCase()}`\n }\n return { schemeName: match[1]?.trim() || undefined, slots }\n}\n\nfunction parseFonts(xml: string): { major: string | undefined; minor: string | undefined } {\n const m = FONT_SCHEME_RE.exec(xml)\n if (!m) return { major: undefined, minor: undefined }\n return { major: m[2]?.trim() || undefined, minor: m[3]?.trim() || undefined }\n}\n\n/** `hex` reads as closer to black than to white — see this file's own header\n * comment (\"bg/text assignment: by measured lightness, not slot name\"). No\n * new luminance math: two `contrastRatio` (`../svg/ink`) calls against pure\n * black/white, compared against each other, rather than a fresh relative-\n * luminance computation of its own. */\nfunction isDark(hex: string): boolean {\n return contrastRatio(hex, \"#FFFFFF\") > contrastRatio(hex, \"#000000\")\n}\n\n/** `text` = whichever of `dk1`/`lt1` measures darker, `bg` = whichever\n * measures lighter — see this file's own header comment for why this is\n * a measured-lightness assignment, not a `dk1`-always-means-text one. The\n * conventional case (`dk1` dark, `lt1` light — every real-world sample this\n * wave checked) and the by-name assignment agree; this only diverges for a\n * producer that put an unconventional raw color in either named slot. */\nfunction resolveBgText(dk1: string, lt1: string): { bg: string; text: string } {\n return isDark(dk1) === isDark(lt1)\n ? { bg: lt1, text: dk1 } // ambiguous (both/neither read dark) — keep the conventional dk1→text/lt1→bg mapping\n : isDark(dk1)\n ? { bg: lt1, text: dk1 }\n : { bg: dk1, text: lt1 }\n}\n\n/** How many discrete steps {@link deriveMuted} walks from the most-muted\n * (blended fully toward `bg`) candidate back toward `text` itself — same\n * 20-step (5% increment) granularity `metaInk` (`../svg/ink.ts`) already\n * uses for its own stepped ink walk, reused here rather than picked fresh. */\nconst MUTED_STEPS = 20\n/** The body-text WCAG floor (`CONTRAST_RATIO_BODY` in `../svg/ink.ts`,\n * inlined rather than imported since that constant isn't exported) — a\n * derived `muted` token must clear this against *both* `bg` and `surface`,\n * not just registerTheme's lower 3.0:1 registration floor, so it reads\n * correctly as body text on either background a theme might paint. */\nconst MUTED_TARGET_RATIO = 4.5\n\n/**\n * Derive a `muted` token from `text` by blending it toward `bg` in\n * {@link MUTED_STEPS} discrete steps (`mixHex`, `../svg/components/color-mix.ts`\n * — the same solid-hex blend `swot.tsx`/`bmc.tsx`/`waterfall.tsx` already\n * share) and measuring each candidate with the already-exported\n * `contrastRatio` (`../svg/ink.ts`) — no new color math, per this wave's own\n * discipline. Walks from the most-muted end (`t=1`, blended fully toward\n * `bg`) down to `t=0` (`text` itself, the highest-contrast endpoint) and\n * returns the *first* candidate — i.e. the most-muted one — that clears\n * {@link MUTED_TARGET_RATIO} against **both** `bg` and `surface` (the two\n * backgrounds `colors.muted` can actually render against). Falls back to\n * `text` unchanged when no step clears the floor even at `t=0` — a\n * pathological source palette (near-identical text/bg/accent tones) that\n * `registerTheme`'s own `assertContrastFloor` (`./definitions.ts`, 3.0:1) is\n * the intended backstop for, not this function's job to paper over.\n */\nexport function deriveMuted(text: string, bg: string, surface: string): string {\n for (let step = MUTED_STEPS; step >= 0; step--) {\n const t = step / MUTED_STEPS\n const candidate = mixHex(text, bg, t)\n if (contrastRatio(candidate, bg) >= MUTED_TARGET_RATIO && contrastRatio(candidate, surface) >= MUTED_TARGET_RATIO) {\n return candidate\n }\n }\n return text\n}\n\n/** Keywords (lower-cased, substring match) that mark a font name as\n * belonging to the serif family — used only to pick which of two\n * `SAFE_FONTS` fallback chains {@link buildFontStack} appends after the\n * extracted face, mirroring consulting's own `[\"Bower\", \"Georgia\", \"Source\n * Han Serif SC\", \"serif\"]` precedent (`./consulting.ts`) for a serif brand\n * font, or a sans equivalent otherwise. This is a display-quality heuristic,\n * not a safety one: `resolveFontFace` (`../svg/fonts.ts`) already falls\n * back to a Windows-safe CJK default (`Microsoft YaHei`) when nothing in\n * the stack matches `SAFE_FONTS` at all, so a missed classification here\n * degrades to \"the fallback face doesn't visually match the brand font's\n * register,\" never a broken/unmeasured export. */\nconst SERIF_HINTS = [\n \"times\",\n \"georgia\",\n \"cambria\",\n \"garamond\",\n \"palatino\",\n \"constantia\",\n \"book antiqua\",\n \"minion\",\n \"serif\",\n \"cochin\",\n \"didot\",\n \"baskerville\",\n \"sitka\",\n \"century\",\n \"goudy\",\n \"bodoni\",\n \"caslon\",\n \"perpetua\",\n \"rockwell\",\n \"playfair\",\n]\n\nfunction isSerifName(name: string): boolean {\n const n = name.toLowerCase()\n return SERIF_HINTS.some((hint) => n.includes(hint))\n}\n\n/** Extracted face at the head of the stack (when present), followed by a\n * Windows-safe fallback chain matching its apparent register — see\n * {@link SERIF_HINTS}'s own doc comment. `undefined` (no `<a:latin\n * typeface>` found at all) skips straight to the sans-serif fallback chain\n * alone — the common Office default (Calibri/Calibri Light) is itself\n * sans, so this is the safer bare default. */\nfunction buildFontStack(extracted: string | undefined): string[] {\n if (extracted === undefined) return [\"Calibri\", \"Microsoft YaHei\", \"sans-serif\"]\n return isSerifName(extracted)\n ? [extracted, \"Georgia\", \"Source Han Serif SC\", \"serif\"]\n : [extracted, \"Calibri\", \"Microsoft YaHei\", \"sans-serif\"]\n}\n\n/** Lower-cases, replaces every run of non-alphanumeric characters with a\n * single hyphen, and trims leading/trailing hyphens — a plain, dependency-free\n * slug (no new package for one string transform). Empty input (or input\n * that's entirely non-alphanumeric — a CJK-only deck name reduces to nothing\n * here) falls back to `fallback` rather than producing an empty/invalid\n * identifier. The fallback is a parameter because this function now serves\n * two callers whose empty-input answer differs: a brand theme id (`\"brand\"`,\n * the default) and a workspace deck directory name (`\"deck\"`,\n * `../cli/workspace.ts`). */\nexport function slugify(input: string, fallback = \"brand\"): string {\n const slug = input\n .toLowerCase()\n .replace(/[^a-z0-9]+/g, \"-\")\n .replace(/^-+|-+$/g, \"\")\n return slug || fallback\n}\n\n/**\n * Extract a {@link BrandThemeFile} from raw OOXML package bytes (a\n * `.thmx`/`.potx`/`.pptx` file's own bytes — this function only ever reads\n * the theme part inside, nothing else in the package). Deterministic: the\n * same bytes always produce the same output (no randomness, no wall-clock\n * read) — required for this wave's fixture tests' double-run-equality\n * assertions.\n *\n * Throws {@link PptwiseError} for two *structural* failures — data this\n * function has no reasonable default for:\n * - no theme part found in the package at all\n * - a theme part with no `dk1`/`lt1` (nothing to derive `bg`/`text` from) or\n * no accent color at all (nothing to derive `primary`/`chartPalette` from)\n *\n * Everything else degrades gracefully rather than throwing — a missing\n * `lt2` falls back to `bg` (no distinct surface tone available), a missing\n * `accent1` falls back to `dk2` then to the first available chart color, a\n * missing font falls back to a Windows-safe generic stack — so a real-world\n * theme missing a slot or two (this wave's own fixture matrix covers\n * several) still produces a usable, if plainer, theme. A palette that\n * degrades all the way into unreadable territory (near-identical\n * text/bg/muted tones) is *not* rejected here — that's `registerTheme`'s\n * own `assertContrastFloor` (`./definitions.ts`) job, at load time, with a\n * message naming the actual failing token/ratio (see `brand-theme-file.ts`).\n */\nexport async function extractBrandTheme(\n bytes: Uint8Array | ArrayBuffer,\n opts: ExtractBrandThemeOptions = {},\n): Promise<BrandThemeFile> {\n const zip = await JSZip.loadAsync(bytes)\n const part = await findThemePart(zip)\n if (!part) {\n throw new PptwiseError(\n \"no theme part found in this file — expected a .thmx/.potx/.pptx OOXML package with a ppt/theme/theme1.xml (or theme/theme/theme1.xml) part\",\n )\n }\n const { schemeName, slots } = parseColors(part.xml)\n if (!slots.dk1 || !slots.lt1) {\n throw new PptwiseError(`theme part ${part.path} is missing dk1/lt1 colors — cannot derive bg/text tokens`)\n }\n const chartPalette = ACCENT_SLOTS.map((slot) => slots[slot]).filter((c): c is string => c !== undefined)\n if (chartPalette.length === 0) {\n throw new PptwiseError(`theme part ${part.path} has no accent colors — cannot derive a chart palette or primary color`)\n }\n\n const { bg, text } = resolveBgText(slots.dk1, slots.lt1)\n const surface = slots.lt2 ?? bg\n const primary = slots.accent1 ?? slots.dk2 ?? chartPalette[0]!\n const accent = slots.accent2 ?? primary\n const muted = deriveMuted(text, bg, surface)\n\n const fonts = parseFonts(part.xml)\n const heading = buildFontStack(fonts.major)\n const body = buildFontStack(fonts.minor ?? fonts.major)\n\n const label = opts.label ?? schemeName ?? \"brand\"\n const id = opts.id ?? slugify(opts.label ?? schemeName ?? \"brand\")\n\n const style: StyleTokens = {\n id,\n colors: { bg, surface, primary, accent, text, muted, chartPalette },\n fonts: { heading, body },\n defaultBackgrounds: {\n cover: { kind: \"color\", value: bg },\n chapter: { kind: \"color\", value: bg },\n content: { kind: \"color\", value: bg },\n ending: { kind: \"color\", value: bg },\n },\n }\n\n return { id, label, style, brand: {}, tags: [] }\n}\n","/**\n * Load an on-disk {@link BrandThemeFile} (`./brand-extract.ts`'s output, or\n * a hand-authored equivalent) into the theme registry (brand-extract wave,\n * 裁定 3: \"装载走 registerTheme\"). Split from `brand-extract.ts` because this\n * module's job is untrusted-input validation + registration — a theme file\n * is user-editable JSON, not something `extractBrandTheme`'s own return\n * value needs re-validated against when it's the one producing it — while\n * that module's job is the pure OOXML→tokens derivation. Both stay\n * browser-safe (no Node-only import), same as every other file under\n * `src/themes/` (`AGENTS.md`'s dependency-closure rule): the actual file\n * read (`readFile`) happens in `src/cli/commands.ts`, which hands this\n * module already-parsed JSON.\n */\nimport { z } from \"zod\"\nimport { PptwiseError } from \"../errors\"\nimport { BrandConfigSchema } from \"@/ir\"\nimport type { BrandThemeFile } from \"./brand-extract\"\nimport { getInstalledThemeIds, registerTheme } from \"./definitions\"\nimport { CANONICAL_THEME_IDS } from \"./index\"\n\n// A theme file is untrusted (user-edited, or hand-authored from scratch) —\n// unlike `extractBrandTheme`'s own return value, it needs full structural\n// validation before `registerTheme` ever sees it, so a malformed field\n// produces a readable `PptwiseError` instead of a raw `TypeError` deep\n// inside `assertContrastFloor`. Deliberately a fresh schema rather than a\n// reuse of `ir/index.ts`'s `StyleOverrideSchema` (every field there is\n// `.optional()` — a deep-partial *override* shape — where a theme file's\n// `style` must be a *complete* `StyleTokens`, `defaultBackgrounds` included).\nconst HexToken = z.string().regex(/^#[0-9A-Fa-f]{3,8}$/, \"expected a hex color like #RRGGBB\")\n\nconst BackgroundSpecFileSchema = z.discriminatedUnion(\"kind\", [\n z.object({ kind: z.literal(\"color\"), value: HexToken }).strict(),\n z\n .object({\n kind: z.literal(\"gradient\"),\n from: HexToken,\n to: HexToken,\n direction: z.enum([\"tb\", \"lr\", \"diagonal\"]).optional(),\n })\n .strict(),\n z\n .object({\n kind: z.literal(\"asset\"),\n asset_id: z.string(),\n overlay: z.object({ color: HexToken, opacity: z.number().min(0).max(1) }).strict().optional(),\n fit: z.enum([\"cover\", \"contain\"]).optional(),\n })\n .strict(),\n])\n\nconst StyleTokensFileSchema = z\n .object({\n id: z.string().min(1),\n allowCustomBackground: z.boolean().optional(),\n colors: z\n .object({\n bg: HexToken,\n surface: HexToken,\n panel: HexToken.optional(),\n primary: HexToken,\n accent: HexToken,\n text: HexToken,\n muted: HexToken,\n border: HexToken.optional(),\n chartPalette: z.array(HexToken).min(1),\n accentPool: z.array(HexToken).min(1).optional(),\n cardStroke: HexToken.optional(),\n })\n .strict(),\n fonts: z\n .object({\n heading: z.array(z.string()).min(1),\n body: z.array(z.string()).min(1),\n mono: z.array(z.string()).min(1).optional(),\n })\n .strict(),\n shape: z\n .object({\n radius: z.number().min(0).max(32).optional(),\n gapScale: z.number().min(0.8).max(1.3).optional(),\n typeScale: z.number().min(0.5).max(2).optional(),\n })\n .strict()\n .optional(),\n defaultBackgrounds: z\n .object({\n cover: BackgroundSpecFileSchema,\n chapter: BackgroundSpecFileSchema,\n content: BackgroundSpecFileSchema,\n ending: BackgroundSpecFileSchema,\n })\n .strict(),\n })\n .strict()\n\nexport const BrandThemeFileSchema = z\n .object({\n id: z.string().min(1),\n label: z.string().optional(),\n style: StyleTokensFileSchema,\n brand: BrandConfigSchema.optional(),\n tags: z.array(z.string()).optional(),\n })\n .strict()\n\n/** Parse+validate raw JSON (already `JSON.parse`d by the caller — see\n * `src/cli/load-ir.ts`'s `loadIrFile`) as a {@link BrandThemeFile}. `source`\n * names the file in the thrown message (a path, or another caller-chosen\n * label) — mirrors `loadStyleFile`'s (`src/cli/commands.ts`) own\n * \"invalid <kind> file <path>:\\n<detail>\" shape so a theme-file error reads\n * exactly like every other config-file validation error in this CLI. */\nexport function parseBrandThemeFile(raw: unknown, source: string): BrandThemeFile {\n const r = BrandThemeFileSchema.safeParse(raw)\n if (!r.success) {\n const detail = r.error.issues.map((i) => `${i.path.join(\".\") || \"(root)\"}: ${i.message}`).join(\"\\n\")\n throw new PptwiseError(`invalid theme file ${source}:\\n${detail}`)\n }\n // BrandThemeFileSchema is a structural mirror of BrandThemeFile (every\n // field name/optionality matches `./brand-extract.ts`'s interface) — the\n // cast documents that equivalence rather than papering over a real\n // mismatch; `brand-theme-file.test.ts` round-trips a real\n // `extractBrandTheme` output through this exact parse to keep the two\n // definitions from silently drifting apart.\n return r.data as BrandThemeFile\n}\n\n/**\n * Register a parsed {@link BrandThemeFile} (`registerTheme`, `./definitions.ts`\n * — 裁定 3: this is the *only* loading path, so its contrast floor\n * automatically gates every custom theme). Two collision outcomes, per 裁定\n * 4:\n *\n * - `file.id` names a builtin (`CANONICAL_THEME_IDS`) → always a hard error,\n * regardless of whether this exact file was already loaded once — a\n * custom theme must never shadow a builtin id (determinism: which\n * `layouts`/`layoutTendencies` \"consulting\" resolves to must never depend\n * on whether some unrelated `--theme-file` ran first).\n * - `file.id` is already registered (not a builtin) → a silent no-op, not a\n * second error. `registerTheme` itself throws unconditionally on *any*\n * id collision, builtin or not — this idempotent skip exists because\n * `pptwise serve`'s rebuild loop (`src/cli/serve.ts`) calls this on every\n * file-watch rebuild for the *same* deck, and the theme-file/deck-dir\n * `theme.json` it re-reads each time resolves to the same id every time.\n * (A theme file edited *between* two rebuilds keeps whichever version\n * registered first for that process's lifetime — no live-reload for a\n * theme file's own content, only for deck content; out of this wave's\n * scope.)\n *\n * Returns `file.id` so a caller can fold it into the deck's own\n * theme-resolution precedence chain (`applyDeckConfig`, `src/cli/commands.ts`).\n */\nexport function registerBrandThemeFile(file: BrandThemeFile): string {\n if ((CANONICAL_THEME_IDS as readonly string[]).includes(file.id)) {\n throw new PptwiseError(\n `theme file id \"${file.id}\" collides with a built-in pptwise theme — pick a different id (\\`pptwise brand extract --id <id>\\`, or edit the theme file's own \"id\" field)`,\n )\n }\n if (!getInstalledThemeIds().includes(file.id)) {\n registerTheme({ id: file.id, style: file.style, brand: file.brand ?? {}, tags: file.tags ?? [] })\n }\n return file.id\n}\n","/**\n * Deck spec schema + validation (spec §5 \"plan artifact and hard gates\", W5\n * task 2 — renamed to \"Deck Spec\" per the vocabulary-v4 rename, spec §6/§8.1;\n * \"spec §N\" citations throughout this file that predate this rename still\n * point at that original W5 design doc, not this rename's own spec — left\n * as historical citations, not renumbered). That rename originally covered\n * the exported concepts and CLI vocabulary only — this module's own\n * directory stayed `src/plan` for a while after, a known stale name the\n * src domain-reorg wave's task T1c closed by moving it to `src/spec`\n * (mechanical `git mv`, every exported symbol unchanged): the path now\n * matches the vocabulary this header already used throughout.\n *\n * A deck spec is a workflow artifact, not a render prerequisite (spec §5's\n * \"escape hatch\": a bare IR v3 renders directly, `spec validate` is a\n * separate, optional gate for the spec-authoring stage of the six-phase\n * workflow). This module stays pure and Node-free — no `fs`, no CLI concerns\n * — so it can sit in `src/index.ts`'s dependency closure exactly like\n * `src/ir` and `src/narrative` already do (`AGENTS.md`'s layout rule).\n * `src/cli/commands.ts` owns the one Node-touching wrapper\n * (`runSpecValidate`: read file, call {@link validateSpec}, format the\n * result).\n *\n * Design mirrors `api.ts`'s `validateIr`/`ValidateResult` throughout\n * (structural zod pass first, then a sequential chain of hard-gate\n * categories, each short-circuiting the chain on its own failure — see\n * {@link validateSpec}'s own comment for why): same overall shape, adapted\n * for spec pages being keyed by an author-assigned `id` instead of IR's\n * positional slide index.\n */\nimport { z } from \"zod\"\nimport { PptwiseError } from \"../errors\"\nimport { BEAT_VALUES, BrandSchema, COMPONENT_TYPES, DeckBrandingSchema, MetaSchema, NarrativeProfileInputSchema } from \"../ir\"\nimport { normalizeDeckRootAliases } from \"../ir/field-aliases\"\nimport {\n normalizeNarrativeShape,\n resolveNarrative,\n STRATEGY_DEFINITIONS,\n type NarrativeProfile,\n type Pacing,\n type Strategy,\n} from \"../narrative\"\nimport { CAPACITY } from \"../svg/audit/capacity\"\nimport { LAYOUT_REGISTRY, type SlideType } from \"../svg/layouts/registry\"\nimport { getInstalledThemeIds } from \"../themes/definitions\"\n\n// ── schema ───────────────────────────────────────────────────────────────\n\n/**\n * Mirrors `SlideSchema.type` / `SlideType` (`ir/index.ts`,\n * `svg/layouts/registry.ts`) exactly. Kept as an independent literal tuple\n * here (not imported from either) — a page spec's `type` is a 4-value enum\n * on its own schema, not a re-export of IR's — but the `satisfies` clause\n * makes any future drift between the two a compile error instead of a\n * silent mismatch.\n */\nconst PAGE_TYPES = [\"cover\", \"chapter\", \"content\", \"ending\"] as const satisfies readonly SlideType[]\n\nexport type PageSpecType = (typeof PAGE_TYPES)[number]\nexport type PageBeat = (typeof BEAT_VALUES)[number]\n\n/**\n * A single page spec (spec §5, §6). `type`/`heading` are required — unlike\n * IR's own `SlideSchema` (where both default/omit for weak-model\n * friendliness), a deck spec is the authoring artifact those fields get\n * *locked* from at assemble time (W5 task 3), so leaving either implicit\n * here would defeat the point. `beat`/`focus`/`summary` stay optional per\n * spec §5's defaults chain (\"beat omitted → auto-rotates by page position —\n * focus/summary/layout/slot can all be omitted\").\n */\nexport const PageSpecSchema = z\n .object({\n id: z.string(),\n type: z.enum(PAGE_TYPES),\n heading: z.string(),\n /** One of the three beat values, or omitted entirely — an omitted\n * beat is never a hard-gate violation on its own (see\n * {@link checkBeatRotation}'s policy functions below). It gets\n * auto-alternated at assemble time (W5 task 3, not this task — still\n * unimplemented as of the P1 variety wave's task 1). Renamed\n * from `rhythm` (vocabulary-v4 rename, spec §4.3/§6/§8.1) — same\n * three values, same semantics, page-level term only, distinct from\n * the deck-level `pacing` axis. A *declared* value here is no longer\n * spec-only advisory material (P1 variety wave, task 1): `assembleDeck`\n * (`./assemble.ts`) now carries it straight into the IR's own\n * `Slide.beat` field, where it combines with a soft selection-weight\n * onto layout picking (`Math.max`, not multiplication — see\n * `SlideSchema.beat`'s own doc comment, `../ir/index.ts`, and\n * `BEAT_TENDENCY_WEIGHT`'s in `../svg/layout-selection.ts` for why) —\n * the checks below (rotation shape) and that downstream weighting\n * (which layouts a given beat favors) are two independent consumers\n * of the same declared value, not two views of one mechanism. */\n beat: z.enum(BEAT_VALUES).optional(),\n /** Optional authoring hint pointing fill/select at a preferred\n * component type or layout id — see {@link checkFocusVocabulary}. */\n focus: z.string().optional(),\n /** Free-text content anchor read by the fill step and never validated or\n * interpreted here. Assemble also surfaces it as `subheading` on\n * boundary pages, which have no page-content field for that line. On a\n * filled content page it remains a fill-only authoring hint. */\n summary: z.string().optional(),\n })\n .strict()\n\nexport type PageSpec = z.infer<typeof PageSpecSchema>\n\n/**\n * Top-level deck spec shape (spec §5, §6). `narrative`/`theme` deliberately\n * have no schema-level `.default(...)` — same reasoning as `PptxIRSchema`'s\n * own `narrative` field (`ir/index.ts`): the resolved value is never baked\n * back into the parsed shape, {@link validateSpec} (here) and, later,\n * assemble (W5 task 3) each resolve it themselves. `seed` is accepted but\n * entirely unexamined by this module — \"not validateSpec's concern\" (spec's\n * own wording): assemble generates and suggests writing one back on first\n * materialization.\n *\n * `version` stays the literal `\"1\"` (unchanged value) but now carries an\n * independent Deck Spec versioning scheme (spec §6: \"`deck.spec.json` 使用\n * 独立的 spec 版本 1。它是新工件,不继承 `deck.plan.json` 的版本号语义\") —\n * this \"1\" is the Deck Spec artifact's own first version, not a continuation\n * of the old deck-plan artifact's version counter, even though the digit is\n * the same.\n */\nexport const DeckSpecSchema = z\n .object({\n version: z.literal(\"1\").default(\"1\"),\n // Same open-schema/closed-semantic split as PptxIRSchema's `narrative`\n // field — see `NarrativeProfileInputSchema`'s doc comment in `ir/index.ts`\n // for the full rationale (reused verbatim here, not redefined, so the\n // two can't drift apart). Field renamed from `scenario` to `narrative`\n // this task (spec §8.1's `DeckPlan`→`DeckSpec` rename, task 2) — its\n // *value* was already in the new strategy/pacing vocabulary as of task 1\n // (vocabulary-v4 rename) — `resolveNarrative` below is what actually\n // enforces that.\n narrative: z.union([z.string(), NarrativeProfileInputSchema]).optional(),\n theme: z.string().optional(),\n filename: z.string().optional(),\n seed: z.number().int().optional(),\n meta: MetaSchema.default({}),\n /** Deck logo placement — reused verbatim from the IR's own `brand` field\n * (`BrandSchema`, `../ir`) so the deck spec and IR can't drift apart on\n * shape, same pattern as `meta` just above. Unlike `meta`, no\n * `.default({})`: IR's own `brand` field is a bare `.optional()` with no\n * default either (`undefined` means \"no brand\", not \"an empty brand\n * object\") — consumed by `Branding` (`src/svg/branding.tsx`) for\n * the deck's logo image and corner position. */\n brand: BrandSchema.optional(),\n /**\n * Where the brand footer and logo appear — reused verbatim from the IR's\n * own `branding` field (`DeckBrandingSchema`, `../ir`) so the spec and IR\n * cannot drift. Optional, no default: omitted stays unset and assemble\n * does not write `\"cover-only\"` into the IR. The renderer treats that\n * as `\"cover-only\"`. Omitted by default. Write `\"full\"` only when every\n * content page needs the brand footer. `\"full\"` also paints confidentiality\n * and date on cover and ending meta rows. Layout `branding: \"none\"` still\n * wins at render.\n */\n branding: DeckBrandingSchema.optional(),\n pages: z.array(PageSpecSchema),\n })\n .strict()\n\nexport type DeckSpec = z.infer<typeof DeckSpecSchema>\n\n/** JSON Schema for the deck spec — feed this to a model before it writes one (see `pptwise schema --spec`). */\nexport function specJsonSchema(): Record<string, unknown> {\n return z.toJSONSchema(DeckSpecSchema) as Record<string, unknown>\n}\n\n// ── result / issue types ────────────────────────────────────────────────\n\nexport interface SpecValidationIssue {\n path: string\n message: string\n /** The offending page's `id`, when the issue is scoped to one specific\n * page and that page's `id` could be determined — absent for deck-level\n * issues (e.g. \"pages non-empty\") and for structural issues on a page\n * whose own `id` itself failed to parse. */\n pageId?: string\n}\n\nexport interface SpecValidateResult {\n ok: boolean\n spec?: DeckSpec\n errors: SpecValidationIssue[]\n /**\n * Same shape and channel as `ValidateResult.normalized` (`../validate-core.ts`)\n * — human-readable `path: alias → canonical`-style rewrite entries for every\n * deterministic pre-parse rewrite `validateSpec` applied before parsing.\n * Sources: `normalizeDeckRootAliases` (`chrome` → `branding`) and\n * `normalizeNarrativeShape` (`../narrative`, T0b fix 2): a top-level\n * `narrative: {id: \"<preset>\"}` shape rewritten to the bare preset string.\n * Present only when at least one rewrite happened; informational, never\n * gates `ok` on its own.\n * `cli/commands.ts`'s `runSpecValidate` prints this through the same\n * `normalizedNote` helper `runValidate`/`runRender` already use for the\n * bare-IR path's own component field-alias notes.\n */\n normalized?: string[]\n}\n\nexport function formatSpecIssues(errors: SpecValidationIssue[]): string {\n return errors.map((e) => (e.pageId ? `page \"${e.pageId}\" — ${e.path}: ${e.message}` : `${e.path}: ${e.message}`)).join(\"\\n\")\n}\n\n/**\n * `\"invalid spec (N issue[s]):\\n<formatted issues>\"` — the exact\n * {@link PptwiseError} message both `runSpecValidate` (`src/cli/commands.ts`)\n * and {@link assembleDeck}'s (`./assemble.ts`) step 1 throw on a failed\n * {@link validateSpec} call. Extracted here instead of duplicated verbatim at\n * each call site so the two can't drift on wording — reuses\n * {@link formatSpecIssues} for the per-issue body.\n */\nexport function formatInvalidSpecError(errors: SpecValidationIssue[]): string {\n return `invalid spec (${errors.length} issue${errors.length === 1 ? \"\" : \"s\"}):\\n${formatSpecIssues(errors)}`\n}\n\n/**\n * Deck-spec-level theme default (spec §5's defaults chain: \"theme omitted →\n * consulting\") — the same default IR's own `theme.id` field carries\n * (`ThemeSchema` in `ir/index.ts`). Exported so a caller already holding a\n * validated {@link DeckSpec} (the CLI's OK-summary line) doesn't re-derive\n * the fallback itself.\n */\nexport function resolveSpecThemeId(spec: DeckSpec): string {\n return spec.theme ?? \"consulting\"\n}\n\n// ── hard gate: pages non-empty ──────────────────────────────────────────\n\nfunction checkPagesNonEmpty(spec: DeckSpec): SpecValidationIssue[] {\n if (spec.pages.length > 0) return []\n return [{ path: \"pages\", message: \"spec has no pages — a spec needs at least a cover page and an ending page\" }]\n}\n\n// ── hard gate: boundary types ───────────────────────────────────────────\n\n/**\n * Structural boundary gate (spec §5): the deck must open on a cover page and\n * close on an ending page, and no interior page may claim either type —\n * cover/ending are reserved for the two boundary positions. `content` and\n * `chapter` are both legal interior types (chapter divider pages are not\n * boundary types, and are excluded from the beat-rotation streak checks\n * below for that same reason). Called only when `spec.pages` is non-empty\n * (see {@link validateSpec}) — on a single-page spec `first`/`last` are the\n * same page and both checks run against it independently, so a lone page\n * that is neither cover nor ending reports both violations.\n */\nfunction checkBoundaryTypes(spec: DeckSpec): SpecValidationIssue[] {\n const { pages } = spec\n const errors: SpecValidationIssue[] = []\n const first = pages[0]!\n const last = pages[pages.length - 1]!\n if (first.type !== \"cover\") {\n errors.push({\n path: \"pages.0.type\",\n pageId: first.id,\n message: `first page must be type \"cover\" (got \"${first.type}\") — a spec must open with a cover page`,\n })\n }\n if (last.type !== \"ending\") {\n errors.push({\n path: `pages.${pages.length - 1}.type`,\n pageId: last.id,\n message: `last page must be type \"ending\" (got \"${last.type}\") — a spec must close with an ending page`,\n })\n }\n for (let i = 1; i < pages.length - 1; i++) {\n const page = pages[i]!\n if (page.type === \"cover\" || page.type === \"ending\") {\n errors.push({\n path: `pages.${i}.type`,\n pageId: page.id,\n message: `page \"${page.id}\" is type \"${page.type}\", only allowed as the first (cover) or last (ending) page — use \"content\" or \"chapter\" for interior pages`,\n })\n }\n }\n return errors\n}\n\n// ── hard gate: page id required + unique ────────────────────────────────\n\n/**\n * Path-traversal-safety check (CWE-22 defense-in-depth, W5 whole-branch\n * review finding 1) on a spec-authored page id — this id becomes `slide.id`\n * at assemble time ({@link buildSlide} in `./assemble.ts`, step 5) and, from\n * there, a `pages/<id>.json` / `assets/<id><ext>` file name if the resulting\n * IR is ever disassembled again (`runDisassemble`'s page write and\n * `writeOneAsset`, both via `assertSafeFileSegment` in `../cli/deck-dir.ts`\n * — the actual write-time gates, and the only checks that matter for\n * *every* id regardless of provenance, since a hand-authored bare IR skips\n * this module entirely). Rejecting an unsafe id here too, at spec-validation\n * time, is pure defense-in-depth: it means a spec-authored id is already\n * safe by the time it could ever reach either sink.\n *\n * This module stays Node-free (`AGENTS.md`'s layout rule, this file's own\n * top comment), so the check is duplicated as plain string logic instead of\n * importing `assertSafeFileSegment` itself (which needs `node:path`'s\n * `resolve`/`relative`) — same \"duplicate a few lines rather than pull a\n * Node-touching module into this closure\" call {@link specHeadingLength}'s\n * own doc comment makes just above. A single trailing path segment can only\n * ever escape whatever directory it is joined under if it is itself\n * absolute, contains a `/`/`\\` separator, or is exactly `\"..\"` —\n * `assertSafeFileSegment`'s own doc comment walks through why those lexical\n * checks alone are already sufficient (it additionally cross-checks via\n * `resolve`/`relative` as belt-and-suspenders, not because the lexical\n * checks alone fall short). Keep in sync with `assertSafeFileSegment` if\n * either check's rules ever change.\n */\nfunction isUnsafePageId(id: string): boolean {\n return id.includes(\"/\") || id.includes(\"\\\\\") || id === \"..\"\n}\n\n/**\n * `id` is required at the schema level (`PageSpecSchema.id: z.string()`, no\n * `.optional()`) so a missing key is already a structural error by the time\n * this runs — what remains to check here is (a) an empty/whitespace-only\n * string, which the schema's plain `z.string()` lets through, (b) a value\n * unsafe to use as a page/asset file name ({@link isUnsafePageId}, W5\n * whole-branch review finding 1), and (c) cross-page uniqueness, which no\n * per-page schema can express. Kebab-case is suggested by spec §5 but never\n * enforced (\"kebab-case suggested, not required\") — neither (a) nor (b)\n * narrows that: spaces, underscores, and uppercase all stay legal, only path\n * separators and a bare `\"..\"` are not.\n */\nfunction checkPageIds(spec: DeckSpec): SpecValidationIssue[] {\n const errors: SpecValidationIssue[] = []\n const seen = new Map<string, number[]>()\n spec.pages.forEach((page, i) => {\n if (page.id.trim() === \"\") {\n errors.push({ path: `pages.${i}.id`, message: `page ${i + 1} has an empty id — every page needs a non-empty, unique id` })\n return\n }\n if (isUnsafePageId(page.id)) {\n errors.push({\n path: `pages.${i}.id`,\n pageId: page.id,\n message: `page id \"${page.id}\" is not a safe file name — ids used as page/asset file names must not contain path separators or \"..\"`,\n })\n return\n }\n const indices = seen.get(page.id)\n if (indices) indices.push(i)\n else seen.set(page.id, [i])\n })\n for (const [id, indices] of seen) {\n if (indices.length < 2) continue\n errors.push({\n path: \"pages\",\n pageId: id,\n message: `duplicate page id \"${id}\" used by ${indices.length} pages (positions ${indices.map((i) => i + 1).join(\", \")}) — page ids must be unique within a spec`,\n })\n }\n return errors\n}\n\n// ── hard gate: heading required + length ────────────────────────────────\n\n/**\n * `CAPACITY.headingMaxChars` (`svg/audit/capacity.ts`) — the exact same 48\n * numeric source `ir-quality.ts`'s long-heading warning reads (see that\n * constant's own derivation comment there). Not re-derived here.\n */\nconst HEADING_MAX_CHARS = CAPACITY.headingMaxChars\n\n/**\n * Character count for the heading-length gate — deliberately plain\n * `.length` (CJK characters count as 1 each), matching `ir-quality.ts`'s own\n * `charLen` helper's semantics exactly (that function's doc comment: \"Count\n * characters. CJK characters count as 1 each (same as .length)\") — not\n * `measureTextUnits`'s visual-width weighting, a different unit system used\n * elsewhere in this codebase for a different purpose (bullets budgets).\n * Duplicated rather than imported: `charLen` is a one-line function and\n * importing it would pull `ir-quality.ts`'s whole module graph\n * (`layout-selection.ts`, `svg-text-layout.ts`, ...) into this Node-free\n * package for a single line of logic. Keep in sync with `ir-quality.ts`'s\n * `charLen` if that one ever changes.\n */\nfunction specHeadingLength(heading: string): number {\n return heading.length\n}\n\nfunction checkHeadings(spec: DeckSpec): SpecValidationIssue[] {\n const errors: SpecValidationIssue[] = []\n spec.pages.forEach((page, i) => {\n if (page.heading.trim() === \"\") {\n errors.push({ path: `pages.${i}.heading`, pageId: page.id, message: `page \"${page.id}\" is missing a required heading` })\n return\n }\n const length = specHeadingLength(page.heading)\n if (length > HEADING_MAX_CHARS) {\n errors.push({\n path: `pages.${i}.heading`,\n pageId: page.id,\n message: `page \"${page.id}\" heading is ${length} characters, exceeds the ${HEADING_MAX_CHARS}-character limit — tighten it into a short, assertive phrase`,\n })\n }\n })\n return errors\n}\n\n// ── hard gate: theme resolution ─────────────────────────────────────────\n\n/**\n * Installed-theme check, same shape as `validateIr`'s own (`api.ts`) —\n * `theme` stays an open string at the schema layer (like IR's `theme.id`),\n * this hard gate is where an unknown id is actually rejected.\n */\nfunction checkTheme(spec: DeckSpec): SpecValidationIssue[] {\n const themeId = resolveSpecThemeId(spec)\n const installed = getInstalledThemeIds()\n if (installed.includes(themeId)) return []\n const message =\n themeId === \"bloom\"\n ? 'theme id \"bloom\" was removed — run `pptwise migrate <input> -o <output>` to rewrite it to \"classroom\"'\n : `unknown theme \"${themeId}\" — available: ${installed.join(\", \")} (see \\`pptwise themes\\`)`\n return [{ path: \"theme\", message }]\n}\n\n// ── hard gate: focus vocabulary ─────────────────────────────────────────\n\nconst LAYOUT_IDS: readonly string[] = Object.keys(LAYOUT_REGISTRY)\n\n/**\n * Focus vocabulary gate (spec §5): `focus` is optional authoring guidance\n * pointing a later fill/select step at a preferred component or layout —\n * when present it must resolve against one of three vocabularies: the\n * resolved strategy's own tendency set (`STRATEGY_DEFINITIONS[strategy].tendencies`, W3\n * data), the full component-type vocabulary ({@link COMPONENT_TYPES}, every\n * component-type name), or the full layout-id vocabulary ({@link LAYOUT_IDS},\n * `LAYOUT_REGISTRY`'s keys).\n *\n * The strategy tendency set is currently always a subset of the other two\n * (every entry in every `StrategyDefinition.tendencies` array already resolves\n * against either component types or layout ids — see that field's own doc\n * comment in `narrative/index.ts`) — checked explicitly anyway, both because\n * the brief's wording keeps it a first-class term of the union (a future\n * tendency value from some other vocabulary would still resolve correctly\n * without touching this function) and because the strategy-specific list is the\n * one most useful to show first in the error message, ahead of the two much\n * longer global lists.\n */\nfunction checkFocusVocabulary(spec: DeckSpec, strategy: Strategy): SpecValidationIssue[] {\n const tendencies = STRATEGY_DEFINITIONS[strategy].tendencies\n const errors: SpecValidationIssue[] = []\n spec.pages.forEach((page, i) => {\n if (page.focus === undefined) return\n if (page.focus === \"logo_wall\") {\n errors.push({\n path: `pages.${i}.focus`,\n pageId: page.id,\n message:\n 'component type \"logo_wall\" was removed — run `pptwise migrate <input> -o <output>` to rewrite it to \"image_grid\"',\n })\n return\n }\n if (page.focus === \"banner-heading\") {\n errors.push({\n path: `pages.${i}.focus`,\n pageId: page.id,\n message:\n 'layout \"banner-heading\" was removed — run `pptwise migrate <input> -o <output>` to rewrite it to \"two-column\"',\n })\n return\n }\n if (tendencies.includes(page.focus) || COMPONENT_TYPES.includes(page.focus) || LAYOUT_IDS.includes(page.focus)) {\n return\n }\n errors.push({\n path: `pages.${i}.focus`,\n pageId: page.id,\n message:\n `unknown focus \"${page.focus}\" for strategy \"${strategy}\" — expected one of this strategy's tendencies ` +\n `(${tendencies.join(\", \")}), a component type (${COMPONENT_TYPES.join(\", \")}), ` +\n `or a layout id (${LAYOUT_IDS.join(\", \")})`,\n })\n })\n return errors\n}\n\n// ── hard gate: beat rotation (parameterized by strategy's beatPolicy) ──\n\ntype DeclaredBeatPage = { index: number; id: string; beat: PageBeat }\n\n/**\n * Content-type pages (cover/chapter/ending excluded, per the brief's streak\n * rule) that declared an explicit `beat` — the exact population every\n * beat-policy check below reasons over. A content page that leaves\n * `beat` unset is filtered out here too, not treated as a streak-breaker —\n * see {@link checkAlternatePolicy}'s doc comment for why that matters.\n */\nfunction declaredBeatContentPages(spec: DeckSpec): DeclaredBeatPage[] {\n const result: DeclaredBeatPage[] = []\n spec.pages.forEach((page, index) => {\n if (page.type === \"content\" && page.beat !== undefined) {\n result.push({ index, id: page.id, beat: page.beat })\n }\n })\n return result\n}\n\n/**\n * `alternate` policy (storytelling strategy): no run of 3 or more consecutive\n * content pages may declare the *same* beat. \"Consecutive\" is evaluated\n * on the declared-beat content-page subsequence\n * ({@link declaredBeatContentPages}), not on raw array adjacency —\n * cover/chapter/ending pages are excluded per the brief, and a content page\n * that leaves `beat` unset is *also* transparent to this scan (filtered\n * out, neither breaking nor extending a run) rather than treated as a\n * guaranteed streak-breaker: nothing at validate time knows what an unset\n * beat will resolve to (assemble's later auto-alternation step decides\n * that), so a run of declared \"anchor\" pages either side of one undeclared\n * page is still a real 3-in-a-row risk once that gap gets filled, and\n * treating it as already-safe would let the loudest form of the violation\n * (every visible declaration identical) through silently. A maximal run\n * reports exactly one error naming every member, not one error per\n * overlapping triple within it.\n */\nfunction checkAlternatePolicy(spec: DeckSpec, strategy: Strategy): SpecValidationIssue[] {\n const seq = declaredBeatContentPages(spec)\n const errors: SpecValidationIssue[] = []\n let i = 0\n while (i < seq.length) {\n let j = i + 1\n while (j < seq.length && seq[j]!.beat === seq[i]!.beat) j++\n const runLength = j - i\n if (runLength >= 3) {\n const members = seq.slice(i, j)\n errors.push({\n path: \"pages\",\n pageId: members[0]!.id,\n message:\n `${runLength} consecutive content pages declare beat \"${seq[i]!.beat}\" ` +\n `(${members.map((m) => m.id).join(\", \")}) — strategy \"${strategy}\" requires beat to alternate, ` +\n `vary at least one of them`,\n })\n }\n i = j\n }\n return errors\n}\n\n/**\n * `anchor-open` policy (pyramid strategy): only the deck's *first* content page\n * is checked — it must declare beat \"anchor\" if it declares a beat at\n * all. An unset beat on that first content page is not a violation (spec:\n * omission always defers to the later auto-fill step). Every other content\n * page's beat is left alone by this policy, by design (spec's own words:\n * \"only checks the opening\"). Vacuously fine when the spec has no content\n * pages at all (e.g. cover → chapter → ending).\n */\nfunction checkAnchorOpenPolicy(spec: DeckSpec, strategy: Strategy): SpecValidationIssue[] {\n const firstContentIndex = spec.pages.findIndex((page) => page.type === \"content\")\n if (firstContentIndex === -1) return []\n const firstContent = spec.pages[firstContentIndex]!\n if (firstContent.beat === undefined || firstContent.beat === \"anchor\") return []\n return [\n {\n path: `pages.${firstContentIndex}.beat`,\n pageId: firstContent.id,\n message: `first content page declares beat \"${firstContent.beat}\" — strategy \"${strategy}\" requires the deck to open its first content page on \"anchor\" beat when a beat is declared`,\n },\n ]\n}\n\n/**\n * `anchor-sparse` policy (showcase strategy): among content pages that declare a\n * beat, \"anchor\" must stay a minority (at most half). Showcase's own\n * beat *default* leans anchor-heavy (spec §5's beat-default column:\n * \"anchor-dominant\" — applied by the later auto-alternation step when beat is\n * omitted), but this gate only ever looks at pages the author explicitly\n * marked — its job is guarding against an agent mechanically stamping\n * \"anchor\" on every page it writes. An anchor page is meant to read as a\n * deliberate, occasional high-impact beat. If every page claims that beat,\n * none of them keep it. Zero declared-beat content pages is not a\n * violation — there is nothing to compute a ratio over (same \"absence never\n * violates\" posture as every other policy here).\n */\nfunction checkAnchorSparsePolicy(spec: DeckSpec, strategy: Strategy): SpecValidationIssue[] {\n const declared = declaredBeatContentPages(spec)\n if (declared.length === 0) return []\n const anchorPages = declared.filter((page) => page.beat === \"anchor\")\n if (anchorPages.length / declared.length <= 0.5) return []\n const pct = Math.round((anchorPages.length / declared.length) * 100)\n return [\n {\n path: \"pages\",\n // First offending anchor page, same \"representative pageId\" shape\n // checkAlternatePolicy's own issue carries (members[0]!.id there) —\n // this gate's violation is deck-wide (a ratio, not one page), but a\n // representative id still gives a CLI/agent caller something to jump\n // to rather than only a bare \"pages\" path.\n pageId: anchorPages[0]!.id,\n message:\n `${anchorPages.length} of ${declared.length} content pages with a declared beat are \"anchor\" ` +\n `(${pct}%: ${anchorPages.map((page) => page.id).join(\", \")}) — strategy \"${strategy}\" requires \"anchor\" to ` +\n `stay a minority of declared beats, vary some to \"dense\" or \"breathing\"`,\n },\n ]\n}\n\n/**\n * Dispatches to the resolved strategy's beat-rotation rule (spec §5's spec\n * hard-gate section, \"beat-rotation rule parameterized by strategy\" — a single universal \"no 3\n * same-beat pages in a row\" rule would reject e.g. briefing's own correct\n * default, the exact self-contradiction the spec's codex-review pass\n * flagged, hence a per-`beatPolicy` rule set instead of one rule for\n * everyone). See `StrategyDefinition.beatPolicy`'s own doc comment\n * (`narrative/index.ts`) for which of the five strategies maps to which policy.\n */\nfunction checkBeatRotation(spec: DeckSpec, strategy: Strategy): SpecValidationIssue[] {\n const policy = STRATEGY_DEFINITIONS[strategy].beatPolicy\n switch (policy) {\n case \"uniform-dense\":\n case \"repetition-ok\":\n // Exempt entirely — uniform/repeated beat across content pages is\n // these strategies' own correct default (briefing's \"uniform dense\",\n // instructional's \"dense tolerated, structure repeats across pages\"), not a violation of\n // anything a generic streak rule would otherwise flag.\n return []\n case \"alternate\":\n return checkAlternatePolicy(spec, strategy)\n case \"anchor-open\":\n return checkAnchorOpenPolicy(spec, strategy)\n case \"anchor-sparse\":\n return checkAnchorSparsePolicy(spec, strategy)\n default: {\n const exhaustive: never = policy\n throw new Error(`unhandled beat policy: ${String(exhaustive)}`)\n }\n }\n}\n\n// ── hard gate: page count vs pacing ─────────────────────────────────────\n\n/**\n * Deck-level page-count range per pacing (spec §5's pacing table,\n * initial values — \"dense 8-30 / balanced 6-24 / spacious 4-16\", not yet\n * tuned against real usage). Independent of `PACING_BUDGETS`\n * (`narrative/index.ts`, per-slide component-count/bullets editorial\n * budget) — this is a separate, deck-wide page-count concern the spec calls\n * out as its own hard gate (\"page count vs. pacing recommended range\").\n * Message wording renamed from \"delivery\" to \"pacing\" (vocabulary-v4\n * residual, routed from the task 1 review) — the axis itself was already\n * `Pacing` at the type level, this closes the last stale word in the\n * error text.\n */\nexport const SPEC_PAGE_COUNT_RANGE: Record<Pacing, { min: number; max: number }> = {\n dense: { min: 8, max: 30 },\n balanced: { min: 6, max: 24 },\n spacious: { min: 4, max: 16 },\n}\n\nfunction checkPageCount(spec: DeckSpec, pacing: Pacing): SpecValidationIssue[] {\n const { min, max } = SPEC_PAGE_COUNT_RANGE[pacing]\n const n = spec.pages.length\n if (n >= min && n <= max) return []\n return [\n {\n path: \"pages\",\n message: `spec has ${n} pages — \"${pacing}\" pacing expects ${min}-${max} pages, change pacing or add/remove pages`,\n },\n ]\n}\n\n// ── entry point ──────────────────────────────────────────────────────────\n\n/** Best-effort page id lookup straight off the *raw* (pre-parse) input, used\n * only for structural (zod) issues — the page that failed to parse may\n * still have a readable `id` sitting right next to whatever field failed. */\nfunction pageIdFromRawInput(input: unknown, index: number): string | undefined {\n if (typeof input !== \"object\" || input === null) return undefined\n const pages = (input as Record<string, unknown>).pages\n if (!Array.isArray(pages)) return undefined\n const page = pages[index] as unknown\n if (typeof page !== \"object\" || page === null) return undefined\n const id = (page as Record<string, unknown>).id\n return typeof id === \"string\" ? id : undefined\n}\n\n/**\n * Validate raw JSON against the spec schema, then — once it parses — run the\n * spec §5 hard-gate chain. Mirrors `validateIr`'s (`api.ts`) overall shape: a\n * structural zod pass first, then a sequence of isolated hard-gate\n * categories, each short-circuiting the whole chain on its own failure\n * (rather than accumulating errors across categories) so a later category\n * never has to guess at what an earlier, already-broken one would have\n * meant — e.g. beat-rotation and page-count both need a resolved\n * narrative, so nothing past the narrative/theme stage runs until that\n * resolves cleanly. Every spec-gate philosophy here is \"hard block, no soft\n * warning\" (spec §5's \"escape hatch\" section — a spec that doesn't fit this shape\n * should be authored as bare IR instead, not warned-and-shipped).\n *\n * Before the schema parse, {@link normalizeNarrativeShape} (`../narrative`,\n * T0b fix 2 scope extension) runs on the raw input, exactly mirroring\n * `validateIr`'s own pre-parse pass (`../validate-core.ts`) — a top-level\n * `narrative: {id: \"<preset>\"}` shape is rewritten to the bare preset\n * string before `DeckSpecSchema.safeParse` ever sees it, so the correction\n * lands in the returned `spec` itself (read again by `checkBeatRotation`/\n * `checkFocusVocabulary`/`checkPageCount` below, and by `runSpecValidate`'s\n * own OK-summary line, `../cli/commands.ts`), not just this function's own\n * local `resolveNarrative` call below. Every return path is wrapped in\n * `withNormalized` so the rewrite note (`SpecValidateResult.normalized`)\n * surfaces on success *or* failure, success or failure alike — same\n * \"informational, never gates `ok`\" contract `ValidateResult.normalized`\n * has.\n */\nexport function validateSpec(input: unknown): SpecValidateResult {\n const rootAliasPass = normalizeDeckRootAliases(input)\n const narrativeShapePass = normalizeNarrativeShape(rootAliasPass.value)\n const normalizedInput = narrativeShapePass.value\n const normalized = [...rootAliasPass.normalized, ...narrativeShapePass.normalized]\n const withNormalized = (result: SpecValidateResult): SpecValidateResult =>\n normalized.length > 0 ? { ...result, normalized } : result\n\n const r = DeckSpecSchema.safeParse(normalizedInput)\n if (!r.success) {\n const errors = r.error.issues.map((issue) => {\n const path = issue.path.join(\".\")\n const m = /^pages\\.(\\d+)/.exec(path)\n return { path, message: issue.message, pageId: m ? pageIdFromRawInput(normalizedInput, Number(m[1])) : undefined }\n })\n return withNormalized({ ok: false, errors })\n }\n const spec = r.data\n\n const emptyErrors = checkPagesNonEmpty(spec)\n if (emptyErrors.length > 0) return withNormalized({ ok: false, errors: emptyErrors })\n\n const boundaryErrors = checkBoundaryTypes(spec)\n if (boundaryErrors.length > 0) return withNormalized({ ok: false, errors: boundaryErrors })\n\n const idErrors = checkPageIds(spec)\n if (idErrors.length > 0) return withNormalized({ ok: false, errors: idErrors })\n\n const headingErrors = checkHeadings(spec)\n if (headingErrors.length > 0) return withNormalized({ ok: false, errors: headingErrors })\n\n const themeErrors = checkTheme(spec)\n if (themeErrors.length > 0) return withNormalized({ ok: false, errors: themeErrors })\n\n // Narrative resolution (spec §5's defaults chain), same open-schema/\n // closed-semantic split as validateIr's own (api.ts) — see that\n // function's comment for the full rationale. `spec.narrative`'s inferred\n // type is wider than `resolveNarrative`'s parameter — safe to narrow here\n // because `resolveNarrative` validates every key/value itself at runtime.\n // When the input was an `{id}` shape, `spec.narrative` here already reads\n // as the rescued bare string (`normalizeNarrativeShape` rewrote it before\n // the schema parse above) — this call just resolves that string like any\n // other preset id. `resolveNarrative`'s own entry additionally tolerates\n // the unrescued `{id}` shape directly too (see its doc comment), so this\n // line stays correct even if some future caller of `resolveNarrative`\n // reaches it without going through a normalizeNarrativeShape pass first.\n let resolvedAxes: NarrativeProfile\n try {\n resolvedAxes = resolveNarrative(spec.narrative as string | Partial<NarrativeProfile> | undefined)\n } catch (err) {\n if (!(err instanceof PptwiseError)) throw err\n return withNormalized({ ok: false, errors: [{ path: \"narrative\", message: err.message }] })\n }\n\n const beatErrors = checkBeatRotation(spec, resolvedAxes.strategy)\n if (beatErrors.length > 0) return withNormalized({ ok: false, errors: beatErrors })\n\n const focusErrors = checkFocusVocabulary(spec, resolvedAxes.strategy)\n if (focusErrors.length > 0) return withNormalized({ ok: false, errors: focusErrors })\n\n const pageCountErrors = checkPageCount(spec, resolvedAxes.pacing)\n if (pageCountErrors.length > 0) return withNormalized({ ok: false, errors: pageCountErrors })\n\n return withNormalized({ ok: true, spec, errors: [] })\n}\n","/**\n * assembleDeck / disassembleDeck — deck spec + per-page content → IR, and\n * back (spec §5's \"assemble is a pure SDK function\", W5 task 3; the spec\n * artifact itself renamed from \"plan\" to \"spec\" per the vocabulary-v4\n * rename, spec §6/§8.1 — old \"spec §N\" citations throughout this file\n * predate that rename and still cite the original W5 design doc, left as\n * historical citations, not renumbered).\n *\n * `assembleDeck` is the pure-function half of the deck-project directory\n * concept (spec §7): a locked {@link DeckSpec} (§5's workflow artifact,\n * validated by `validateSpec` in `./index.ts`) plus a `pages` record keyed\n * by page id, materialized into a renderable {@link PptxIR}. The CLI's\n * directory wrapper (W5 task 5, not this file) is the only Node-touching\n * piece — it reads `deck.spec.json` + `pages/<id>.json` off disk and calls\n * straight through to {@link assembleDeck}, exactly like `src/cli/commands.ts`\n * already wraps `validateIr`/`validateSpec`. This module itself stays zero-fs\n * (no `node:*` imports, nothing from `src/cli*` or `src/platform/node.ts`) so\n * it can sit in `src/index.ts`'s dependency closure (`AGENTS.md`'s layout\n * rule) — the exact same posture `./index.ts` (this folder's schema/validate\n * module) already holds, documented in that file's own top comment.\n *\n * `assembleDeck` also materializes each page's effective layout id into its\n * own `layout` field when the page file omitted one (W4 design decision 10):\n * once the IR above is built and schema-validated, every slide whose\n * `layout` is still unset gets exactly what `resolveEffectiveLayoutId`\n * (`../svg/layout-selection.ts`) resolves for it — the same function the\n * render chain itself calls, so `deck.json` never carries a second,\n * independently-derived guess at what a page will render as (see this\n * module's own {@link materializeEffectiveLayouts} for the mechanics). A page\n * whose file already set `layout` is left untouched. `../svg/layout-selection.ts`\n * is a pure function with no Node-only import anywhere in its own closure and\n * nothing in that closure imports back from `src/spec`, so depending on it\n * from here adds a new edge, not a cycle, and does not pull anything\n * Node-only into `src/index.ts`'s dependency closure.\n *\n * `disassembleDeck` is the documented-lossy inverse (spec §7: \"disassemble\n * — the inverse of assemble, an optional tail item for W5\"): it reconstructs a spec + pages record from\n * an existing IR, well enough that re-`assembleDeck`-ing the result\n * reproduces the same slide content, but spec-only fields that never made it\n * into the IR in the first place (`focus`, and `summary` on a filled content\n * page) cannot be recovered. See that function's own doc\n * comment for the full accounting. `beat` *did* have this same \"never made\n * it into the IR\" status until the P1 variety wave's task 1 gave it a real\n * `Slide.beat` field (`../ir/index.ts`) — it is now a plain passthrough on\n * both sides, same as `layout`/`heading`, no longer in this lossy list.\n */\nimport { PptwiseError } from \"../errors\"\nimport { PptxIRSchema, type BackgroundSpec, type Component, type PptxIR, type Slide } from \"../ir\"\nimport { resolveEffectiveLayoutId } from \"../svg/layout-selection\"\nimport { formatInvalidSpecError, validateSpec, type DeckSpec, type PageSpec } from \"./index\"\n\n// ── PageContent (per-page authoring record, spec §7's `pages/<id>.json`) ──\n\n/**\n * One page's fillable content — everything a page spec's `id` does *not*\n * already lock in. Deliberately excludes `type`/`heading` (spec-owned, see\n * {@link assembleDeck}'s locked-field gate) and `subheading`/`decor`\n * (legitimate `Slide` fields, but outside this record's shape by spec §7's\n * own layout — \"pages/<id>.json contains only components\" — a spec/pages deck can't\n * author either one — a hand-authored bare IR still can). Every field here is\n * a same-name, same-shape subset of `Slide`'s own optional fields\n * (`../ir`'s `SlideSchema`) — reused, not redeclared, so the two can't drift.\n */\nexport interface PageContent {\n components?: Component[]\n layout?: string\n arrangement?: NonNullable<Slide[\"arrangement\"]>\n background?: BackgroundSpec\n image_side?: \"left\" | \"right\"\n footnote?: string\n notes?: string\n}\n\nexport interface AssembleResult {\n ir: PptxIR\n /**\n * Set only when `spec.seed` was absent and {@link assembleDeck} generated\n * one deterministically (see the seed section of this function's doc\n * comment) — `undefined` when `spec.seed` was already present and simply\n * passed through. The CLI shell (W5 task 5) is the one that acts on this:\n * suggest writing the value back into the spec file, never rewrite it\n * itself (assemble stays a pure function, no fs side effects here).\n */\n generatedSeed?: number\n /**\n * Count of pages whose `layout` field this call filled in via\n * {@link materializeEffectiveLayouts} (W4 design decision 10) —\n * `undefined` when zero, same \"absent means nothing to report\" posture as\n * {@link generatedSeed} just above (a page with an explicit pin already in\n * its page file, or every omitted page landing on the image-cover\n * takeover's `null` bypass, both leave this unset). Purely informational:\n * the CLI shell (`runAssemble`, `../cli/commands.ts`) surfaces it as a\n * one-line note, nothing here or downstream acts on the number itself.\n */\n materializedLayoutCount?: number\n}\n\n// ── deterministic seed generation ───────────────────────────────────────\n\n/**\n * djb2 string hash. The exact same five-line algorithm as\n * `svg/variety.ts`'s `stableHash` (and `svg/components/chart-svg.tsx`'s own\n * private copy of it) — reimplemented locally here for the same reason\n * those two already give each other in `variety.ts`'s doc comment: it is a\n * five-line primitive, and importing it from `svg/variety.ts` would pull a\n * `spec → svg` dependency this module has no business taking (`src/spec` is\n * this module's own package name — renamed from `src/plan` in the src\n * domain-reorg wave, a mechanical follow-up now that every artifact concept\n * it exports was already called \"spec\", see this file's own top comment).\n * `src/spec` sits beside `src/ir` (an IR-adjacent, pre-render authoring\n * concern). `src/svg` is a *consumer* of IR (the render chain), not a\n * neighbor of `spec` — reaching \"up\" into it here would point the\n * dependency arrow the wrong way for what is conceptually a lower-level\n * module. The two hashes are also semantically independent on purpose (see\n * {@link generateSeed}):\n * this one intentionally excludes heading text that `deckSeed` intentionally\n * includes, so sharing an implementation would invite sharing behavior that\n * must not be shared.\n */\nfunction stableHash(s: string): number {\n let h = 5381\n for (let i = 0; i < s.length; i++) h = ((h << 5) + h + s.charCodeAt(i)) | 0\n return Math.abs(h)\n}\n\n/**\n * Deterministic seed for a spec that omits `seed` (spec §5's \"seed mechanism\n * revision\": modification stability requires an *explicit*, persisted seed —\n * this is the one-time generation `assembleDeck` performs on first\n * materialization, spec's own words: \"generated once at creation time,\n * stable thereafter\"). Hashes `filename + the spec's own\n * ordered page-id sequence` — deliberately *not* heading text or any\n * per-page content, unlike `svg/variety.ts`'s `deckSeed` (pre-v0.3 content\n * hash, still used when a bare IR omits `seed` entirely): editing an\n * existing page's heading or components must not reshuffle every other\n * page's auto-selected layout (the exact regression spec §6 calls out this\n * seed field to fix), so this hash is a function of deck *shape* — which\n * pages exist, in what order — not deck *content*. Reordering, adding, or\n * removing pages does change it (page identity plus position both feed the\n * hash: two decks with the same id set in a different order still hash\n * differently — join with a separator no id can itself contain, `\"\\n\"`, so\n * `[\"ab\", \"c\"]` and `[\"a\", \"bc\"]` can never collide).\n */\nfunction generateSeed(filename: string | undefined, pageIds: readonly string[]): number {\n return stableHash([filename ?? \"\", ...pageIds].join(\"\\n\"))\n}\n\n// ── assembleDeck ────────────────────────────────────────────────────────\n\nconst LOCKED_KEYS = [\"type\", \"heading\"] as const\n\n/**\n * Assemble a validated deck spec plus a per-page content record into a\n * renderable IR. See this module's own top comment for the overall shape.\n * Step numbers below match the W5 task-3 brief's own numbered \"inject\n * semantics\" list verbatim — kept in that exact order because two of them\n * (locked-field vs. orphan) both throw and their relative order is\n * otherwise unobservable from either doc comment alone.\n *\n * 1. `spec` is `unknown` (same boundary `validateSpec` itself has — a spec\n * is almost always freshly `JSON.parse`d off disk by the caller) —\n * invalid shape or a failed hard gate throws {@link PptwiseError} with\n * `validateSpec`'s own formatted issue list, not a re-derived message.\n * 2. Shape guard + locked-field protection: a `pages[id]` entry must first be\n * a plain object — not `null`, an array, or a primitive — else throws.\n * `pages` is `unknown`-shaped off disk same as `spec` itself (step 1), so\n * a JSON `null`/string/array content value is a real possibility, not\n * just a type-system hole, and `Object.hasOwn` throws its own\n * uninformative native `TypeError` on `null` (and silently no-ops on a\n * string/array/number) rather than this gate's own readable message.\n * Once that holds, a `pages[id]` entry that carries a `type` or `heading`\n * *key* — even set to `undefined` — throws. `Object.hasOwn`, not a\n * `!== undefined` read, is what makes that \"even `undefined`\" case\n * catchable: `PageContent` itself never declares either field, but a\n * page file freshly parsed off disk is `unknown` before it reaches this\n * function's declared `PageContent` parameter type, so a stray\n * `\"heading\": null`-turned-`undefined` or a copy-pasted empty key is\n * exactly the drift this gate exists to catch instead of silently\n * ignoring.\n * 3. Orphan keys: a `pages` entry whose id isn't any spec page's id. Listed\n * together with a fix suggestion, checked only after every present page\n * has cleared the locked-field gate (step 2) — an orphan file that\n * *also* happens to redeclare `heading` reports as locked-field first.\n * 4. Missing pages (a spec id with no `pages` entry) become a placeholder\n * slide — never an error. Spec §7's own words: \"assemble's precise\n * semantics — a missing page always succeeds (placeholder), a structural\n * contradiction (orphan file / bad spec / id conflict) errors\". A\n * declared `summary` becomes the placeholder's `subheading` so a\n * `--draft` preview of an unfilled deck still reads as more than a bare\n * \"Untitled\". Step 5 preserves the same visible line for filled boundary\n * pages while keeping it fill-only on filled content pages. A declared\n * `beat` carries straight into the\n * placeholder's own `Slide.beat` too (P1 variety wave, task 1 — see step\n * 5's note below). A placeholder page still participates in this\n * function's own layout materialization below, so its beat still needs\n * to reach the IR for that weighting to see it once the page is filled\n * in later without re-materializing.\n * 5. Present pages become a full slide: `id`/`type`/`heading` from the page\n * spec (never the content record — see step 2), plus whichever of\n * {@link PageContent}'s seven fields the content record actually set.\n * `beat` now carries straight into the IR's own `Slide.beat` field too —\n * reached from `PageSpec`, not `PageContent` (same source as `id`/`type`/\n * `heading`, since `beat` is spec-owned, not per-page content) — as of\n * the P1 variety wave's task 1. Previously dropped here as a spec-only\n * authoring anchor, `SlideSchema.beat`'s own doc comment\n * (`../ir/index.ts`) has the full accounting of what it now does\n * downstream. `focus` remains a spec-only authoring anchor. A filled\n * boundary page (`cover`, `chapter`, or `ending`) carries `summary` into\n * `subheading`, because its page file has no body-content field that can\n * express the line after filling. A filled `content` page keeps summary\n * spec-only, so its fill prompt does not become duplicate visible slide\n * content.\n * 6. Top-level: `version` is always the literal `\"4\"` (IR's own version,\n * unrelated to the deck spec's own `version: \"1\"`) — the deck spec's own\n * `narrative` field (renamed this task from `scenario`, spec §8.1's\n * `DeckPlan`→`DeckSpec` rename, task 2) carries across into the v4 IR's\n * own `narrative` field, its value already in the new strategy/pacing\n * vocabulary (vocabulary-v4 rename, task 1 — `spec/index.ts`'s own\n * `resolveNarrative` call already validates it against that vocabulary\n * before this function ever runs).\n * `theme`/`filename`/`brand`/`branding`/`meta`/`seed` (step 7) carry over from the spec when\n * present. When absent, this function omits the field from the raw\n * object it hands to {@link PptxIRSchema} rather than re-deriving IR's\n * own default value a second time — one default source of truth, the\n * schema itself (e.g. `theme` omitted here becomes `{ id: \"consulting\" }`,\n * exactly like a bare hand-authored IR that never mentions theme at all —\n * not a value this function needs to know). `branding` is the same omit\n * posture: a spec that never sets it produces an IR that never sets it,\n * and the renderer treats that as `\"cover-only\"`. This function must not\n * infer branding from narrative.\n * 7. Seed: `spec.seed` present → passed through, `generatedSeed` stays\n * `undefined` on the result. Absent → {@link generateSeed} derives one\n * from `filename` + the spec's own ordered page-id list (never page\n * *content* — see that function's own doc comment for why), written to\n * `ir.seed` *and* returned as `generatedSeed` so a CLI shell can suggest\n * writing it back into the spec file (this function never touches disk\n * itself).\n * 8. Idempotence: every step above is a pure function of its inputs (no\n * randomness, no wall-clock, no reliance on unordered iteration) — two\n * calls with structurally-equal `spec`/`pages` produce deep-equal\n * results, `generatedSeed` included. Exercised directly by this module's\n * test suite rather than asserted here.\n */\nexport function assembleDeck(spec: unknown, pages: Record<string, PageContent>): AssembleResult {\n // Step 1\n const validated = validateSpec(spec)\n if (!validated.ok) {\n throw new PptwiseError(formatInvalidSpecError(validated.errors))\n }\n const deckSpec = validated.spec!\n\n // Step 2 — shape guard + locked-field protection, scanned before orphan\n // detection (see this function's own doc comment for why the order is\n // observable).\n for (const page of deckSpec.pages) {\n const raw = pages[page.id]\n if (raw === undefined) continue\n if (typeof raw !== \"object\" || raw === null || Array.isArray(raw)) {\n throw new PptwiseError(`page \"${page.id}\": page content must be an object`)\n }\n for (const key of LOCKED_KEYS) {\n if (Object.hasOwn(raw, key)) {\n throw new PptwiseError(`page \"${page.id}\": \"${key}\" is locked by the spec — remove it from the page file`)\n }\n }\n }\n\n // Step 3 — orphan pages keys\n const specIds = new Set(deckSpec.pages.map((page) => page.id))\n const orphanIds = Object.keys(pages).filter((id) => !specIds.has(id))\n if (orphanIds.length > 0) {\n throw new PptwiseError(\n `orphan page id${orphanIds.length === 1 ? \"\" : \"s\"} ${orphanIds.map((id) => `\"${id}\"`).join(\", \")} — not in the spec, delete the page file or add the page to the spec`,\n )\n }\n\n // Steps 4 + 5 — build each slide\n const slides = deckSpec.pages.map((page) => buildSlide(page, pages[page.id]))\n\n // Step 7 — seed (computed before step 6's raw object so it can be spliced\n // straight in — spec-only, never reads `pages`, see generateSeed's own doc).\n const generatedSeed =\n deckSpec.seed === undefined ? generateSeed(deckSpec.filename, deckSpec.pages.map((page) => page.id)) : undefined\n const seed = deckSpec.seed ?? generatedSeed!\n\n // Step 6 — top-level IR fields\n const rawIr = {\n version: \"4\" as const,\n ...(deckSpec.narrative !== undefined ? { narrative: deckSpec.narrative } : {}),\n ...(deckSpec.theme !== undefined ? { theme: { id: deckSpec.theme } } : {}),\n ...(deckSpec.filename !== undefined ? { filename: deckSpec.filename } : {}),\n ...(deckSpec.brand !== undefined ? { brand: deckSpec.brand } : {}),\n ...(deckSpec.branding !== undefined ? { branding: deckSpec.branding } : {}),\n meta: deckSpec.meta,\n seed,\n slides,\n }\n\n const parsed = PptxIRSchema.safeParse(rawIr)\n if (!parsed.success) {\n const detail = parsed.error.issues.map((issue) => `${issue.path.join(\".\") || \"(root)\"}: ${issue.message}`).join(\"\\n\")\n throw new PptwiseError(`assembled deck did not produce valid IR:\\n${detail}`)\n }\n\n // Materialization (W4 design decision 10) — must run after the schema\n // parse above, not on `rawIr`: `resolveEffectiveLayoutId` needs the fully\n // *defaulted* IR (a spec that omits `theme`, for instance, needs\n // `parsed.data.theme.id === \"consulting\"` already filled in by the schema,\n // not the bare `rawIr` that simply omits the key).\n const { ir, materializedCount } = materializeEffectiveLayouts(parsed.data)\n\n return {\n ir,\n ...(generatedSeed !== undefined ? { generatedSeed } : {}),\n ...(materializedCount > 0 ? { materializedLayoutCount: materializedCount } : {}),\n }\n}\n\n/**\n * Writes each page's auto-selected layout id into its own `layout` field\n * (W4 design decision 10: once `assembleDeck` has built the IR, run the same\n * `resolveEffectiveLayoutId` selection over it and write the auto-picked\n * result into each page's own `layout` field) — so `deck.json`, the artifact\n * {@link assembleDeck}'s caller actually writes to disk, always names an\n * explicit layout per page instead of leaving a downstream reader (or a\n * future re-`assembleDeck` call with a different seed) to re-derive one.\n * Runs exactly once per {@link assembleDeck} call, after {@link PptxIRSchema}\n * has already produced `ir` — every input `resolveEffectiveLayoutId` needs\n * (resolved theme id, resolved narrative strategy, the final `seed`) is only\n * available on that fully-defaulted object, never on the raw pre-parse shape.\n *\n * Two cases leave a slide untouched:\n *\n * - `slide.layout` already set (the page file wrote one explicitly, `step 5`\n * of {@link assembleDeck}'s own doc comment) — skipped without even calling\n * the resolver. `resolveEffectiveLayoutId` would just echo a valid\n * layout pin straight back anyway (its own explicit-pin short-circuit),\n * but skipping the call keeps \"an explicit page file value survives\n * untouched\" true by construction, not by coincidence of what the resolver\n * happens to do with it.\n * - The resolver returns `null` — the image-cover takeover\n * (`ImageCoverPage`'s bespoke frame, no `LAYOUT_REGISTRY` id to name, see\n * `resolveEffectiveLayoutId`'s own doc comment). `null` has no home in\n * `layout`'s `string | undefined` shape, and this function's job is to\n * materialize *exactly* what the resolver already means by its return\n * value, never invent a representation the resolver itself doesn't have.\n *\n * Every other omitted-`layout` slide gets `resolveEffectiveLayoutId`'s\n * return value spliced in — always a real `LAYOUT_REGISTRY` id for the 13\n * built-in themes (their curated pools are never empty for any slide type).\n *\n * Reads every slide off the *same* `ir` object across the whole pass, and\n * builds a fresh `slides` array instead of mutating one in place — required\n * by `resolveEffectiveLayoutId`'s own adjacent-anti-repetition fold\n * (`../svg/layout-selection.ts`'s `resolveDeckEffectiveLayoutIds`), which\n * walks the deck once against `ir.slides` exactly as given and caches by\n * `ir` object identity: feeding it a slide whose `layout` this function's\n * own earlier iteration had already overwritten would corrupt \"slide i-1's\n * final effective id\" into \"slide i-1's materialized id\" for every later\n * slide — the same value for an auto-picked neighbor, but silently wrong the\n * moment a spec pins one page's layout explicitly next to an auto-picked one\n * (the pin would then read back as slide i-1's own \"effective id\" a second\n * time, double-counting it as a repetition risk it was never actually a\n * candidate for).\n *\n * Round-trip note: `disassembleDeck` (below) reads whatever this function\n * wrote the exact same way it reads a hand-authored pin — see that\n * function's own doc comment for why that is an accepted consequence, not a\n * bug.\n */\nfunction materializeEffectiveLayouts(ir: PptxIR): { ir: PptxIR; materializedCount: number } {\n let materializedCount = 0\n const slides = ir.slides.map((slide, index) => {\n if (slide.layout !== undefined) return slide\n const effectiveLayoutId = resolveEffectiveLayoutId(ir, slide, index)\n if (effectiveLayoutId === null) return slide\n materializedCount++\n return { ...slide, layout: effectiveLayoutId }\n })\n return materializedCount === 0 ? { ir, materializedCount } : { ir: { ...ir, slides }, materializedCount }\n}\n\n/** Step 4 (no content record → placeholder) / step 5 (content record → full slide). */\nfunction buildSlide(page: PageSpec, raw: PageContent | undefined): Record<string, unknown> {\n if (raw === undefined) {\n return {\n id: page.id,\n type: page.type,\n heading: page.heading,\n placeholder: true,\n ...(page.beat !== undefined ? { beat: page.beat } : {}),\n ...(page.summary !== undefined ? { subheading: page.summary } : {}),\n }\n }\n return {\n id: page.id,\n type: page.type,\n heading: page.heading,\n ...(page.beat !== undefined ? { beat: page.beat } : {}),\n ...(page.type !== \"content\" && page.summary !== undefined ? { subheading: page.summary } : {}),\n ...(raw.components !== undefined ? { components: raw.components } : {}),\n ...(raw.layout !== undefined ? { layout: raw.layout } : {}),\n ...(raw.arrangement !== undefined ? { arrangement: raw.arrangement } : {}),\n ...(raw.background !== undefined ? { background: raw.background } : {}),\n ...(raw.image_side !== undefined ? { image_side: raw.image_side } : {}),\n ...(raw.footnote !== undefined ? { footnote: raw.footnote } : {}),\n ...(raw.notes !== undefined ? { notes: raw.notes } : {}),\n }\n}\n\n// ── disassembleDeck ─────────────────────────────────────────────────────\n\n/** Heading synthesized for a bare IR slide whose own `heading` is missing or\n * blank (`SlideSchema.heading` is optional with no default — a hand-authored\n * IR is free to omit it entirely — but `PageSpecSchema.heading` is\n * required, non-empty, spec §5: a page spec without a heading has no\n * reasonable default). Adjudication (W5 task-3 brief flags this as an open\n * call): a fixed, deterministic placeholder was picked over synthesizing\n * from the slide's first text-bearing component, because there frequently\n * isn't one to find — a `kpi_cards`-only or `chart`-only content slide has\n * no paragraph/bullets/quote text at all, so a \"first text content\"\n * heuristic would need its own silent fallback for exactly the slides most\n * likely to hit this path, plus ad-hoc truncation to clear the 48-char spec\n * heading gate (`CAPACITY.headingMaxChars`, `./index.ts`). A single fixed\n * string is total, deterministic, always legal against that gate, and — as\n * a visibly-fake title — a much louder signal to fill in a real heading\n * than a truncated content fragment that might accidentally read as\n * intentional. */\nconst UNTITLED_HEADING = \"Untitled\"\n\n/**\n * Inverse of {@link assembleDeck}: reconstructs `{ spec, pages }` from an\n * existing IR well enough that `assembleDeck(...disassembleDeck(ir))`\n * reproduces the deck-project content surface. The map is not lossless for\n * fields that the spec/pages format cannot represent. The known losses and\n * recoveries are:\n *\n * - `focus` never appears on any produced {@link PageSpec} — it is a\n * spec-only authoring anchor with no corresponding `Slide` field at all\n * (see {@link assembleDeck} step 5's doc comment). Nothing here could\n * recover a value that was never written anywhere. `beat` used to share\n * this fate but no longer does (P1 variety wave, task 1): it is now a\n * plain passthrough on both sides, exactly like `layout`/`heading` below —\n * `assembleDeck` step 5/6 reads `pageSpec.beat` into `slide.beat`, this\n * function reads `slide.beat` straight back below.\n * - `summary` is recovered from `subheading` for every placeholder slide and\n * for filled boundary slides (`cover`, `chapter`, and `ending`). This\n * reverses steps 4 and 5 without losing their visible subtitle. A filled\n * content slide's own `subheading` remains a separate authored field. It\n * has no {@link PageContent} field to land in, and treating it as summary\n * would turn visible slide copy into a fill-only prompt, so it is dropped.\n * Same for `decor`: a real `Slide` field absent from `PageContent`.\n * - `theme.style` / `theme.brand` overrides collapse to a bare theme-id\n * string (`DeckSpecSchema.theme` has no shape for either) — only `theme.id`\n * survives. That `theme.brand` is `ThemeSchema.brand` (`BrandConfigSchema`\n * — `suppressFooterOnCardContent`/`suppressFooterRule`/`suppressFooterMeta`,\n * brand-footer flags owned by the *theme*) — not to be confused with the deck-level\n * `brand` field below, a different, unrelated schema despite the shared\n * name.\n * - `ir.assets.images` is not part of this function's return value at all —\n * `{ spec, pages }` has no `assets` field, and this module stays zero-fs\n * by design (this file's own top comment), so it has no way to write an\n * `assets/` directory itself. Any `asset_id` reference inside a copied\n * `components`/`background` survives untouched ({@link extractPageContent}\n * copies both as-is), but the underlying image bytes are deliberately left\n * for the caller: the CLI shell closes that gap. `runDisassemble`\n * (`../cli/commands.ts`) walks the *input* IR's `ir.assets.images` itself\n * and materializes every entry into `<outDir>/assets/<id>.<ext>`\n * (`writeDeckAssets`, `../cli/deck-dir.ts`) so a later `readDeckDir`'s\n * `scanAssets` re-registers them exactly like any other deck-directory\n * asset. Skipping that shell-side step is not a doc-comment nuance — it\n * reproduces as a real bug: an image deck disassembles with every\n * `asset_id` left dangling, then re-assembles and renders with the image\n * silently missing.\n *\n * Round-trip-safe despite the above, worth calling out because of that name\n * collision: the top-level `brand` field (`BrandSchema` — `logo_asset_id` /\n * `position`, the deck logo/position `Branding` reads,\n * `src/svg/branding.tsx`) is a plain passthrough on both sides\n * ({@link assembleDeck} step 6 reads `spec.brand` into `ir.brand` — this\n * function reads `ir.brand` back into `spec.brand` below) — carried through\n * unmodified, same as `narrative`/`filename`/`seed`/`branding`, never\n * synthesized or dropped.\n *\n * `layout` deserves a different kind of callout: it round-trips as plain\n * content like any other field ({@link extractPageContent} copies\n * `slide.layout` into `PageContent.layout` whenever the slide has one, no\n * special case) — but a `deck.json` produced by {@link assembleDeck} now\n * carries a `layout` on nearly every slide (W4 design decision 10's\n * materialization, see that function's own doc comment), even on pages whose\n * *original page file* never set one. Nothing on `Slide` marks which is\n * which — `layout` is just a string either way — so disassembling an\n * already-assembled `deck.json` writes every materialized pick into the\n * regenerated page file as if it had been an explicit pin all along, and a\n * later re-`assembleDeck` call skips materialization for that page from then\n * on, same as any hand-authored pin. Accepted, not a bug: `disassembleDeck`\n * is a one-time bare-IR importer into an editable project directory, not a\n * round-trip channel for `deck.json` itself. A deck project's own\n * `pages/<id>.json` files are the durable, edit-in-place artifacts —\n * `deck.json` is downstream output, and feeding it back through\n * `disassembleDeck` (instead of editing the project directory that produced\n * it) is what actually costs those pages their revision-stability\n * re-selection eligibility going forward. A real, user-visible narrowing,\n * worth knowing about here, not worth adding code to guard against for a\n * usage pattern nothing in this codebase actually exercises.\n *\n * Two structural fields are synthesized rather than copied when the source\n * slide omits them, each documented where it is generated:\n * {@link UNTITLED_HEADING} for a missing/blank `heading`, and a positional\n * `p-<1-based-ordinal>-<type>` scheme for a missing `id` (stable across\n * repeated calls on the same IR — it is a pure function of slide position\n * and type — but, unlike a spec-assigned id, *not* stable across inserting\n * or reordering slides — out of scope here, since a bare IR with no `id` at\n * all has no stabler identity to fall back to in the first place).\n *\n * That generated `p-<ordinal>-<type>` id is safe by construction as a\n * page/asset file-name segment (W5 whole-branch review finding 1, verified\n * — not just asserted — rather than also routing it through\n * `assertSafeFileSegment`, `../cli/deck-dir.ts`): `<ordinal>` is\n * `index + 1`, always a plain non-negative integer, and `<type>` is a\n * `Slide[\"type\"]`, a closed schema enum (`\"cover\" | \"chapter\" | \"content\" |\n * \"ending\"`, `SlideSchema.type` in `../ir/index.ts`) — neither half can ever\n * contain a `/`, a `\\`, or resolve to `\"..\"`, so the joined id can't either.\n * A carried-over `slide.id` (the `??` branch's other side) has no such\n * guarantee — that is the one this function passes straight through\n * unchecked, same as every other field {@link extractPageContent} copies —\n * which is exactly why the write-time gate in `../cli/deck-dir.ts` (not this\n * function) is what actually closes the vulnerability.\n */\nexport function disassembleDeck(ir: PptxIR): { spec: DeckSpec; pages: Record<string, PageContent> } {\n const pages: Record<string, PageContent> = {}\n const pageSpecs: PageSpec[] = ir.slides.map((slide, index) => {\n const id = slide.id ?? `p-${index + 1}-${slide.type}`\n const heading = slide.heading !== undefined && slide.heading.trim() !== \"\" ? slide.heading : UNTITLED_HEADING\n const pageSpec: PageSpec = {\n id,\n type: slide.type,\n heading,\n ...(slide.beat !== undefined ? { beat: slide.beat } : {}),\n ...((slide.placeholder === true || slide.type !== \"content\") && slide.subheading !== undefined\n ? { summary: slide.subheading }\n : {}),\n }\n if (slide.placeholder !== true) pages[id] = extractPageContent(slide)\n return pageSpec\n })\n\n const spec: DeckSpec = {\n version: \"1\",\n // `ir.narrative` (v4 field, vocabulary-v4 rename) carries straight into\n // the deck spec's own `narrative` field — its value is\n // already in the new strategy/pacing vocabulary, no remapping needed.\n ...(ir.narrative !== undefined ? { narrative: ir.narrative } : {}),\n theme: ir.theme.id,\n filename: ir.filename,\n ...(ir.seed !== undefined ? { seed: ir.seed } : {}),\n ...(ir.brand !== undefined ? { brand: ir.brand } : {}),\n ...(ir.branding !== undefined ? { branding: ir.branding } : {}),\n meta: ir.meta,\n pages: pageSpecs,\n }\n\n return { spec, pages }\n}\n\n/** Non-placeholder-slide half of {@link disassembleDeck} — the same seven\n * fields {@link buildSlide} injects, read back off the slide. `components`\n * is included only when non-empty: `Slide.components` always defaults to\n * `[]` (never `undefined`, `SlideSchema` in `../ir`), but that default and\n * an author explicitly wanting an empty list are indistinguishable, so an\n * empty array is treated the same as \"omitted\" — round-trips to the exact\n * same `[]` either way once re-defaulted by `assembleDeck`. */\nfunction extractPageContent(slide: Slide): PageContent {\n const content: PageContent = {}\n if (slide.components.length > 0) content.components = slide.components\n if (slide.layout !== undefined) content.layout = slide.layout\n if (slide.arrangement !== undefined) content.arrangement = slide.arrangement\n if (slide.background !== undefined) content.background = slide.background\n if (slide.image_side !== undefined) content.image_side = slide.image_side\n if (slide.footnote !== undefined) content.footnote = slide.footnote\n if (slide.notes !== undefined) content.notes = slide.notes\n return content\n}\n","/**\n * Deterministic, pure `deck.plan.json` → `deck.spec.json` migration (spec\n * §9.2, vocabulary-v4 rename, task 2). Mirrors `../ir/migrate.ts`'s\n * `migrateIrV3ToV4` in spirit — mechanical field rename only, no schema\n * validation, no model call, no content rewrite (same posture spec §9.3\n * states for the IR v3→v4 primitive: \"只做已声明的结构映射,不运行模型,\n * 不重写内容,不重新选择 layout\" — this function is that same contract\n * applied to the other artifact this rename touches). A caller should run\n * `validateSpec` (`./index.ts`) against the result to confirm it lands as a\n * legal deck spec, exactly as it would for a `deck.spec.json` authored by\n * hand — this function itself never parses against `DeckSpecSchema`.\n *\n * Field-for-field, value-for-value per spec §9.2's table:\n *\n * ```text\n * deck.plan.json → deck.spec.json\n * scenario → narrative\n * pages[].rhythm → pages[].beat\n * 其余字段 → 原样保留\n * ```\n *\n * Takes the raw, `JSON.parse`d `deck.plan.json` contents (`unknown`) rather\n * than an already-validated shape — unlike `migrateIrV3ToV4` (which takes an\n * already-`PptxIRV3Schema.parse`d object), there is no schema left in this\n * codebase a pre-rename plan file could validate against: `DeckSpecSchema`\n * (`./index.ts`) already requires the *post*-rename field names (`narrative`,\n * `beat`), so parsing a plan-shaped document against it would just fail on\n * the very keys this function exists to rename. Non-object input, or a\n * `pages` value that isn't an array, passes through completely unchanged —\n * reporting a malformed source file is `runMigrate`'s job\n * (`../cli/commands.ts`), not this mechanical rename step's.\n */\nimport {\n migrateBannerHeadingToTwoColumn,\n migrateBloomToClassroom,\n migrateChromeToBranding,\n migrateLogoWallToImageGrid,\n} from \"../ir/migrate\"\n\nexport function migrateDeckPlanToSpec(raw: unknown): unknown {\n if (typeof raw !== \"object\" || raw === null || Array.isArray(raw)) return raw\n const { scenario, pages, ...rest } = raw as Record<string, unknown>\n const result: Record<string, unknown> = { ...rest }\n if (scenario !== undefined) result.narrative = scenario\n if (Array.isArray(pages)) {\n result.pages = pages.map(migratePageRhythmToBeat)\n } else if (pages !== undefined) {\n // Not an array — structurally invalid either way, left untouched for\n // `validateSpec` to report on its own terms (same \"mechanical, not\n // validating\" posture as the rest of this function).\n result.pages = pages\n }\n // After scenario→narrative / rhythm→beat: chrome→branding, then the\n // one-shot bloom→classroom relocate, leftover logo_wall → image_grid,\n // leftover banner-heading → two-column. Dual-source (chrome + branding)\n // hard-errors via migrateChromeToBranding.\n return migrateBannerHeadingToTwoColumn(\n migrateLogoWallToImageGrid(migrateBloomToClassroom(migrateChromeToBranding(result))),\n )\n}\n\n/** `pages[].rhythm` → `pages[].beat` (spec §9.2) — applied per page so a\n * mix of already-migrated and not-yet-migrated page objects (unlikely in\n * practice, but not this function's job to rule out) still converts\n * correctly: a page with no `rhythm` key passes through unchanged. */\nfunction migratePageRhythmToBeat(page: unknown): unknown {\n if (typeof page !== \"object\" || page === null || Array.isArray(page)) return page\n const { rhythm, ...rest } = page as Record<string, unknown>\n const next: Record<string, unknown> = { ...rest }\n if (rhythm !== undefined) next.beat = rhythm\n return next\n}\n","import type { Component, PptxIR, Slide } from \"@/ir\"\nimport { renderSlideSvg } from \"../api\"\nimport { getPlatform } from \"../platform/registry\"\nimport { resolveStyle } from \"../themes\"\nimport { CANONICAL_THEME_IDS, THEME_LABELS, type CanonicalThemeId } from \"../themes/index\"\nimport { getThemeDefinition, type ThemeDefinition } from \"../themes/definitions\"\nimport type { StyleColors } from \"../themes/tokens\"\nimport { parseTransform } from \"./audit/svg-audit\"\n\ntype ImageComponent = Extract<Component, { type: \"image\" }>\n\n/**\n * Real rendered frame for one image slot, in canvas px (0-1280 × 0-720 —\n * `../constants`'s `CANVAS_W_PX`/`CANVAS_H_PX`). This is the whole point of\n * this module (asset-brief plan, 裁定 1): it comes from parsing an actual\n * render pass's SVG output, never from hand-copying `image.tsx`'s\n * `w * 0.5`/`MAX_IMAGE_H` constants — those change independently of this\n * file, and a shadow copy would silently start lying the day they do.\n */\nexport interface AssetBriefFrame {\n x: number\n y: number\n w: number\n h: number\n /** e.g. \"2:1\" when w:h reduces to a small clean ratio, else \"1.63:1\". */\n aspect: string\n}\n\nexport interface AssetBriefFit {\n mode: \"cover\" | \"contain\"\n /** One sentence: crop behavior + safe-zone guidance for a generator. */\n note: string\n}\n\nexport interface AssetBriefPalette {\n /** Deduplicated theme hex values, in `StyleColors`' own field order. */\n hexes: string[]\n primary: string\n accent: string\n}\n\nexport interface AssetBriefMood {\n /** `ThemeDefinition.tags` passed through as-is (empty for every one of\n * the 13 builtins today — none has adopted this field yet — but a\n * registered custom theme may set it). */\n tags: readonly string[]\n /** One sentence assembled from the theme's own label/motif — never a\n * hand-written per-theme paragraph (asset-brief plan 裁定: \"不新写 13 段\n * 文案\"). */\n description: string\n}\n\nexport interface AssetBriefPage {\n index: number\n id?: string\n type: Slide[\"type\"]\n heading?: string\n}\n\nexport interface AssetBriefItem {\n page: AssetBriefPage\n asset_id: string\n /** `ir.assets.images[asset_id].alt` passed straight through (A11Y-01 alt\n * chain wave, task 1) — this is already a per-asset structure, so a\n * generator agent filling in `missing`/`suggested_prompt` items sees\n * right alongside them which ids already have an accessibility\n * description written and which don't. Omitted (not an empty string)\n * when the asset has no `alt` — same \"don't fabricate a value that\n * isn't there\" discipline the rest of this file already follows for\n * `frame`/`suggested_pixels`. */\n alt?: string\n /** Discriminant reserved for v2 (asset-brief plan 裁定 4): only `\"image\"`\n * is produced today (one `AssetBriefItem` per `image` component, or per\n * shared frame — see `shared` below). `\"background\"` is the documented\n * extension slot for background asset specs, deliberately not built in\n * v1 (no `background` geometry extraction exists yet) — this field lets\n * a future v2 add it without a breaking shape change for consumers that\n * already switch on `kind`. */\n kind: \"image\"\n /** No usable `src` in `ir.assets.images` for this `asset_id` — this is a\n * generation to-do item, not a defect (asset-brief plan 裁定 2). */\n missing: boolean\n /** `false` when the selected layout dropped this image component\n * entirely (e.g. an overflow guard) — still listed rather than silently\n * dropped (task brief's explicit requirement). `frame`/`suggested_pixels`\n * are omitted in that case; there is no real geometry to report. */\n rendered: boolean\n frame?: AssetBriefFrame\n /** 2× the frame's own pixel dimensions — omitted alongside `frame` when `rendered` is false. */\n suggested_pixels?: { w: number; h: number }\n fit: AssetBriefFit\n palette: AssetBriefPalette\n mood: AssetBriefMood\n /** One English paragraph, paste-ready for an image-generation tool. */\n suggested_prompt: string\n /** True when this item's `frame` cannot be attributed to one specific\n * `image` component: `occurrenceCount` (>=2) `image` components on this\n * same page share this `asset_id`. Because a shared `asset_id` resolves\n * to the exact same dummy href in the render-only pass, every occurrence\n * produces an *identical* `<image>` `href` in the rendered SVG — there is\n * no signal left to tell which `<image>` element came from which\n * component, in principle, not just in this implementation. Rather than\n * guess (see `buildAssetBrief`'s own doc comment for the bug this\n * replaced), every real rendered frame for the shared `asset_id` gets its\n * own item, all flagged `shared: true`, none claiming a specific\n * component. Omitted (not `false`) for the single-occurrence case — the\n * overwhelmingly common one — so that path's output shape is byte-for-\n * byte what it was before this field existed. */\n shared?: true\n /** Present iff `shared` is true: how many `image` components on this page\n * reference this same `asset_id`. */\n occurrenceCount?: number\n}\n\nexport interface AssetBrief {\n theme: string\n items: AssetBriefItem[]\n}\n\n/**\n * A 1×1 transparent PNG, reused as the dummy asset every referenced\n * `asset_id` gets overridden to for the render-only pass below (asset-brief\n * plan 裁定 2). Its own pixel content is never inspected — only the `<image>`\n * geometry the renderer places it into matters — so one shared constant\n * asset is enough regardless of how many image slots a deck has.\n */\nconst DUMMY_PNG_DATA_URI =\n \"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII=\"\n\ninterface RawImageFrame {\n x: number\n y: number\n w: number\n h: number\n preserveAspectRatio: string\n}\n\n/**\n * Extract every `<image>` element's *absolute* canvas geometry out of one\n * rendered slide's SVG markup, keyed by the `#<asset_id>` fragment the\n * render-only override below appends to every dummy `href` (so this never\n * needs to guess which `<image>` belongs to which component — see\n * `buildAssetBrief`'s own comment on the override for why the fragment\n * always survives verbatim into the output).\n *\n * Parses via `DOMParser` behind the platform seam (`getPlatform().domParser\n * ?? globalThis.DOMParser`) — the exact same seam and the exact same\n * `parseTransform` accumulation `./audit/svg-audit.ts`'s `auditSvgMarkup`\n * already uses to walk a rendered slide's real transform stack. Iron rule 3\n * (`src/index.ts`'s dependency closure must stay Node-free) is why this\n * isn't a `linkedom` import: the platform seam is what keeps this file\n * usable in a browser (native `DOMParser`) and in Node (`installNodePlatform()`\n * registers linkedom's) alike, without this module ever importing either\n * directly. A hand-rolled regex tokenizer could also track `<g transform>`\n * nesting well enough for this renderer's own output shapes, but would be a\n * second, bespoke implementation of exactly what `parseTransform` already\n * does correctly for every existing consumer — reusing it is less code and\n * cannot drift from the audit path's own geometry truth.\n */\nfunction extractImageFrames(markup: string): Map<string, RawImageFrame[]> {\n const Parser = getPlatform().domParser ?? globalThis.DOMParser\n if (!Parser) {\n throw new Error(\n 'DOMParser unavailable — in Node, call installNodePlatform() from \"@liustack/pptwise/node\" first (the pptwise CLI does this automatically)',\n )\n }\n const doc = new Parser().parseFromString(markup, \"image/svg+xml\")\n const byAssetId = new Map<string, RawImageFrame[]>()\n\n const visit = (el: Element, ox: number, oy: number, os: number) => {\n const { dx, dy, scale } = parseTransform(el)\n const ax = ox + os * dx\n const ay = oy + os * dy\n const as = os * scale\n if (el.tagName.toLowerCase() === \"image\") {\n const href = el.getAttribute(\"href\") ?? \"\"\n const hashAt = href.indexOf(\"#\")\n if (hashAt !== -1) {\n const assetId = href.slice(hashAt + 1)\n const frame: RawImageFrame = {\n x: ax + as * Number(el.getAttribute(\"x\") ?? 0),\n y: ay + as * Number(el.getAttribute(\"y\") ?? 0),\n w: as * Number(el.getAttribute(\"width\") ?? 0),\n h: as * Number(el.getAttribute(\"height\") ?? 0),\n preserveAspectRatio: el.getAttribute(\"preserveAspectRatio\") ?? \"xMidYMid meet\",\n }\n const list = byAssetId.get(assetId)\n if (list) list.push(frame)\n else byAssetId.set(assetId, [frame])\n }\n }\n for (const child of Array.from(el.children)) visit(child, ax, ay, as)\n }\n visit(doc.documentElement, 0, 0, 1)\n return byAssetId\n}\n\nfunction gcd(a: number, b: number): number {\n let x = Math.abs(Math.round(a))\n let y = Math.abs(Math.round(b))\n while (y !== 0) [x, y] = [y, x % y]\n return x || 1\n}\n\n/**\n * \"2:1\" when `w/h` lands within 1% of a small `n:d` ratio (`d` up to 12 —\n * covers every whole/half/thirds ratio this renderer's fixed slot math\n * produces, e.g. 613×307's 1.9967 ≈ 2/1), else a 2-decimal \"1.63:1\" fallback.\n * `w`/`h` are already-rounded canvas px, so false near-misses from float\n * noise (not real ratio ambiguity) are the only thing the tolerance guards\n * against.\n */\nfunction formatAspect(w: number, h: number): string {\n if (h <= 0 || w <= 0) return `${w}:${h}`\n const ratio = w / h\n const TOL = 0.01\n for (let d = 1; d <= 12; d++) {\n const n = Math.round(ratio * d)\n if (n >= 1 && Math.abs(ratio - n / d) <= TOL) {\n const g = gcd(n, d)\n return `${n / g}:${d / g}`\n }\n }\n return `${ratio.toFixed(2)}:1`\n}\n\nfunction toFrame(raw: RawImageFrame): AssetBriefFrame {\n const w = Math.round(raw.w)\n const h = Math.round(raw.h)\n return { x: Math.round(raw.x), y: Math.round(raw.y), w, h, aspect: formatAspect(w, h) }\n}\n\nfunction buildFit(mode: ImageComponent[\"fit\"], aspect: string | undefined): AssetBriefFit {\n if (mode === \"cover\") {\n return {\n mode,\n note: aspect\n ? `Cover crop (xMidYMid slice): generate close to ${aspect} to avoid any crop — otherwise the renderer center-crops to fill the frame, so keep essential subject matter within the center safe zone.`\n : `Cover crop (xMidYMid slice): the renderer center-crops to fill the frame — keep essential subject matter centered.`,\n }\n }\n return {\n mode,\n note: aspect\n ? `Contain fit (xMidYMid meet): the renderer letterboxes to show the whole image inside the frame with no crop — match ${aspect} to avoid empty margins.`\n : `Contain fit (xMidYMid meet): the renderer letterboxes to show the whole image inside the frame with no crop.`,\n }\n}\n\n/**\n * Dedupe theme identity colors into a flat hex list, in `StyleColors`' own\n * field order — deliberately excludes `chartPalette`/`accentPool` (data-viz\n * and multi-accent-cycling pools respectively, not the theme's core\n * identity a photo brief should pull from).\n */\nfunction buildPalette(colors: StyleColors): AssetBriefPalette {\n const candidates = [colors.bg, colors.surface, colors.panel, colors.primary, colors.accent, colors.text, colors.muted, colors.border]\n const hexes = [...new Set(candidates.filter((c): c is string => Boolean(c)))]\n return { hexes, primary: colors.primary, accent: colors.accent }\n}\n\nfunction themeLabel(id: string): string {\n return (CANONICAL_THEME_IDS as readonly string[]).includes(id) ? THEME_LABELS[id as CanonicalThemeId] : id\n}\n\nfunction humanizeMotif(motif: string): string {\n return motif.replace(/-motif$/, \"\").replace(/-/g, \" \")\n}\n\n/**\n * One assembled sentence from the theme's own already-declared label/tags/\n * motif — never a 13-theme hand-written paragraph set (asset-brief plan's\n * own explicit constraint). Every one of the 13 builtins has an empty\n * `tags` array today (none has adopted the field yet), so `tagPhrase` is a\n * no-op for all of them right now and only starts contributing once a theme\n * (builtin or registered) actually sets one.\n */\nfunction buildMood(themeId: string, themeDef: ThemeDefinition): AssetBriefMood {\n const label = themeLabel(themeId)\n const tagPhrase = themeDef.tags.length > 0 ? ` (${themeDef.tags.join(\", \")})` : \"\"\n const motifPhrase = themeDef.motif ? ` with a ${humanizeMotif(themeDef.motif)} decorative motif` : \"\"\n return { tags: themeDef.tags, description: `${label} theme${tagPhrase}${motifPhrase}.` }\n}\n\nfunction buildPrompt(mood: AssetBriefMood, palette: AssetBriefPalette, frame: AssetBriefFrame | undefined, fit: AssetBriefFit): string {\n const supporting = palette.hexes.filter((h) => h !== palette.primary && h !== palette.accent)\n const paletteText = `Color palette: primary ${palette.primary}, accent ${palette.accent}${supporting.length > 0 ? `, supporting tones ${supporting.join(\", \")}` : \"\"}.`\n const compositionText = frame\n ? `Compose for a ${frame.aspect} frame. ${fit.note}`\n : `Frame geometry unavailable — this image slot was not rendered under the deck's currently selected layout; generate at a versatile aspect ratio and verify placement once a layout renders it.`\n return `${mood.description} ${paletteText} ${compositionText}`\n}\n\n/**\n * Build an image-generation brief for every `image` component in a deck: the\n * real rendered frame (from an actual render pass — see `extractImageFrames`),\n * fit/crop mode with a safe-zone note, suggested generation pixels (2× the\n * frame), the resolved theme's palette/mood, and a paste-ready English\n * `suggested_prompt`. Pure function of `ir` (same input → same output, proven\n * by `asset-brief.test.ts`'s double-call check) — like `auditDeck`, it never\n * mutates its argument and never touches the filesystem or network.\n *\n * v1 scope (asset-brief plan 裁定 4): only `image`-typed components — not\n * `image_grid`/`image_compare` (separate component types with their own\n * asset_id arrays), and not `background` asset specs. Both are natural v2\n * extensions of this same shape and are deliberately left off `AssetBrief`\n * rather than bolted on half-done.\n *\n * Missing-asset handling (裁定 2): every `image` component's `asset_id`,\n * present or not, gets overridden to the same dummy 1×1 PNG (tagged with a\n * `#<asset_id>` fragment) in an in-memory copy of `ir` used *only* for this\n * function's own extraction render pass — `ir` itself, and the real render/\n * export path, never see this override. `missing` below is decided from the\n * real `ir.assets.images` (before the override), so it still accurately\n * reports \"nothing usable was ever supplied here\" even though the\n * extraction render always has *something* to draw. Overriding every\n * asset_id uniformly (not just the missing ones) is what makes the fragment\n * a reliable identifier regardless of whether the real asset was already\n * resolved — the alternative (leaving resolved assets' real `src` alone)\n * would have no way to tell two different real images' hrefs apart when a\n * page has more than one.\n *\n * Known limitation: an `asset_id` shared between an `image` component and a\n * `background` asset spec on the same page would get the same dummy\n * fragment on both, which could ambiguate which rendered `<image>` this\n * function attributes to the component — rare in practice (v1 doesn't cover\n * background geometry at all, see above) and left as a documented gap\n * rather than engineered around.\n *\n * Shared-`asset_id` handling (task reviewer finding, fixed after v1's first\n * pass): the IR schema places no uniqueness constraint on `asset_id`\n * (`src/ir/index.ts`) — two different `image` components on the same page\n * legally reference the same one. Because the override above (deliberately)\n * maps a shared `asset_id` to one shared dummy href, both occurrences render\n * an *identical* `<image href>` — the rendered SVG carries no signal left to\n * tell them apart. (v1's first pass tried anyway: a FIFO queue keyed by\n * `asset_id`, drained in `slide.components` order under the assumption that\n * extraction order would match — it doesn't in general, e.g.\n * `content-image-lead-split.tsx` renders its narrow-column body *before*\n * its visual-lead column in the JSX it returns, so the queue was backwards\n * and every frame ended up attributed to the wrong occurrence. Not a fixable\n * ordering bug: no ordering convention could be relied on across every\n * layout's own JSX emission order, present and future.) The honest fix:\n * per page, group `image` components by `asset_id` first. A group of size 1\n * (the overwhelmingly common case) keeps the exact single-item shape this\n * function always produced. A group of size >1 abandons per-component\n * attribution entirely — it emits one item per *real rendered frame* for\n * that `asset_id` (not per component), each flagged `shared: true` with the\n * group's `occurrenceCount`, so every real frame is still reported (nothing\n * silently dropped) without asserting a specific component<->frame pairing\n * that cannot be known. If fewer frames render than there are occurrences\n * (e.g. the layout dropped one), the shortfall is padded with `rendered:\n * false` shared items so the count of components sharing the id is still\n * fully visible to a reader of the brief.\n */\nexport function buildAssetBrief(ir: PptxIR): AssetBrief {\n const themeDef = getThemeDefinition(ir.theme.id)\n const tokens = resolveStyle(ir.theme.id, ir.theme.style)\n const palette = buildPalette(tokens.colors)\n const mood = buildMood(ir.theme.id, themeDef)\n\n const occurrences: { slideIndex: number; component: ImageComponent }[] = []\n ir.slides.forEach((slide, slideIndex) => {\n for (const component of slide.components) {\n if (component.type === \"image\") occurrences.push({ slideIndex, component })\n }\n })\n\n const overrides: PptxIR[\"assets\"][\"images\"] = {}\n for (const { component } of occurrences) {\n overrides[component.asset_id] = { src: `${DUMMY_PNG_DATA_URI}#${component.asset_id}` }\n }\n const renderIr: PptxIR = { ...ir, assets: { images: { ...ir.assets.images, ...overrides } } }\n\n const framesBySlide = new Map<number, Map<string, RawImageFrame[]>>()\n for (const slideIndex of new Set(occurrences.map((o) => o.slideIndex))) {\n framesBySlide.set(slideIndex, extractImageFrames(renderSlideSvg(renderIr, slideIndex)))\n }\n\n const items: AssetBriefItem[] = []\n ir.slides.forEach((slide, slideIndex) => {\n // Group this page's `image` components by `asset_id`, preserving each\n // id's first-encounter order — same overall item ordering the old flat\n // `occurrences.map` produced for the (overwhelmingly common)\n // one-component-per-asset_id case.\n const groups = new Map<string, ImageComponent[]>()\n for (const component of slide.components) {\n if (component.type !== \"image\") continue\n const list = groups.get(component.asset_id)\n if (list) list.push(component)\n else groups.set(component.asset_id, [component])\n }\n if (groups.size === 0) return\n\n const page: AssetBriefPage = { index: slideIndex, id: slide.id, type: slide.type, heading: slide.heading }\n const frameMap = framesBySlide.get(slideIndex)\n const isMissing = (assetId: string) => !ir.assets.images[assetId]?.src\n const altOf = (assetId: string) => ir.assets.images[assetId]?.alt\n\n for (const [assetId, group] of groups) {\n const frames = frameMap?.get(assetId) ?? []\n\n if (group.length === 1) {\n // Unchanged single-occurrence path — exact same output as before\n // this fix (task requirement).\n const raw = frames[0]\n const frame = raw ? toFrame(raw) : undefined\n const fit = buildFit(group[0]!.fit, frame?.aspect)\n items.push({\n page,\n asset_id: assetId,\n alt: altOf(assetId),\n kind: \"image\",\n missing: isMissing(assetId),\n rendered: frame !== undefined,\n frame,\n suggested_pixels: frame ? { w: frame.w * 2, h: frame.h * 2 } : undefined,\n fit,\n palette,\n mood,\n suggested_prompt: buildPrompt(mood, palette, frame, fit),\n })\n continue\n }\n\n // Shared asset_id, >1 occurrence on this page — see this function's\n // own doc comment (\"Shared-asset_id handling\") for why per-component\n // attribution is skipped rather than guessed. `fit` mode is read off\n // the group's first component; if occurrences disagree on `fit` that\n // choice is a documented simplification, not a claim about which\n // occurrence any one frame belongs to.\n const occurrenceCount = group.length\n const sharedFit = group[0]!.fit\n const missing = isMissing(assetId)\n for (const raw of frames) {\n const frame = toFrame(raw)\n const fit = buildFit(sharedFit, frame.aspect)\n items.push({\n page,\n asset_id: assetId,\n alt: altOf(assetId),\n kind: \"image\",\n missing,\n rendered: true,\n frame,\n suggested_pixels: { w: frame.w * 2, h: frame.h * 2 },\n fit,\n palette,\n mood,\n shared: true,\n occurrenceCount,\n suggested_prompt: buildPrompt(mood, palette, frame, fit),\n })\n }\n // Fewer real frames than occurrences: pad with `rendered: false`\n // shared items so the full occurrenceCount stays visible rather than\n // silently under-reporting how many components reference this id.\n for (let i = frames.length; i < occurrenceCount; i++) {\n const fit = buildFit(sharedFit, undefined)\n items.push({\n page,\n asset_id: assetId,\n alt: altOf(assetId),\n kind: \"image\",\n missing,\n rendered: false,\n fit,\n palette,\n mood,\n shared: true,\n occurrenceCount,\n suggested_prompt: buildPrompt(mood, palette, undefined, fit),\n })\n }\n }\n })\n\n return { theme: ir.theme.id, items }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAO,IAAM,UAAU;;;ACAvB,SAAS,SAAS;AA+BX,IAAM,iBAAiB,EAC3B,OAAO;AAAA,EACN,SAAS,EAAE,QAAQ,GAAG,EAAE,QAAQ,GAAG;AAAA,EACnC,UAAU,EAAE,OAAO,EAAE,QAAQ,cAAc;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAM3C,UAAU,EAAE,MAAM,CAAC,EAAE,OAAO,GAAG,2BAA2B,CAAC,EAAE,SAAS;AAAA,EACtE,OAAO,YAAY,QAAQ,EAAE,IAAI,aAAa,CAAC;AAAA,EAC/C,MAAM,WAAW,QAAQ,CAAC,CAAC;AAAA,EAC3B,QAAQ,aAAa,QAAQ,EAAE,QAAQ,CAAC,EAAE,CAAC;AAAA,EAC3C,OAAO,YAAY,SAAS;AAAA;AAAA;AAAA,EAG5B,QAAQ,mBAAmB,SAAS;AAAA,EACpC,MAAM,EAAE,OAAO,EAAE,IAAI,EAAE,SAAS;AAAA,EAChC,QAAQ,EAAE,MAAM,WAAW;AAC7B,CAAC,EACA,OAAO;;;ACzCH,SAAS,wBAAwB,KAAuB;AAC7D,MAAI,OAAO,QAAQ,YAAY,QAAQ,QAAQ,MAAM,QAAQ,GAAG,EAAG,QAAO;AAC1E,QAAM,MAAM;AACZ,QAAM,YAAY,OAAO,OAAO,KAAK,QAAQ;AAC7C,QAAM,cAAc,OAAO,OAAO,KAAK,UAAU;AACjD,MAAI,aAAa,aAAa;AAC5B,UAAM,IAAI,aAAa,0DAA0D;AAAA,EACnF;AACA,MAAI,CAAC,UAAW,QAAO;AACvB,QAAM,OAAgC,EAAE,GAAG,KAAK,UAAU,IAAI,OAAO;AACrE,SAAO,KAAK;AACZ,SAAO;AACT;AASO,SAAS,wBAAwB,KAAuB;AAC7D,MAAI,OAAO,QAAQ,YAAY,QAAQ,QAAQ,MAAM,QAAQ,GAAG,EAAG,QAAO;AAC1E,QAAM,MAAM;AACZ,QAAM,QAAQ,IAAI;AAClB,MAAI,UAAU,SAAS;AACrB,WAAO,EAAE,GAAG,KAAK,OAAO,YAAY;AAAA,EACtC;AACA,MAAI,OAAO,UAAU,YAAY,UAAU,QAAQ,CAAC,MAAM,QAAQ,KAAK,GAAG;AACxE,UAAM,WAAW;AACjB,QAAI,SAAS,OAAO,SAAS;AAC3B,aAAO,EAAE,GAAG,KAAK,OAAO,EAAE,GAAG,UAAU,IAAI,YAAY,EAAE;AAAA,IAC3D;AAAA,EACF;AACA,SAAO;AACT;AAEA,SAAS,cAAc,OAAkD;AACvE,SAAO,OAAO,UAAU,YAAY,UAAU,QAAQ,CAAC,MAAM,QAAQ,KAAK;AAC5E;AAEA,SAAS,oBAAoB,OAAkD;AAC7E,SAAO,cAAc,KAAK,KAAK,MAAM,SAAS;AAChD;AAEA,SAAS,iBAAiB,YAA8B;AACtD,SAAO,MAAM,QAAQ,UAAU,KAAK,WAAW,KAAK,mBAAmB;AACzE;AAQA,SAAS,oBAAoB,MAAwB;AACnD,MAAI,CAAC,cAAc,IAAI,EAAG,QAAO;AACjC,QAAM,OAAgC,EAAE,UAAU,KAAK,SAAS;AAChE,MAAI,OAAO,OAAO,MAAM,OAAO,KAAK,KAAK,UAAU,OAAW,MAAK,UAAU,KAAK;AAClF,SAAO;AACT;AAQA,SAAS,yBAAyB,WAA6D;AAC7F,QAAM,QAAQ,UAAU;AACxB,MAAI,CAAC,MAAM,QAAQ,KAAK,GAAG;AACzB,WAAO,EAAE,MAAM,cAAc,MAAM;AAAA,EACrC;AACA,SAAO,EAAE,MAAM,cAAc,OAAO,MAAM,MAAM,GAAG,CAAC,EAAE,IAAI,mBAAmB,EAAE;AACjF;AAEA,SAAS,uBAAuB,YAAkC;AAChE,SAAO,WAAW,IAAI,CAAC,cAAe,oBAAoB,SAAS,IAAI,yBAAyB,SAAS,IAAI,SAAU;AACzH;AAgBO,SAAS,2BAA2B,KAAuB;AAChE,MAAI,CAAC,cAAc,GAAG,EAAG,QAAO;AAChC,MAAI;AACJ,QAAM,OAAO,MAA+B;AAC1C,QAAI,CAAC,KAAM,QAAO,EAAE,GAAG,IAAI;AAC3B,WAAO;AAAA,EACT;AAEA,MAAI,iBAAiB,IAAI,UAAU,GAAG;AACpC,SAAK,EAAE,aAAa,uBAAuB,IAAI,UAAuB;AAAA,EACxE;AAEA,MAAI,MAAM,QAAQ,IAAI,MAAM,GAAG;AAC7B,QAAI,gBAAgB;AACpB,UAAM,SAAS,IAAI,OAAO,IAAI,CAAC,UAAU;AACvC,UAAI,CAAC,cAAc,KAAK,KAAK,CAAC,iBAAiB,MAAM,UAAU,EAAG,QAAO;AACzE,sBAAgB;AAChB,aAAO,EAAE,GAAG,OAAO,YAAY,uBAAuB,MAAM,UAAuB,EAAE;AAAA,IACvF,CAAC;AACD,QAAI,cAAe,MAAK,EAAE,SAAS;AAAA,EACrC;AAEA,SAAO,QAAQ;AACjB;AAEA,SAAS,0BAA0B,KAA8B,KAA8D;AAC7H,MAAI,IAAI,GAAG,MAAM,iBAAkB,QAAO;AAC1C,SAAO,EAAE,GAAG,KAAK,CAAC,GAAG,GAAG,aAAa;AACvC;AAeO,SAAS,gCAAgC,KAAuB;AACrE,MAAI,CAAC,cAAc,GAAG,EAAG,QAAO;AAChC,MAAI;AACJ,QAAM,OAAO,MAA+B;AAC1C,QAAI,CAAC,KAAM,QAAO,EAAE,GAAG,IAAI;AAC3B,WAAO;AAAA,EACT;AAEA,QAAM,YAAY,0BAA0B,KAAK,QAAQ;AACzD,QAAM,WAAW,0BAA0B,aAAa,KAAK,OAAO;AACpE,MAAI,aAAa,UAAU;AACzB,UAAM,YAAY,YAAY;AAC9B,WAAO,OAAO,KAAK,GAAG,SAAS;AAAA,EACjC;AAEA,MAAI,MAAM,QAAQ,IAAI,MAAM,GAAG;AAC7B,QAAI,gBAAgB;AACpB,UAAM,SAAS,IAAI,OAAO,IAAI,CAAC,UAAU;AACvC,UAAI,CAAC,cAAc,KAAK,EAAG,QAAO;AAClC,YAAM,YAAY,0BAA0B,OAAO,QAAQ;AAC3D,UAAI,CAAC,UAAW,QAAO;AACvB,sBAAgB;AAChB,aAAO;AAAA,IACT,CAAC;AACD,QAAI,cAAe,MAAK,EAAE,SAAS;AAAA,EACrC;AAEA,MAAI,MAAM,QAAQ,IAAI,KAAK,GAAG;AAC5B,QAAI,eAAe;AACnB,UAAM,QAAQ,IAAI,MAAM,IAAI,CAAC,SAAS;AACpC,UAAI,CAAC,cAAc,IAAI,EAAG,QAAO;AACjC,YAAM,YAAY,0BAA0B,MAAM,QAAQ;AAC1D,YAAM,WAAW,0BAA0B,aAAa,MAAM,OAAO;AACrE,YAAM,YAAY,YAAY;AAC9B,UAAI,CAAC,UAAW,QAAO;AACvB,qBAAe;AACf,aAAO;AAAA,IACT,CAAC;AACD,QAAI,aAAc,MAAK,EAAE,QAAQ;AAAA,EACnC;AAEA,SAAO,QAAQ;AACjB;AAQA,IAAM,2BAA6D,EAAE,WAAW,eAAe;AAQ/F,IAAM,yBAA2D,EAAE,MAAM,SAAS,cAAc,WAAW;AAkC3G,SAAS,sBACP,UAC8C;AAC9C,MAAI,aAAa,UAAa,OAAO,aAAa,SAAU,QAAO;AAEnE,QAAM,YAAqC,CAAC;AAC5C,aAAW,CAAC,KAAK,KAAK,KAAK,OAAO,QAAQ,QAAQ,GAAG;AACnD,QAAI,QAAQ,QAAQ;AAClB,gBAAU,WAAW,OAAO,UAAU,WAAY,yBAAyB,KAAK,KAAK,QAAS;AAAA,IAChG,WAAW,QAAQ,YAAY;AAC7B,gBAAU,SAAS,OAAO,UAAU,WAAY,uBAAuB,KAAK,KAAK,QAAS;AAAA,IAC5F,WAAW,QAAQ,YAAY;AAC7B,gBAAU,WAAW;AAAA,IACvB,OAAO;AAIL,gBAAU,GAAG,IAAI;AAAA,IACnB;AAAA,EACF;AACA,SAAO;AACT;AAsBO,SAAS,gBAAgB,IAAsB;AACpD,QAAM,YAAY,sBAAsB,GAAG,QAAwD;AACnG,QAAM,KAAK;AAAA,IACT,SAAS;AAAA,IACT,UAAU,GAAG;AAAA,IACb,GAAI,cAAc,SAAY,EAAE,UAAU,IAAI,CAAC;AAAA,IAC/C,OAAO,GAAG;AAAA,IACV,MAAM,GAAG;AAAA,IACT,QAAQ,GAAG;AAAA,IACX,GAAI,GAAG,UAAU,SAAY,EAAE,OAAO,GAAG,MAAM,IAAI,CAAC;AAAA,IACpD,GAAI,GAAG,WAAW,SAAY,EAAE,QAAQ,GAAG,OAAO,IAAI,CAAC;AAAA,IACvD,GAAI,GAAG,SAAS,SAAY,EAAE,MAAM,GAAG,KAAK,IAAI,CAAC;AAAA,IACjD,QAAQ,GAAG;AAAA,EACb;AACA,SAAO;AAAA,IACL,2BAA2B,wBAAwB,wBAAwB,EAAE,CAAC,CAAC;AAAA,EACjF;AACF;;;ACxPA,OAAO,WAAW;AAyClB,IAAM,eAAmC,CAAC,WAAW,WAAW,WAAW,WAAW,WAAW,SAAS;AAS1G,IAAM,gBAAgB;AACtB,IAAM,yBAAyB;AAE/B,IAAM,gBAAgB;AACtB,IAAM,cACJ;AACF,IAAM,iBACJ;AAOF,eAAe,cAAc,KAA4C;AACvE,QAAM,aAAa,OAAO,KAAK,IAAI,KAAK,EACrC,OAAO,CAAC,SAAS,CAAC,IAAI,MAAM,IAAI,EAAG,OAAO,cAAc,KAAK,IAAI,KAAK,CAAC,KAAK,SAAS,sBAAsB,CAAC,EAC5G,KAAK,CAAC,GAAG,MAAM,EAAE,SAAS,EAAE,MAAM;AACrC,QAAM,OAAO,WAAW,CAAC;AACzB,MAAI,SAAS,OAAW,QAAO;AAC/B,QAAM,MAAM,MAAM,IAAI,MAAM,IAAI,EAAG,MAAM,QAAQ;AACjD,SAAO,EAAE,MAAM,IAAI;AACrB;AAEA,SAAS,YAAY,KAAkE;AACrF,QAAM,QAAQ,cAAc,KAAK,GAAG;AACpC,MAAI,CAAC,MAAO,QAAO,EAAE,YAAY,QAAW,OAAO,CAAC,EAAE;AACtD,QAAM,QAAkB,CAAC;AACzB,aAAW,KAAK,MAAM,CAAC,EAAG,SAAS,WAAW,GAAG;AAC/C,UAAM,OAAO,EAAE,CAAC;AAChB,UAAM,MAAM,EAAE,CAAC,KAAK,EAAE,CAAC;AACvB,QAAI,IAAK,OAAM,IAAI,IAAI,IAAI,IAAI,YAAY,CAAC;AAAA,EAC9C;AACA,SAAO,EAAE,YAAY,MAAM,CAAC,GAAG,KAAK,KAAK,QAAW,MAAM;AAC5D;AAEA,SAAS,WAAW,KAAuE;AACzF,QAAM,IAAI,eAAe,KAAK,GAAG;AACjC,MAAI,CAAC,EAAG,QAAO,EAAE,OAAO,QAAW,OAAO,OAAU;AACpD,SAAO,EAAE,OAAO,EAAE,CAAC,GAAG,KAAK,KAAK,QAAW,OAAO,EAAE,CAAC,GAAG,KAAK,KAAK,OAAU;AAC9E;AAOA,SAAS,OAAO,KAAsB;AACpC,SAAO,cAAc,KAAK,SAAS,IAAI,cAAc,KAAK,SAAS;AACrE;AAQA,SAAS,cAAc,KAAa,KAA2C;AAC7E,SAAO,OAAO,GAAG,MAAM,OAAO,GAAG,IAC7B,EAAE,IAAI,KAAK,MAAM,IAAI,IACrB,OAAO,GAAG,IACR,EAAE,IAAI,KAAK,MAAM,IAAI,IACrB,EAAE,IAAI,KAAK,MAAM,IAAI;AAC7B;AAMA,IAAM,cAAc;AAMpB,IAAM,qBAAqB;AAkBpB,SAAS,YAAY,MAAc,IAAY,SAAyB;AAC7E,WAAS,OAAO,aAAa,QAAQ,GAAG,QAAQ;AAC9C,UAAM,IAAI,OAAO;AACjB,UAAM,YAAY,OAAO,MAAM,IAAI,CAAC;AACpC,QAAI,cAAc,WAAW,EAAE,KAAK,sBAAsB,cAAc,WAAW,OAAO,KAAK,oBAAoB;AACjH,aAAO;AAAA,IACT;AAAA,EACF;AACA,SAAO;AACT;AAaA,IAAM,cAAc;AAAA,EAClB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAEA,SAAS,YAAY,MAAuB;AAC1C,QAAM,IAAI,KAAK,YAAY;AAC3B,SAAO,YAAY,KAAK,CAAC,SAAS,EAAE,SAAS,IAAI,CAAC;AACpD;AAQA,SAAS,eAAe,WAAyC;AAC/D,MAAI,cAAc,OAAW,QAAO,CAAC,WAAW,mBAAmB,YAAY;AAC/E,SAAO,YAAY,SAAS,IACxB,CAAC,WAAW,WAAW,uBAAuB,OAAO,IACrD,CAAC,WAAW,WAAW,mBAAmB,YAAY;AAC5D;AAWO,SAAS,QAAQ,OAAe,WAAW,SAAiB;AACjE,QAAM,OAAO,MACV,YAAY,EACZ,QAAQ,eAAe,GAAG,EAC1B,QAAQ,YAAY,EAAE;AACzB,SAAO,QAAQ;AACjB;AA2BA,eAAsB,kBACpB,OACA,OAAiC,CAAC,GACT;AACzB,QAAM,MAAM,MAAM,MAAM,UAAU,KAAK;AACvC,QAAM,OAAO,MAAM,cAAc,GAAG;AACpC,MAAI,CAAC,MAAM;AACT,UAAM,IAAI;AAAA,MACR;AAAA,IACF;AAAA,EACF;AACA,QAAM,EAAE,YAAY,MAAM,IAAI,YAAY,KAAK,GAAG;AAClD,MAAI,CAAC,MAAM,OAAO,CAAC,MAAM,KAAK;AAC5B,UAAM,IAAI,aAAa,cAAc,KAAK,IAAI,gEAA2D;AAAA,EAC3G;AACA,QAAM,eAAe,aAAa,IAAI,CAAC,SAAS,MAAM,IAAI,CAAC,EAAE,OAAO,CAAC,MAAmB,MAAM,MAAS;AACvG,MAAI,aAAa,WAAW,GAAG;AAC7B,UAAM,IAAI,aAAa,cAAc,KAAK,IAAI,6EAAwE;AAAA,EACxH;AAEA,QAAM,EAAE,IAAI,KAAK,IAAI,cAAc,MAAM,KAAK,MAAM,GAAG;AACvD,QAAM,UAAU,MAAM,OAAO;AAC7B,QAAM,UAAU,MAAM,WAAW,MAAM,OAAO,aAAa,CAAC;AAC5D,QAAM,SAAS,MAAM,WAAW;AAChC,QAAM,QAAQ,YAAY,MAAM,IAAI,OAAO;AAE3C,QAAM,QAAQ,WAAW,KAAK,GAAG;AACjC,QAAM,UAAU,eAAe,MAAM,KAAK;AAC1C,QAAM,OAAO,eAAe,MAAM,SAAS,MAAM,KAAK;AAEtD,QAAM,QAAQ,KAAK,SAAS,cAAc;AAC1C,QAAM,KAAK,KAAK,MAAM,QAAQ,KAAK,SAAS,cAAc,OAAO;AAEjE,QAAM,QAAqB;AAAA,IACzB;AAAA,IACA,QAAQ,EAAE,IAAI,SAAS,SAAS,QAAQ,MAAM,OAAO,aAAa;AAAA,IAClE,OAAO,EAAE,SAAS,KAAK;AAAA,IACvB,oBAAoB;AAAA,MAClB,OAAO,EAAE,MAAM,SAAS,OAAO,GAAG;AAAA,MAClC,SAAS,EAAE,MAAM,SAAS,OAAO,GAAG;AAAA,MACpC,SAAS,EAAE,MAAM,SAAS,OAAO,GAAG;AAAA,MACpC,QAAQ,EAAE,MAAM,SAAS,OAAO,GAAG;AAAA,IACrC;AAAA,EACF;AAEA,SAAO,EAAE,IAAI,OAAO,OAAO,OAAO,CAAC,GAAG,MAAM,CAAC,EAAE;AACjD;;;AC5UA,SAAS,KAAAA,UAAS;AAelB,IAAM,WAAWC,GAAE,OAAO,EAAE,MAAM,uBAAuB,mCAAmC;AAE5F,IAAM,2BAA2BA,GAAE,mBAAmB,QAAQ;AAAA,EAC5DA,GAAE,OAAO,EAAE,MAAMA,GAAE,QAAQ,OAAO,GAAG,OAAO,SAAS,CAAC,EAAE,OAAO;AAAA,EAC/DA,GACG,OAAO;AAAA,IACN,MAAMA,GAAE,QAAQ,UAAU;AAAA,IAC1B,MAAM;AAAA,IACN,IAAI;AAAA,IACJ,WAAWA,GAAE,KAAK,CAAC,MAAM,MAAM,UAAU,CAAC,EAAE,SAAS;AAAA,EACvD,CAAC,EACA,OAAO;AAAA,EACVA,GACG,OAAO;AAAA,IACN,MAAMA,GAAE,QAAQ,OAAO;AAAA,IACvB,UAAUA,GAAE,OAAO;AAAA,IACnB,SAASA,GAAE,OAAO,EAAE,OAAO,UAAU,SAASA,GAAE,OAAO,EAAE,IAAI,CAAC,EAAE,IAAI,CAAC,EAAE,CAAC,EAAE,OAAO,EAAE,SAAS;AAAA,IAC5F,KAAKA,GAAE,KAAK,CAAC,SAAS,SAAS,CAAC,EAAE,SAAS;AAAA,EAC7C,CAAC,EACA,OAAO;AACZ,CAAC;AAED,IAAM,wBAAwBA,GAC3B,OAAO;AAAA,EACN,IAAIA,GAAE,OAAO,EAAE,IAAI,CAAC;AAAA,EACpB,uBAAuBA,GAAE,QAAQ,EAAE,SAAS;AAAA,EAC5C,QAAQA,GACL,OAAO;AAAA,IACN,IAAI;AAAA,IACJ,SAAS;AAAA,IACT,OAAO,SAAS,SAAS;AAAA,IACzB,SAAS;AAAA,IACT,QAAQ;AAAA,IACR,MAAM;AAAA,IACN,OAAO;AAAA,IACP,QAAQ,SAAS,SAAS;AAAA,IAC1B,cAAcA,GAAE,MAAM,QAAQ,EAAE,IAAI,CAAC;AAAA,IACrC,YAAYA,GAAE,MAAM,QAAQ,EAAE,IAAI,CAAC,EAAE,SAAS;AAAA,IAC9C,YAAY,SAAS,SAAS;AAAA,EAChC,CAAC,EACA,OAAO;AAAA,EACV,OAAOA,GACJ,OAAO;AAAA,IACN,SAASA,GAAE,MAAMA,GAAE,OAAO,CAAC,EAAE,IAAI,CAAC;AAAA,IAClC,MAAMA,GAAE,MAAMA,GAAE,OAAO,CAAC,EAAE,IAAI,CAAC;AAAA,IAC/B,MAAMA,GAAE,MAAMA,GAAE,OAAO,CAAC,EAAE,IAAI,CAAC,EAAE,SAAS;AAAA,EAC5C,CAAC,EACA,OAAO;AAAA,EACV,OAAOA,GACJ,OAAO;AAAA,IACN,QAAQA,GAAE,OAAO,EAAE,IAAI,CAAC,EAAE,IAAI,EAAE,EAAE,SAAS;AAAA,IAC3C,UAAUA,GAAE,OAAO,EAAE,IAAI,GAAG,EAAE,IAAI,GAAG,EAAE,SAAS;AAAA,IAChD,WAAWA,GAAE,OAAO,EAAE,IAAI,GAAG,EAAE,IAAI,CAAC,EAAE,SAAS;AAAA,EACjD,CAAC,EACA,OAAO,EACP,SAAS;AAAA,EACZ,oBAAoBA,GACjB,OAAO;AAAA,IACN,OAAO;AAAA,IACP,SAAS;AAAA,IACT,SAAS;AAAA,IACT,QAAQ;AAAA,EACV,CAAC,EACA,OAAO;AACZ,CAAC,EACA,OAAO;AAEH,IAAM,uBAAuBA,GACjC,OAAO;AAAA,EACN,IAAIA,GAAE,OAAO,EAAE,IAAI,CAAC;AAAA,EACpB,OAAOA,GAAE,OAAO,EAAE,SAAS;AAAA,EAC3B,OAAO;AAAA,EACP,OAAO,kBAAkB,SAAS;AAAA,EAClC,MAAMA,GAAE,MAAMA,GAAE,OAAO,CAAC,EAAE,SAAS;AACrC,CAAC,EACA,OAAO;AAQH,SAAS,oBAAoB,KAAc,QAAgC;AAChF,QAAM,IAAI,qBAAqB,UAAU,GAAG;AAC5C,MAAI,CAAC,EAAE,SAAS;AACd,UAAM,SAAS,EAAE,MAAM,OAAO,IAAI,CAAC,MAAM,GAAG,EAAE,KAAK,KAAK,GAAG,KAAK,QAAQ,KAAK,EAAE,OAAO,EAAE,EAAE,KAAK,IAAI;AACnG,UAAM,IAAI,aAAa,sBAAsB,MAAM;AAAA,EAAM,MAAM,EAAE;AAAA,EACnE;AAOA,SAAO,EAAE;AACX;AA2BO,SAAS,uBAAuB,MAA8B;AACnE,MAAK,oBAA0C,SAAS,KAAK,EAAE,GAAG;AAChE,UAAM,IAAI;AAAA,MACR,kBAAkB,KAAK,EAAE;AAAA,IAC3B;AAAA,EACF;AACA,MAAI,CAAC,qBAAqB,EAAE,SAAS,KAAK,EAAE,GAAG;AAC7C,kBAAc,EAAE,IAAI,KAAK,IAAI,OAAO,KAAK,OAAO,OAAO,KAAK,SAAS,CAAC,GAAG,MAAM,KAAK,QAAQ,CAAC,EAAE,CAAC;AAAA,EAClG;AACA,SAAO,KAAK;AACd;;;ACpIA,SAAS,KAAAC,UAAS;AA0BlB,IAAM,aAAa,CAAC,SAAS,WAAW,WAAW,QAAQ;AAcpD,IAAM,iBAAiBC,GAC3B,OAAO;AAAA,EACN,IAAIA,GAAE,OAAO;AAAA,EACb,MAAMA,GAAE,KAAK,UAAU;AAAA,EACvB,SAASA,GAAE,OAAO;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAkBlB,MAAMA,GAAE,KAAK,WAAW,EAAE,SAAS;AAAA;AAAA;AAAA,EAGnC,OAAOA,GAAE,OAAO,EAAE,SAAS;AAAA;AAAA;AAAA;AAAA;AAAA,EAK3B,SAASA,GAAE,OAAO,EAAE,SAAS;AAC/B,CAAC,EACA,OAAO;AAqBH,IAAM,iBAAiBA,GAC3B,OAAO;AAAA,EACN,SAASA,GAAE,QAAQ,GAAG,EAAE,QAAQ,GAAG;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EASnC,WAAWA,GAAE,MAAM,CAACA,GAAE,OAAO,GAAG,2BAA2B,CAAC,EAAE,SAAS;AAAA,EACvE,OAAOA,GAAE,OAAO,EAAE,SAAS;AAAA,EAC3B,UAAUA,GAAE,OAAO,EAAE,SAAS;AAAA,EAC9B,MAAMA,GAAE,OAAO,EAAE,IAAI,EAAE,SAAS;AAAA,EAChC,MAAM,WAAW,QAAQ,CAAC,CAAC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQ3B,OAAO,YAAY,SAAS;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAW5B,UAAU,mBAAmB,SAAS;AAAA,EACtC,OAAOA,GAAE,MAAM,cAAc;AAC/B,CAAC,EACA,OAAO;AAKH,SAAS,iBAA0C;AACxD,SAAOA,GAAE,aAAa,cAAc;AACtC;AAkCO,SAAS,iBAAiB,QAAuC;AACtE,SAAO,OAAO,IAAI,CAAC,MAAO,EAAE,SAAS,SAAS,EAAE,MAAM,YAAO,EAAE,IAAI,KAAK,EAAE,OAAO,KAAK,GAAG,EAAE,IAAI,KAAK,EAAE,OAAO,EAAG,EAAE,KAAK,IAAI;AAC7H;AAUO,SAAS,uBAAuB,QAAuC;AAC5E,SAAO,iBAAiB,OAAO,MAAM,SAAS,OAAO,WAAW,IAAI,KAAK,GAAG;AAAA,EAAO,iBAAiB,MAAM,CAAC;AAC7G;AASO,SAAS,mBAAmB,MAAwB;AACzD,SAAO,KAAK,SAAS;AACvB;AAIA,SAAS,mBAAmB,MAAuC;AACjE,MAAI,KAAK,MAAM,SAAS,EAAG,QAAO,CAAC;AACnC,SAAO,CAAC,EAAE,MAAM,SAAS,SAAS,iFAA4E,CAAC;AACjH;AAeA,SAAS,mBAAmB,MAAuC;AACjE,QAAM,EAAE,MAAM,IAAI;AAClB,QAAM,SAAgC,CAAC;AACvC,QAAM,QAAQ,MAAM,CAAC;AACrB,QAAM,OAAO,MAAM,MAAM,SAAS,CAAC;AACnC,MAAI,MAAM,SAAS,SAAS;AAC1B,WAAO,KAAK;AAAA,MACV,MAAM;AAAA,MACN,QAAQ,MAAM;AAAA,MACd,SAAS,yCAAyC,MAAM,IAAI;AAAA,IAC9D,CAAC;AAAA,EACH;AACA,MAAI,KAAK,SAAS,UAAU;AAC1B,WAAO,KAAK;AAAA,MACV,MAAM,SAAS,MAAM,SAAS,CAAC;AAAA,MAC/B,QAAQ,KAAK;AAAA,MACb,SAAS,yCAAyC,KAAK,IAAI;AAAA,IAC7D,CAAC;AAAA,EACH;AACA,WAAS,IAAI,GAAG,IAAI,MAAM,SAAS,GAAG,KAAK;AACzC,UAAM,OAAO,MAAM,CAAC;AACpB,QAAI,KAAK,SAAS,WAAW,KAAK,SAAS,UAAU;AACnD,aAAO,KAAK;AAAA,QACV,MAAM,SAAS,CAAC;AAAA,QAChB,QAAQ,KAAK;AAAA,QACb,SAAS,SAAS,KAAK,EAAE,cAAc,KAAK,IAAI;AAAA,MAClD,CAAC;AAAA,IACH;AAAA,EACF;AACA,SAAO;AACT;AA+BA,SAAS,eAAe,IAAqB;AAC3C,SAAO,GAAG,SAAS,GAAG,KAAK,GAAG,SAAS,IAAI,KAAK,OAAO;AACzD;AAcA,SAAS,aAAa,MAAuC;AAC3D,QAAM,SAAgC,CAAC;AACvC,QAAM,OAAO,oBAAI,IAAsB;AACvC,OAAK,MAAM,QAAQ,CAAC,MAAM,MAAM;AAC9B,QAAI,KAAK,GAAG,KAAK,MAAM,IAAI;AACzB,aAAO,KAAK,EAAE,MAAM,SAAS,CAAC,OAAO,SAAS,QAAQ,IAAI,CAAC,kEAA6D,CAAC;AACzH;AAAA,IACF;AACA,QAAI,eAAe,KAAK,EAAE,GAAG;AAC3B,aAAO,KAAK;AAAA,QACV,MAAM,SAAS,CAAC;AAAA,QAChB,QAAQ,KAAK;AAAA,QACb,SAAS,YAAY,KAAK,EAAE;AAAA,MAC9B,CAAC;AACD;AAAA,IACF;AACA,UAAM,UAAU,KAAK,IAAI,KAAK,EAAE;AAChC,QAAI,QAAS,SAAQ,KAAK,CAAC;AAAA,QACtB,MAAK,IAAI,KAAK,IAAI,CAAC,CAAC,CAAC;AAAA,EAC5B,CAAC;AACD,aAAW,CAAC,IAAI,OAAO,KAAK,MAAM;AAChC,QAAI,QAAQ,SAAS,EAAG;AACxB,WAAO,KAAK;AAAA,MACV,MAAM;AAAA,MACN,QAAQ;AAAA,MACR,SAAS,sBAAsB,EAAE,aAAa,QAAQ,MAAM,qBAAqB,QAAQ,IAAI,CAAC,MAAM,IAAI,CAAC,EAAE,KAAK,IAAI,CAAC;AAAA,IACvH,CAAC;AAAA,EACH;AACA,SAAO;AACT;AASA,IAAM,oBAAoB,SAAS;AAenC,SAAS,kBAAkB,SAAyB;AAClD,SAAO,QAAQ;AACjB;AAEA,SAAS,cAAc,MAAuC;AAC5D,QAAM,SAAgC,CAAC;AACvC,OAAK,MAAM,QAAQ,CAAC,MAAM,MAAM;AAC9B,QAAI,KAAK,QAAQ,KAAK,MAAM,IAAI;AAC9B,aAAO,KAAK,EAAE,MAAM,SAAS,CAAC,YAAY,QAAQ,KAAK,IAAI,SAAS,SAAS,KAAK,EAAE,kCAAkC,CAAC;AACvH;AAAA,IACF;AACA,UAAM,SAAS,kBAAkB,KAAK,OAAO;AAC7C,QAAI,SAAS,mBAAmB;AAC9B,aAAO,KAAK;AAAA,QACV,MAAM,SAAS,CAAC;AAAA,QAChB,QAAQ,KAAK;AAAA,QACb,SAAS,SAAS,KAAK,EAAE,gBAAgB,MAAM,4BAA4B,iBAAiB;AAAA,MAC9F,CAAC;AAAA,IACH;AAAA,EACF,CAAC;AACD,SAAO;AACT;AASA,SAAS,WAAW,MAAuC;AACzD,QAAM,UAAU,mBAAmB,IAAI;AACvC,QAAM,YAAY,qBAAqB;AACvC,MAAI,UAAU,SAAS,OAAO,EAAG,QAAO,CAAC;AACzC,QAAM,UACJ,YAAY,UACR,+GACA,kBAAkB,OAAO,uBAAkB,UAAU,KAAK,IAAI,CAAC;AACrE,SAAO,CAAC,EAAE,MAAM,SAAS,QAAQ,CAAC;AACpC;AAIA,IAAM,aAAgC,OAAO,KAAK,eAAe;AAqBjE,SAAS,qBAAqB,MAAgB,UAA2C;AACvF,QAAM,aAAa,qBAAqB,QAAQ,EAAE;AAClD,QAAM,SAAgC,CAAC;AACvC,OAAK,MAAM,QAAQ,CAAC,MAAM,MAAM;AAC9B,QAAI,KAAK,UAAU,OAAW;AAC9B,QAAI,KAAK,UAAU,aAAa;AAC9B,aAAO,KAAK;AAAA,QACV,MAAM,SAAS,CAAC;AAAA,QAChB,QAAQ,KAAK;AAAA,QACb,SACE;AAAA,MACJ,CAAC;AACD;AAAA,IACF;AACA,QAAI,KAAK,UAAU,kBAAkB;AACnC,aAAO,KAAK;AAAA,QACV,MAAM,SAAS,CAAC;AAAA,QAChB,QAAQ,KAAK;AAAA,QACb,SACE;AAAA,MACJ,CAAC;AACD;AAAA,IACF;AACA,QAAI,WAAW,SAAS,KAAK,KAAK,KAAK,gBAAgB,SAAS,KAAK,KAAK,KAAK,WAAW,SAAS,KAAK,KAAK,GAAG;AAC9G;AAAA,IACF;AACA,WAAO,KAAK;AAAA,MACV,MAAM,SAAS,CAAC;AAAA,MAChB,QAAQ,KAAK;AAAA,MACb,SACE,kBAAkB,KAAK,KAAK,mBAAmB,QAAQ,wDACnD,WAAW,KAAK,IAAI,CAAC,wBAAwB,gBAAgB,KAAK,IAAI,CAAC,sBACxD,WAAW,KAAK,IAAI,CAAC;AAAA,IAC5C,CAAC;AAAA,EACH,CAAC;AACD,SAAO;AACT;AAaA,SAAS,yBAAyB,MAAoC;AACpE,QAAM,SAA6B,CAAC;AACpC,OAAK,MAAM,QAAQ,CAAC,MAAM,UAAU;AAClC,QAAI,KAAK,SAAS,aAAa,KAAK,SAAS,QAAW;AACtD,aAAO,KAAK,EAAE,OAAO,IAAI,KAAK,IAAI,MAAM,KAAK,KAAK,CAAC;AAAA,IACrD;AAAA,EACF,CAAC;AACD,SAAO;AACT;AAmBA,SAAS,qBAAqB,MAAgB,UAA2C;AACvF,QAAM,MAAM,yBAAyB,IAAI;AACzC,QAAM,SAAgC,CAAC;AACvC,MAAI,IAAI;AACR,SAAO,IAAI,IAAI,QAAQ;AACrB,QAAI,IAAI,IAAI;AACZ,WAAO,IAAI,IAAI,UAAU,IAAI,CAAC,EAAG,SAAS,IAAI,CAAC,EAAG,KAAM;AACxD,UAAM,YAAY,IAAI;AACtB,QAAI,aAAa,GAAG;AAClB,YAAM,UAAU,IAAI,MAAM,GAAG,CAAC;AAC9B,aAAO,KAAK;AAAA,QACV,MAAM;AAAA,QACN,QAAQ,QAAQ,CAAC,EAAG;AAAA,QACpB,SACE,GAAG,SAAS,4CAA4C,IAAI,CAAC,EAAG,IAAI,MAChE,QAAQ,IAAI,CAAC,MAAM,EAAE,EAAE,EAAE,KAAK,IAAI,CAAC,sBAAiB,QAAQ;AAAA,MAEpE,CAAC;AAAA,IACH;AACA,QAAI;AAAA,EACN;AACA,SAAO;AACT;AAWA,SAAS,sBAAsB,MAAgB,UAA2C;AACxF,QAAM,oBAAoB,KAAK,MAAM,UAAU,CAAC,SAAS,KAAK,SAAS,SAAS;AAChF,MAAI,sBAAsB,GAAI,QAAO,CAAC;AACtC,QAAM,eAAe,KAAK,MAAM,iBAAiB;AACjD,MAAI,aAAa,SAAS,UAAa,aAAa,SAAS,SAAU,QAAO,CAAC;AAC/E,SAAO;AAAA,IACL;AAAA,MACE,MAAM,SAAS,iBAAiB;AAAA,MAChC,QAAQ,aAAa;AAAA,MACrB,SAAS,qCAAqC,aAAa,IAAI,sBAAiB,QAAQ;AAAA,IAC1F;AAAA,EACF;AACF;AAeA,SAAS,wBAAwB,MAAgB,UAA2C;AAC1F,QAAM,WAAW,yBAAyB,IAAI;AAC9C,MAAI,SAAS,WAAW,EAAG,QAAO,CAAC;AACnC,QAAM,cAAc,SAAS,OAAO,CAAC,SAAS,KAAK,SAAS,QAAQ;AACpE,MAAI,YAAY,SAAS,SAAS,UAAU,IAAK,QAAO,CAAC;AACzD,QAAM,MAAM,KAAK,MAAO,YAAY,SAAS,SAAS,SAAU,GAAG;AACnE,SAAO;AAAA,IACL;AAAA,MACE,MAAM;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,MAMN,QAAQ,YAAY,CAAC,EAAG;AAAA,MACxB,SACE,GAAG,YAAY,MAAM,OAAO,SAAS,MAAM,qDACvC,GAAG,MAAM,YAAY,IAAI,CAAC,SAAS,KAAK,EAAE,EAAE,KAAK,IAAI,CAAC,sBAAiB,QAAQ;AAAA,IAEvF;AAAA,EACF;AACF;AAWA,SAAS,kBAAkB,MAAgB,UAA2C;AACpF,QAAM,SAAS,qBAAqB,QAAQ,EAAE;AAC9C,UAAQ,QAAQ;AAAA,IACd,KAAK;AAAA,IACL,KAAK;AAKH,aAAO,CAAC;AAAA,IACV,KAAK;AACH,aAAO,qBAAqB,MAAM,QAAQ;AAAA,IAC5C,KAAK;AACH,aAAO,sBAAsB,MAAM,QAAQ;AAAA,IAC7C,KAAK;AACH,aAAO,wBAAwB,MAAM,QAAQ;AAAA,IAC/C,SAAS;AACP,YAAM,aAAoB;AAC1B,YAAM,IAAI,MAAM,0BAA0B,OAAO,UAAU,CAAC,EAAE;AAAA,IAChE;AAAA,EACF;AACF;AAgBO,IAAM,wBAAsE;AAAA,EACjF,OAAO,EAAE,KAAK,GAAG,KAAK,GAAG;AAAA,EACzB,UAAU,EAAE,KAAK,GAAG,KAAK,GAAG;AAAA,EAC5B,UAAU,EAAE,KAAK,GAAG,KAAK,GAAG;AAC9B;AAEA,SAAS,eAAe,MAAgB,QAAuC;AAC7E,QAAM,EAAE,KAAK,IAAI,IAAI,sBAAsB,MAAM;AACjD,QAAM,IAAI,KAAK,MAAM;AACrB,MAAI,KAAK,OAAO,KAAK,IAAK,QAAO,CAAC;AAClC,SAAO;AAAA,IACL;AAAA,MACE,MAAM;AAAA,MACN,SAAS,YAAY,CAAC,kBAAa,MAAM,oBAAoB,GAAG,IAAI,GAAG;AAAA,IACzE;AAAA,EACF;AACF;AAOA,SAAS,mBAAmB,OAAgB,OAAmC;AAC7E,MAAI,OAAO,UAAU,YAAY,UAAU,KAAM,QAAO;AACxD,QAAM,QAAS,MAAkC;AACjD,MAAI,CAAC,MAAM,QAAQ,KAAK,EAAG,QAAO;AAClC,QAAM,OAAO,MAAM,KAAK;AACxB,MAAI,OAAO,SAAS,YAAY,SAAS,KAAM,QAAO;AACtD,QAAM,KAAM,KAAiC;AAC7C,SAAO,OAAO,OAAO,WAAW,KAAK;AACvC;AA6BO,SAAS,aAAa,OAAoC;AAC/D,QAAM,gBAAgB,yBAAyB,KAAK;AACpD,QAAM,qBAAqB,wBAAwB,cAAc,KAAK;AACtE,QAAM,kBAAkB,mBAAmB;AAC3C,QAAM,aAAa,CAAC,GAAG,cAAc,YAAY,GAAG,mBAAmB,UAAU;AACjF,QAAM,iBAAiB,CAAC,WACtB,WAAW,SAAS,IAAI,EAAE,GAAG,QAAQ,WAAW,IAAI;AAEtD,QAAM,IAAI,eAAe,UAAU,eAAe;AAClD,MAAI,CAAC,EAAE,SAAS;AACd,UAAM,SAAS,EAAE,MAAM,OAAO,IAAI,CAAC,UAAU;AAC3C,YAAM,OAAO,MAAM,KAAK,KAAK,GAAG;AAChC,YAAM,IAAI,gBAAgB,KAAK,IAAI;AACnC,aAAO,EAAE,MAAM,SAAS,MAAM,SAAS,QAAQ,IAAI,mBAAmB,iBAAiB,OAAO,EAAE,CAAC,CAAC,CAAC,IAAI,OAAU;AAAA,IACnH,CAAC;AACD,WAAO,eAAe,EAAE,IAAI,OAAO,OAAO,CAAC;AAAA,EAC7C;AACA,QAAM,OAAO,EAAE;AAEf,QAAM,cAAc,mBAAmB,IAAI;AAC3C,MAAI,YAAY,SAAS,EAAG,QAAO,eAAe,EAAE,IAAI,OAAO,QAAQ,YAAY,CAAC;AAEpF,QAAM,iBAAiB,mBAAmB,IAAI;AAC9C,MAAI,eAAe,SAAS,EAAG,QAAO,eAAe,EAAE,IAAI,OAAO,QAAQ,eAAe,CAAC;AAE1F,QAAM,WAAW,aAAa,IAAI;AAClC,MAAI,SAAS,SAAS,EAAG,QAAO,eAAe,EAAE,IAAI,OAAO,QAAQ,SAAS,CAAC;AAE9E,QAAM,gBAAgB,cAAc,IAAI;AACxC,MAAI,cAAc,SAAS,EAAG,QAAO,eAAe,EAAE,IAAI,OAAO,QAAQ,cAAc,CAAC;AAExF,QAAM,cAAc,WAAW,IAAI;AACnC,MAAI,YAAY,SAAS,EAAG,QAAO,eAAe,EAAE,IAAI,OAAO,QAAQ,YAAY,CAAC;AAcpF,MAAI;AACJ,MAAI;AACF,mBAAe,iBAAiB,KAAK,SAA2D;AAAA,EAClG,SAAS,KAAK;AACZ,QAAI,EAAE,eAAe,cAAe,OAAM;AAC1C,WAAO,eAAe,EAAE,IAAI,OAAO,QAAQ,CAAC,EAAE,MAAM,aAAa,SAAS,IAAI,QAAQ,CAAC,EAAE,CAAC;AAAA,EAC5F;AAEA,QAAM,aAAa,kBAAkB,MAAM,aAAa,QAAQ;AAChE,MAAI,WAAW,SAAS,EAAG,QAAO,eAAe,EAAE,IAAI,OAAO,QAAQ,WAAW,CAAC;AAElF,QAAM,cAAc,qBAAqB,MAAM,aAAa,QAAQ;AACpE,MAAI,YAAY,SAAS,EAAG,QAAO,eAAe,EAAE,IAAI,OAAO,QAAQ,YAAY,CAAC;AAEpF,QAAM,kBAAkB,eAAe,MAAM,aAAa,MAAM;AAChE,MAAI,gBAAgB,SAAS,EAAG,QAAO,eAAe,EAAE,IAAI,OAAO,QAAQ,gBAAgB,CAAC;AAE5F,SAAO,eAAe,EAAE,IAAI,MAAM,MAAM,QAAQ,CAAC,EAAE,CAAC;AACtD;;;AC3oBA,SAAS,WAAW,GAAmB;AACrC,MAAI,IAAI;AACR,WAAS,IAAI,GAAG,IAAI,EAAE,QAAQ,IAAK,MAAM,KAAK,KAAK,IAAI,EAAE,WAAW,CAAC,IAAK;AAC1E,SAAO,KAAK,IAAI,CAAC;AACnB;AAoBA,SAAS,aAAa,UAA8B,SAAoC;AACtF,SAAO,WAAW,CAAC,YAAY,IAAI,GAAG,OAAO,EAAE,KAAK,IAAI,CAAC;AAC3D;AAIA,IAAM,cAAc,CAAC,QAAQ,SAAS;AA8F/B,SAAS,aAAa,MAAe,OAAoD;AAE9F,QAAM,YAAY,aAAa,IAAI;AACnC,MAAI,CAAC,UAAU,IAAI;AACjB,UAAM,IAAI,aAAa,uBAAuB,UAAU,MAAM,CAAC;AAAA,EACjE;AACA,QAAM,WAAW,UAAU;AAK3B,aAAW,QAAQ,SAAS,OAAO;AACjC,UAAM,MAAM,MAAM,KAAK,EAAE;AACzB,QAAI,QAAQ,OAAW;AACvB,QAAI,OAAO,QAAQ,YAAY,QAAQ,QAAQ,MAAM,QAAQ,GAAG,GAAG;AACjE,YAAM,IAAI,aAAa,SAAS,KAAK,EAAE,mCAAmC;AAAA,IAC5E;AACA,eAAW,OAAO,aAAa;AAC7B,UAAI,OAAO,OAAO,KAAK,GAAG,GAAG;AAC3B,cAAM,IAAI,aAAa,SAAS,KAAK,EAAE,OAAO,GAAG,6DAAwD;AAAA,MAC3G;AAAA,IACF;AAAA,EACF;AAGA,QAAM,UAAU,IAAI,IAAI,SAAS,MAAM,IAAI,CAAC,SAAS,KAAK,EAAE,CAAC;AAC7D,QAAM,YAAY,OAAO,KAAK,KAAK,EAAE,OAAO,CAAC,OAAO,CAAC,QAAQ,IAAI,EAAE,CAAC;AACpE,MAAI,UAAU,SAAS,GAAG;AACxB,UAAM,IAAI;AAAA,MACR,iBAAiB,UAAU,WAAW,IAAI,KAAK,GAAG,IAAI,UAAU,IAAI,CAAC,OAAO,IAAI,EAAE,GAAG,EAAE,KAAK,IAAI,CAAC;AAAA,IACnG;AAAA,EACF;AAGA,QAAM,SAAS,SAAS,MAAM,IAAI,CAAC,SAAS,WAAW,MAAM,MAAM,KAAK,EAAE,CAAC,CAAC;AAI5E,QAAM,gBACJ,SAAS,SAAS,SAAY,aAAa,SAAS,UAAU,SAAS,MAAM,IAAI,CAAC,SAAS,KAAK,EAAE,CAAC,IAAI;AACzG,QAAM,OAAO,SAAS,QAAQ;AAG9B,QAAM,QAAQ;AAAA,IACZ,SAAS;AAAA,IACT,GAAI,SAAS,cAAc,SAAY,EAAE,WAAW,SAAS,UAAU,IAAI,CAAC;AAAA,IAC5E,GAAI,SAAS,UAAU,SAAY,EAAE,OAAO,EAAE,IAAI,SAAS,MAAM,EAAE,IAAI,CAAC;AAAA,IACxE,GAAI,SAAS,aAAa,SAAY,EAAE,UAAU,SAAS,SAAS,IAAI,CAAC;AAAA,IACzE,GAAI,SAAS,UAAU,SAAY,EAAE,OAAO,SAAS,MAAM,IAAI,CAAC;AAAA,IAChE,GAAI,SAAS,aAAa,SAAY,EAAE,UAAU,SAAS,SAAS,IAAI,CAAC;AAAA,IACzE,MAAM,SAAS;AAAA,IACf;AAAA,IACA;AAAA,EACF;AAEA,QAAM,SAAS,aAAa,UAAU,KAAK;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;AAAA,EAA6C,MAAM,EAAE;AAAA,EAC9E;AAOA,QAAM,EAAE,IAAI,kBAAkB,IAAI,4BAA4B,OAAO,IAAI;AAEzE,SAAO;AAAA,IACL;AAAA,IACA,GAAI,kBAAkB,SAAY,EAAE,cAAc,IAAI,CAAC;AAAA,IACvD,GAAI,oBAAoB,IAAI,EAAE,yBAAyB,kBAAkB,IAAI,CAAC;AAAA,EAChF;AACF;AAsDA,SAAS,4BAA4B,IAAuD;AAC1F,MAAI,oBAAoB;AACxB,QAAM,SAAS,GAAG,OAAO,IAAI,CAAC,OAAO,UAAU;AAC7C,QAAI,MAAM,WAAW,OAAW,QAAO;AACvC,UAAM,oBAAoB,yBAAyB,IAAI,OAAO,KAAK;AACnE,QAAI,sBAAsB,KAAM,QAAO;AACvC;AACA,WAAO,EAAE,GAAG,OAAO,QAAQ,kBAAkB;AAAA,EAC/C,CAAC;AACD,SAAO,sBAAsB,IAAI,EAAE,IAAI,kBAAkB,IAAI,EAAE,IAAI,EAAE,GAAG,IAAI,OAAO,GAAG,kBAAkB;AAC1G;AAGA,SAAS,WAAW,MAAgB,KAAuD;AACzF,MAAI,QAAQ,QAAW;AACrB,WAAO;AAAA,MACL,IAAI,KAAK;AAAA,MACT,MAAM,KAAK;AAAA,MACX,SAAS,KAAK;AAAA,MACd,aAAa;AAAA,MACb,GAAI,KAAK,SAAS,SAAY,EAAE,MAAM,KAAK,KAAK,IAAI,CAAC;AAAA,MACrD,GAAI,KAAK,YAAY,SAAY,EAAE,YAAY,KAAK,QAAQ,IAAI,CAAC;AAAA,IACnE;AAAA,EACF;AACA,SAAO;AAAA,IACL,IAAI,KAAK;AAAA,IACT,MAAM,KAAK;AAAA,IACX,SAAS,KAAK;AAAA,IACd,GAAI,KAAK,SAAS,SAAY,EAAE,MAAM,KAAK,KAAK,IAAI,CAAC;AAAA,IACrD,GAAI,KAAK,SAAS,aAAa,KAAK,YAAY,SAAY,EAAE,YAAY,KAAK,QAAQ,IAAI,CAAC;AAAA,IAC5F,GAAI,IAAI,eAAe,SAAY,EAAE,YAAY,IAAI,WAAW,IAAI,CAAC;AAAA,IACrE,GAAI,IAAI,WAAW,SAAY,EAAE,QAAQ,IAAI,OAAO,IAAI,CAAC;AAAA,IACzD,GAAI,IAAI,gBAAgB,SAAY,EAAE,aAAa,IAAI,YAAY,IAAI,CAAC;AAAA,IACxE,GAAI,IAAI,eAAe,SAAY,EAAE,YAAY,IAAI,WAAW,IAAI,CAAC;AAAA,IACrE,GAAI,IAAI,eAAe,SAAY,EAAE,YAAY,IAAI,WAAW,IAAI,CAAC;AAAA,IACrE,GAAI,IAAI,aAAa,SAAY,EAAE,UAAU,IAAI,SAAS,IAAI,CAAC;AAAA,IAC/D,GAAI,IAAI,UAAU,SAAY,EAAE,OAAO,IAAI,MAAM,IAAI,CAAC;AAAA,EACxD;AACF;AAoBA,IAAM,mBAAmB;AAqGlB,SAAS,gBAAgB,IAAoE;AAClG,QAAM,QAAqC,CAAC;AAC5C,QAAM,YAAwB,GAAG,OAAO,IAAI,CAAC,OAAO,UAAU;AAC5D,UAAM,KAAK,MAAM,MAAM,KAAK,QAAQ,CAAC,IAAI,MAAM,IAAI;AACnD,UAAM,UAAU,MAAM,YAAY,UAAa,MAAM,QAAQ,KAAK,MAAM,KAAK,MAAM,UAAU;AAC7F,UAAM,WAAqB;AAAA,MACzB;AAAA,MACA,MAAM,MAAM;AAAA,MACZ;AAAA,MACA,GAAI,MAAM,SAAS,SAAY,EAAE,MAAM,MAAM,KAAK,IAAI,CAAC;AAAA,MACvD,IAAK,MAAM,gBAAgB,QAAQ,MAAM,SAAS,cAAc,MAAM,eAAe,SACjF,EAAE,SAAS,MAAM,WAAW,IAC5B,CAAC;AAAA,IACP;AACA,QAAI,MAAM,gBAAgB,KAAM,OAAM,EAAE,IAAI,mBAAmB,KAAK;AACpE,WAAO;AAAA,EACT,CAAC;AAED,QAAM,OAAiB;AAAA,IACrB,SAAS;AAAA;AAAA;AAAA;AAAA,IAIT,GAAI,GAAG,cAAc,SAAY,EAAE,WAAW,GAAG,UAAU,IAAI,CAAC;AAAA,IAChE,OAAO,GAAG,MAAM;AAAA,IAChB,UAAU,GAAG;AAAA,IACb,GAAI,GAAG,SAAS,SAAY,EAAE,MAAM,GAAG,KAAK,IAAI,CAAC;AAAA,IACjD,GAAI,GAAG,UAAU,SAAY,EAAE,OAAO,GAAG,MAAM,IAAI,CAAC;AAAA,IACpD,GAAI,GAAG,aAAa,SAAY,EAAE,UAAU,GAAG,SAAS,IAAI,CAAC;AAAA,IAC7D,MAAM,GAAG;AAAA,IACT,OAAO;AAAA,EACT;AAEA,SAAO,EAAE,MAAM,MAAM;AACvB;AASA,SAAS,mBAAmB,OAA2B;AACrD,QAAM,UAAuB,CAAC;AAC9B,MAAI,MAAM,WAAW,SAAS,EAAG,SAAQ,aAAa,MAAM;AAC5D,MAAI,MAAM,WAAW,OAAW,SAAQ,SAAS,MAAM;AACvD,MAAI,MAAM,gBAAgB,OAAW,SAAQ,cAAc,MAAM;AACjE,MAAI,MAAM,eAAe,OAAW,SAAQ,aAAa,MAAM;AAC/D,MAAI,MAAM,eAAe,OAAW,SAAQ,aAAa,MAAM;AAC/D,MAAI,MAAM,aAAa,OAAW,SAAQ,WAAW,MAAM;AAC3D,MAAI,MAAM,UAAU,OAAW,SAAQ,QAAQ,MAAM;AACrD,SAAO;AACT;;;AC/hBO,SAAS,sBAAsB,KAAuB;AAC3D,MAAI,OAAO,QAAQ,YAAY,QAAQ,QAAQ,MAAM,QAAQ,GAAG,EAAG,QAAO;AAC1E,QAAM,EAAE,UAAU,OAAO,GAAG,KAAK,IAAI;AACrC,QAAM,SAAkC,EAAE,GAAG,KAAK;AAClD,MAAI,aAAa,OAAW,QAAO,YAAY;AAC/C,MAAI,MAAM,QAAQ,KAAK,GAAG;AACxB,WAAO,QAAQ,MAAM,IAAI,uBAAuB;AAAA,EAClD,WAAW,UAAU,QAAW;AAI9B,WAAO,QAAQ;AAAA,EACjB;AAKA,SAAO;AAAA,IACL,2BAA2B,wBAAwB,wBAAwB,MAAM,CAAC,CAAC;AAAA,EACrF;AACF;AAMA,SAAS,wBAAwB,MAAwB;AACvD,MAAI,OAAO,SAAS,YAAY,SAAS,QAAQ,MAAM,QAAQ,IAAI,EAAG,QAAO;AAC7E,QAAM,EAAE,QAAQ,GAAG,KAAK,IAAI;AAC5B,QAAM,OAAgC,EAAE,GAAG,KAAK;AAChD,MAAI,WAAW,OAAW,MAAK,OAAO;AACtC,SAAO;AACT;;;ACuDA,IAAM,qBACJ;AAgCF,SAAS,mBAAmB,QAA8C;AACxE,QAAM,SAAS,YAAY,EAAE,aAAa,WAAW;AACrD,MAAI,CAAC,QAAQ;AACX,UAAM,IAAI;AAAA,MACR;AAAA,IACF;AAAA,EACF;AACA,QAAM,MAAM,IAAI,OAAO,EAAE,gBAAgB,QAAQ,eAAe;AAChE,QAAM,YAAY,oBAAI,IAA6B;AAEnD,QAAM,QAAQ,CAAC,IAAa,IAAY,IAAY,OAAe;AACjE,UAAM,EAAE,IAAI,IAAI,MAAM,IAAI,eAAe,EAAE;AAC3C,UAAM,KAAK,KAAK,KAAK;AACrB,UAAM,KAAK,KAAK,KAAK;AACrB,UAAM,KAAK,KAAK;AAChB,QAAI,GAAG,QAAQ,YAAY,MAAM,SAAS;AACxC,YAAM,OAAO,GAAG,aAAa,MAAM,KAAK;AACxC,YAAM,SAAS,KAAK,QAAQ,GAAG;AAC/B,UAAI,WAAW,IAAI;AACjB,cAAM,UAAU,KAAK,MAAM,SAAS,CAAC;AACrC,cAAM,QAAuB;AAAA,UAC3B,GAAG,KAAK,KAAK,OAAO,GAAG,aAAa,GAAG,KAAK,CAAC;AAAA,UAC7C,GAAG,KAAK,KAAK,OAAO,GAAG,aAAa,GAAG,KAAK,CAAC;AAAA,UAC7C,GAAG,KAAK,OAAO,GAAG,aAAa,OAAO,KAAK,CAAC;AAAA,UAC5C,GAAG,KAAK,OAAO,GAAG,aAAa,QAAQ,KAAK,CAAC;AAAA,UAC7C,qBAAqB,GAAG,aAAa,qBAAqB,KAAK;AAAA,QACjE;AACA,cAAM,OAAO,UAAU,IAAI,OAAO;AAClC,YAAI,KAAM,MAAK,KAAK,KAAK;AAAA,YACpB,WAAU,IAAI,SAAS,CAAC,KAAK,CAAC;AAAA,MACrC;AAAA,IACF;AACA,eAAW,SAAS,MAAM,KAAK,GAAG,QAAQ,EAAG,OAAM,OAAO,IAAI,IAAI,EAAE;AAAA,EACtE;AACA,QAAM,IAAI,iBAAiB,GAAG,GAAG,CAAC;AAClC,SAAO;AACT;AAEA,SAAS,IAAI,GAAW,GAAmB;AACzC,MAAI,IAAI,KAAK,IAAI,KAAK,MAAM,CAAC,CAAC;AAC9B,MAAI,IAAI,KAAK,IAAI,KAAK,MAAM,CAAC,CAAC;AAC9B,SAAO,MAAM,EAAG,EAAC,GAAG,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;AAClC,SAAO,KAAK;AACd;AAUA,SAAS,aAAa,GAAW,GAAmB;AAClD,MAAI,KAAK,KAAK,KAAK,EAAG,QAAO,GAAG,CAAC,IAAI,CAAC;AACtC,QAAM,QAAQ,IAAI;AAClB,QAAM,MAAM;AACZ,WAAS,IAAI,GAAG,KAAK,IAAI,KAAK;AAC5B,UAAM,IAAI,KAAK,MAAM,QAAQ,CAAC;AAC9B,QAAI,KAAK,KAAK,KAAK,IAAI,QAAQ,IAAI,CAAC,KAAK,KAAK;AAC5C,YAAM,IAAI,IAAI,GAAG,CAAC;AAClB,aAAO,GAAG,IAAI,CAAC,IAAI,IAAI,CAAC;AAAA,IAC1B;AAAA,EACF;AACA,SAAO,GAAG,MAAM,QAAQ,CAAC,CAAC;AAC5B;AAEA,SAAS,QAAQ,KAAqC;AACpD,QAAM,IAAI,KAAK,MAAM,IAAI,CAAC;AAC1B,QAAM,IAAI,KAAK,MAAM,IAAI,CAAC;AAC1B,SAAO,EAAE,GAAG,KAAK,MAAM,IAAI,CAAC,GAAG,GAAG,KAAK,MAAM,IAAI,CAAC,GAAG,GAAG,GAAG,QAAQ,aAAa,GAAG,CAAC,EAAE;AACxF;AAEA,SAAS,SAAS,MAA6B,QAA2C;AACxF,MAAI,SAAS,SAAS;AACpB,WAAO;AAAA,MACL;AAAA,MACA,MAAM,SACF,kDAAkD,MAAM,mJACxD;AAAA,IACN;AAAA,EACF;AACA,SAAO;AAAA,IACL;AAAA,IACA,MAAM,SACF,4HAAuH,MAAM,6BAC7H;AAAA,EACN;AACF;AAQA,SAAS,aAAa,QAAwC;AAC5D,QAAM,aAAa,CAAC,OAAO,IAAI,OAAO,SAAS,OAAO,OAAO,OAAO,SAAS,OAAO,QAAQ,OAAO,MAAM,OAAO,OAAO,OAAO,MAAM;AACpI,QAAM,QAAQ,CAAC,GAAG,IAAI,IAAI,WAAW,OAAO,CAAC,MAAmB,QAAQ,CAAC,CAAC,CAAC,CAAC;AAC5E,SAAO,EAAE,OAAO,SAAS,OAAO,SAAS,QAAQ,OAAO,OAAO;AACjE;AAEA,SAAS,WAAW,IAAoB;AACtC,SAAQ,oBAA0C,SAAS,EAAE,IAAI,aAAa,EAAsB,IAAI;AAC1G;AAEA,SAAS,cAAc,OAAuB;AAC5C,SAAO,MAAM,QAAQ,WAAW,EAAE,EAAE,QAAQ,MAAM,GAAG;AACvD;AAUA,SAAS,UAAU,SAAiB,UAA2C;AAC7E,QAAM,QAAQ,WAAW,OAAO;AAChC,QAAM,YAAY,SAAS,KAAK,SAAS,IAAI,KAAK,SAAS,KAAK,KAAK,IAAI,CAAC,MAAM;AAChF,QAAM,cAAc,SAAS,QAAQ,WAAW,cAAc,SAAS,KAAK,CAAC,sBAAsB;AACnG,SAAO,EAAE,MAAM,SAAS,MAAM,aAAa,GAAG,KAAK,SAAS,SAAS,GAAG,WAAW,IAAI;AACzF;AAEA,SAAS,YAAY,MAAsB,SAA4B,OAAoC,KAA4B;AACrI,QAAM,aAAa,QAAQ,MAAM,OAAO,CAAC,MAAM,MAAM,QAAQ,WAAW,MAAM,QAAQ,MAAM;AAC5F,QAAM,cAAc,0BAA0B,QAAQ,OAAO,YAAY,QAAQ,MAAM,GAAG,WAAW,SAAS,IAAI,sBAAsB,WAAW,KAAK,IAAI,CAAC,KAAK,EAAE;AACpK,QAAM,kBAAkB,QACpB,iBAAiB,MAAM,MAAM,WAAW,IAAI,IAAI,KAChD;AACJ,SAAO,GAAG,KAAK,WAAW,IAAI,WAAW,IAAI,eAAe;AAC9D;AAgEO,SAAS,gBAAgB,IAAwB;AACtD,QAAM,WAAW,mBAAmB,GAAG,MAAM,EAAE;AAC/C,QAAM,SAAS,aAAa,GAAG,MAAM,IAAI,GAAG,MAAM,KAAK;AACvD,QAAM,UAAU,aAAa,OAAO,MAAM;AAC1C,QAAM,OAAO,UAAU,GAAG,MAAM,IAAI,QAAQ;AAE5C,QAAM,cAAmE,CAAC;AAC1E,KAAG,OAAO,QAAQ,CAAC,OAAO,eAAe;AACvC,eAAW,aAAa,MAAM,YAAY;AACxC,UAAI,UAAU,SAAS,QAAS,aAAY,KAAK,EAAE,YAAY,UAAU,CAAC;AAAA,IAC5E;AAAA,EACF,CAAC;AAED,QAAM,YAAwC,CAAC;AAC/C,aAAW,EAAE,UAAU,KAAK,aAAa;AACvC,cAAU,UAAU,QAAQ,IAAI,EAAE,KAAK,GAAG,kBAAkB,IAAI,UAAU,QAAQ,GAAG;AAAA,EACvF;AACA,QAAM,WAAmB,EAAE,GAAG,IAAI,QAAQ,EAAE,QAAQ,EAAE,GAAG,GAAG,OAAO,QAAQ,GAAG,UAAU,EAAE,EAAE;AAE5F,QAAM,gBAAgB,oBAAI,IAA0C;AACpE,aAAW,cAAc,IAAI,IAAI,YAAY,IAAI,CAAC,MAAM,EAAE,UAAU,CAAC,GAAG;AACtE,kBAAc,IAAI,YAAY,mBAAmB,eAAe,UAAU,UAAU,CAAC,CAAC;AAAA,EACxF;AAEA,QAAM,QAA0B,CAAC;AACjC,KAAG,OAAO,QAAQ,CAAC,OAAO,eAAe;AAKvC,UAAM,SAAS,oBAAI,IAA8B;AACjD,eAAW,aAAa,MAAM,YAAY;AACxC,UAAI,UAAU,SAAS,QAAS;AAChC,YAAM,OAAO,OAAO,IAAI,UAAU,QAAQ;AAC1C,UAAI,KAAM,MAAK,KAAK,SAAS;AAAA,UACxB,QAAO,IAAI,UAAU,UAAU,CAAC,SAAS,CAAC;AAAA,IACjD;AACA,QAAI,OAAO,SAAS,EAAG;AAEvB,UAAM,OAAuB,EAAE,OAAO,YAAY,IAAI,MAAM,IAAI,MAAM,MAAM,MAAM,SAAS,MAAM,QAAQ;AACzG,UAAM,WAAW,cAAc,IAAI,UAAU;AAC7C,UAAM,YAAY,CAAC,YAAoB,CAAC,GAAG,OAAO,OAAO,OAAO,GAAG;AACnE,UAAM,QAAQ,CAAC,YAAoB,GAAG,OAAO,OAAO,OAAO,GAAG;AAE9D,eAAW,CAAC,SAAS,KAAK,KAAK,QAAQ;AACrC,YAAM,SAAS,UAAU,IAAI,OAAO,KAAK,CAAC;AAE1C,UAAI,MAAM,WAAW,GAAG;AAGtB,cAAM,MAAM,OAAO,CAAC;AACpB,cAAM,QAAQ,MAAM,QAAQ,GAAG,IAAI;AACnC,cAAM,MAAM,SAAS,MAAM,CAAC,EAAG,KAAK,OAAO,MAAM;AACjD,cAAM,KAAK;AAAA,UACT;AAAA,UACA,UAAU;AAAA,UACV,KAAK,MAAM,OAAO;AAAA,UAClB,MAAM;AAAA,UACN,SAAS,UAAU,OAAO;AAAA,UAC1B,UAAU,UAAU;AAAA,UACpB;AAAA,UACA,kBAAkB,QAAQ,EAAE,GAAG,MAAM,IAAI,GAAG,GAAG,MAAM,IAAI,EAAE,IAAI;AAAA,UAC/D;AAAA,UACA;AAAA,UACA;AAAA,UACA,kBAAkB,YAAY,MAAM,SAAS,OAAO,GAAG;AAAA,QACzD,CAAC;AACD;AAAA,MACF;AAQA,YAAM,kBAAkB,MAAM;AAC9B,YAAM,YAAY,MAAM,CAAC,EAAG;AAC5B,YAAM,UAAU,UAAU,OAAO;AACjC,iBAAW,OAAO,QAAQ;AACxB,cAAM,QAAQ,QAAQ,GAAG;AACzB,cAAM,MAAM,SAAS,WAAW,MAAM,MAAM;AAC5C,cAAM,KAAK;AAAA,UACT;AAAA,UACA,UAAU;AAAA,UACV,KAAK,MAAM,OAAO;AAAA,UAClB,MAAM;AAAA,UACN;AAAA,UACA,UAAU;AAAA,UACV;AAAA,UACA,kBAAkB,EAAE,GAAG,MAAM,IAAI,GAAG,GAAG,MAAM,IAAI,EAAE;AAAA,UACnD;AAAA,UACA;AAAA,UACA;AAAA,UACA,QAAQ;AAAA,UACR;AAAA,UACA,kBAAkB,YAAY,MAAM,SAAS,OAAO,GAAG;AAAA,QACzD,CAAC;AAAA,MACH;AAIA,eAAS,IAAI,OAAO,QAAQ,IAAI,iBAAiB,KAAK;AACpD,cAAM,MAAM,SAAS,WAAW,MAAS;AACzC,cAAM,KAAK;AAAA,UACT;AAAA,UACA,UAAU;AAAA,UACV,KAAK,MAAM,OAAO;AAAA,UAClB,MAAM;AAAA,UACN;AAAA,UACA,UAAU;AAAA,UACV;AAAA,UACA;AAAA,UACA;AAAA,UACA,QAAQ;AAAA,UACR;AAAA,UACA,kBAAkB,YAAY,MAAM,SAAS,QAAW,GAAG;AAAA,QAC7D,CAAC;AAAA,MACH;AAAA,IACF;AAAA,EACF,CAAC;AAED,SAAO,EAAE,OAAO,GAAG,MAAM,IAAI,MAAM;AACrC;","names":["z","z","z","z"]}
@@ -0,0 +1,19 @@
1
+ // src/platform/registry.ts
2
+ function findRemoteAssetRef(svgMarkup) {
3
+ const m = /\s(?:xlink:href|href)\s*=\s*["'](https?:\/\/[^"']*)["']/i.exec(svgMarkup);
4
+ return m ? m[1] : null;
5
+ }
6
+ var current = {};
7
+ function installPlatform(p) {
8
+ current = { ...current, ...p };
9
+ }
10
+ function getPlatform() {
11
+ return current;
12
+ }
13
+
14
+ export {
15
+ findRemoteAssetRef,
16
+ installPlatform,
17
+ getPlatform
18
+ };
19
+ //# sourceMappingURL=chunk-VUOLBHD7.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/platform/registry.ts"],"sourcesContent":["/**\n * A rasterized page — RGBA, 4 bytes per pixel, row-major, top-to-bottom,\n * straight (non-premultiplied) alpha. Deliberately *not* the DOM `ImageData`\n * type: that interface only exists as an ambient global where a canvas\n * implementation provides it, and this shape must be constructible in Node\n * (from sharp's raw buffer) without depending on any DOM global actually\n * existing at runtime — only the browser implementation happens to be able\n * to hand back a real `ImageData` object, which satisfies this shape\n * structurally without either side needing to import the other's type.\n */\nexport interface RasterizedImage {\n width: number\n height: number\n data: Uint8ClampedArray\n}\n\n/**\n * Scan SVG markup for a remote (`http://`/`https://`) image reference on\n * `href`/`xlink:href`, before any `rasterizeSvg` implementation touches it.\n * Shared by every implementation (`node.ts`'s sharp path, `browser.ts`'s\n * canvas path) rather than each reimplementing its own copy, because the\n * guarantee it enforces is a platform-wide one, not a browser-only one:\n * spec §3.1/§7 promise the default audit chain never starts a network\n * request, and `resolveLocalAssets` already inlines every *local* file to a\n * `data:` URI before render — the only way an `http(s)://` href survives\n * into rendered markup at all is a deck that deliberately keeps a remote URL\n * as its own asset `src`. Left there, a Node rasterizer could silently\n * attempt to fetch it (a real network request `pptwise audit` must never\n * make) and a browser canvas would silently drop it and taint the canvas\n * (the audit-v2 controller ruling this guard implements) — either way the\n * rasterized page would show *not what the text actually sits on*, which is\n * exactly the \"checked nothing, reported clean\" failure mode this whole\n * wave exists to rule out. Returns the offending URL for the caller's error\n * message, or `null` when the markup is clean.\n */\nexport function findRemoteAssetRef(svgMarkup: string): string | null {\n const m = /\\s(?:xlink:href|href)\\s*=\\s*[\"'](https?:\\/\\/[^\"']*)[\"']/i.exec(svgMarkup)\n return m ? m[1]! : null\n}\n\n/** Environment seams. The SDK entry stays browser-safe: Node implementations\n * live in ./node and are installed explicitly (CLI does it automatically). */\nexport interface PptwisePlatform {\n /** DOMParser constructor used to parse rendered SVG markup. */\n domParser?: typeof DOMParser\n /** Re-encode an image data URL to PNG (Office rejects webp and friends). */\n recodeImageToPng?: (dataUrl: string) => Promise<string>\n /**\n * Rasterize SVG markup to a fixed-size pixel buffer (audit-v2 phase B,\n * spec §4.3/§11.7) — the one primitive the optional pixel-contrast audit\n * needs and the *only* one Sharp/canvas-shaped work is allowed to hide\n * behind (`src/svg/audit/pixel-audit.ts` never imports a rasterizer\n * itself). `installNodePlatform()` wires this to Sharp; a real browser\n * gets its own default (`./browser.ts`'s `rasterizeSvgInBrowser`) applied\n * at the call site the same way `domParser`'s `?? globalThis.DOMParser`\n * fallback already works — not through this seam, since nothing calls\n * `installPlatform()` automatically in a browser. Every implementation\n * must reject markup `findRemoteAssetRef` flags rather than touch the\n * network or a tainted canvas.\n */\n rasterizeSvg?: (svgMarkup: string, width: number, height: number) => Promise<RasterizedImage>\n /**\n * Optional fetch used when inlining remote assets. The Node CLI installs a\n * proxy-aware implementation. Absent, callers fall back to global fetch\n * (tests that `vi.stubGlobal(\"fetch\")` without installing a platform).\n */\n fetch?: typeof fetch\n}\n\nlet current: PptwisePlatform = {}\n\nexport function installPlatform(p: PptwisePlatform): void {\n current = { ...current, ...p }\n}\n\nexport function getPlatform(): PptwisePlatform {\n return current\n}\n"],"mappings":";AAmCO,SAAS,mBAAmB,WAAkC;AACnE,QAAM,IAAI,2DAA2D,KAAK,SAAS;AACnF,SAAO,IAAI,EAAE,CAAC,IAAK;AACrB;AA+BA,IAAI,UAA2B,CAAC;AAEzB,SAAS,gBAAgB,GAA0B;AACxD,YAAU,EAAE,GAAG,SAAS,GAAG,EAAE;AAC/B;AAEO,SAAS,cAA+B;AAC7C,SAAO;AACT;","names":[]}