From 27062ee254da26a7c6c837ead609a4e0e16e76ef Mon Sep 17 00:00:00 2001 From: "songtianyi.theo" Date: Wed, 17 Jun 2026 18:09:46 +0800 Subject: [PATCH] feat(slides): harden svglide visual pipeline --- .../references/lark-slides-create-svg.md | 37 +- .../references/ppt-master-asset-inventory.md | 100 - .../lark-slides/references/style-presets.json | 2 +- .../lark-slides/references/style-presets.md | 6 +- .../references/svg-aesthetic-review.md | 5 +- skills/lark-slides/references/svg-seeds.json | 55 +- .../lark-slides/references/svglide-craft.md | 6 +- .../svglide-design-pattern-inventory.md | 53 + ...p.json => svglide-design-pattern-map.json} | 1308 +++++------ .../references/svglide-project-pipeline.md | 17 +- .../references/validation-checklist.md | 23 +- .../scripts/ppt_master_asset_inventory.py | 882 ------- .../ppt_master_asset_inventory_test.py | 96 - skills/lark-slides/scripts/svg_preflight.py | 253 +- .../lark-slides/scripts/svg_preflight_test.py | 76 +- .../lark-slides/scripts/svg_preview_lint.py | 62 + .../scripts/svg_preview_lint_test.py | 44 + .../scripts/svglide_asset_selector.py | 4 +- .../scripts/svglide_asset_selector_test.py | 25 +- .../scripts/svglide_gen_runtime.py | 2028 +++++++++++++++++ .../scripts/svglide_gen_runtime_test.py | 776 +++++++ .../scripts/svglide_golden_suite.py | 112 + .../scripts/svglide_golden_suite_test.py | 78 + .../scripts/svglide_project_runner.py | 534 ++++- .../scripts/svglide_project_runner_test.py | 516 ++++- .../lark-slides/scripts/svglide_strategist.py | 675 ++++++ .../scripts/svglide_strategist_test.py | 231 ++ skills_embed.go | 5 +- 28 files changed, 6161 insertions(+), 1848 deletions(-) delete mode 100644 skills/lark-slides/references/ppt-master-asset-inventory.md create mode 100644 skills/lark-slides/references/svglide-design-pattern-inventory.md rename skills/lark-slides/references/{ppt-master-asset-map.json => svglide-design-pattern-map.json} (91%) delete mode 100644 skills/lark-slides/scripts/ppt_master_asset_inventory.py delete mode 100644 skills/lark-slides/scripts/ppt_master_asset_inventory_test.py create mode 100644 skills/lark-slides/scripts/svglide_gen_runtime.py create mode 100644 skills/lark-slides/scripts/svglide_gen_runtime_test.py create mode 100644 skills/lark-slides/scripts/svglide_golden_suite.py create mode 100644 skills/lark-slides/scripts/svglide_golden_suite_test.py create mode 100644 skills/lark-slides/scripts/svglide_strategist.py create mode 100644 skills/lark-slides/scripts/svglide_strategist_test.py diff --git a/skills/lark-slides/references/lark-slides-create-svg.md b/skills/lark-slides/references/lark-slides-create-svg.md index 09503bc94..51cd0641b 100644 --- a/skills/lark-slides/references/lark-slides-create-svg.md +++ b/skills/lark-slides/references/lark-slides-create-svg.md @@ -203,6 +203,30 @@ SVG 创建不使用单独的规划目录。新建或大幅改写 SVG deck 时, } ``` +每页还必须有 `visual_design_contract`。它不是新的一套平行 plan,而是把现有 +`seed_id`、`visual_recipe`、`renderer_id`、`design_pattern_selection` 和页面 +视觉目标锁成可验收字段: + +```json +{ + "visual_design_contract": { + "schema_version": "svglide-visual-design-contract/v1", + "page_kind": "chart_takeaway", + "visual_thesis": "这一页视觉上要让用户记住的结论", + "composition_archetype": "data_stage", + "pattern_bundle": ["chart.bar_chart"], + "density": "dense", + "primary_motif": "takeaway_chart", + "required_visual_evidence": ["chart_geometry", "insight_strip", "full_page_archetype"] + } +} +``` + +`required_visual_evidence` 必须由 renderer 写入 +`receipts/emitted_components.json` 的 page-level component `effects`、 +`primitives`、`renderer_id` 或 component id。`quality_gate` 会核对这些证据; +只在 plan 里声明但 SVG/component report 没有落地时不得进入 dry-run/live。 + 模板也复用现有 `template_tool.py search -> summarize -> extract` 路由。模板摘要只用于选择主题、页面流、视觉节奏和布局骨架;生成 SVG 时要把模板结构翻译成 SVG layout boxes / visual recipes,不要照搬模板 XML,也不要读取完整模板 XML。 SVG deck 的 `slides[]` 还必须包含这些可校验字段,避免生成结果虽然能创建但内容千篇一律、信息量不足或在资料缺失时编造事实: @@ -244,7 +268,7 @@ SVG deck 的 `slides[]` 还必须包含这些可校验字段,避免生成结 "visual_intent": "use a product-console dashboard surface to make metrics feel operational", "visual_focal_point": "central metric card and trend line", "visual_signature": "fake product console frame + micro chart geometry + status chips", - "reference_asset": {"source": "ppt-master", "asset_id": "chart.kpi_cards", "usage": "page-type geometry only; do not copy raw SVG paths"}, + "reference_asset": {"source": "svglide_design_pattern", "asset_id": "chart.kpi_cards", "usage": "page-type geometry only; do not copy raw SVG paths"}, "svg_effects": ["chart_geometry", "connector_flow", "typography"], "required_primitives": ["dashboard", "micro_chart"], "svg_primitives": ["dashboard", "micro_chart", "typography", "geometric_shape"], @@ -267,16 +291,16 @@ SVG deck 的 `slides[]` 还必须包含这些可校验字段,避免生成结 } ``` -### PPT Master Lessons For SVGlide +### SVGlide Design Pattern Lessons -`ppt-master` 的可借鉴点是生成流程和页型合同,不是 PPTX/DrawingML 导出链路,也不是复制现成 SVG。SVGlide 生成时必须把它的经验收敛成 protocol-safe 字段: +SVGlide design pattern 的可借鉴点是生成流程和页型合同,不是 PPTX/DrawingML 导出链路,也不是复制现成 SVG。SVGlide 生成时必须把经验收敛成 protocol-safe 字段: - `page_rhythm`: `anchor` / `breathing` / `dense`。8 页以上 deck 不能全是 dense 或全是同一张卡片结构;封面、目录、章节、收尾应形成节奏。 - `page_type`: 页面叙事类型,例如 `cover`、`editor_note`、`contents`、`chart_takeaway`、`chapter`、`closing`。 - `chart_type`: 当页面主视觉是图表时必须声明,例如 `kpi_cards`、`bar_chart`、`dumbbell_chart`、`bubble_chart`、`donut_chart`、`horizontal_bar_chart`、`comparison_table`、`sankey_chart`、`pareto_chart`、`hub_spoke`、`dual_axis_line_chart`、`quadrant_text_bullets`。 - `main_visual_anchor`: 一句话说明这页的主视觉锚点。不能写成“高级图表”或“信息可视化”;必须能被截图肉眼确认。 - `annotation_zone`: 右侧观察、底部 source、图例或 callout 的固定区域。密集页靠图表承载信息,文字只做解释。 -- `reference_asset`: 记录借鉴的 ppt-master layout/chart/image/style 资产,只表示结构和选择理由,不表示复制 raw SVG。 +- `reference_asset`: 记录借鉴的 SVGlide design pattern layout/chart/image/style 资产,只表示结构和选择理由,不表示复制 raw SVG。 经验规则: @@ -289,7 +313,7 @@ SVG deck 的 `slides[]` 还必须包含这些可校验字段,避免生成结 经验归属: - 本文件只保留 create-svg 的入口流程、plan 字段边界和 CLI 发布约束。 -- 页型、图表几何、视觉节奏和 ppt-master 借鉴规则归 [svglide-craft.md](svglide-craft.md)。 +- 页型、图表几何、视觉节奏和 SVGlide design pattern 借鉴规则归 [svglide-craft.md](svglide-craft.md)。 - 项目目录、stage、timing、prepare resume、asset usage receipt 和 live guard 归 [svglide-project-pipeline.md](svglide-project-pipeline.md)。 - 自动门禁和 archetype drift 归 [validation-checklist.md](validation-checklist.md)。 - 截图/人工审美判断和 label/backing 遮挡经验归 [svg-aesthetic-review.md](svg-aesthetic-review.md)。 @@ -397,6 +421,7 @@ SVG 私有 recipe 只属于 `slides +create-svg` route,用于鼓励更强的 S | `layout_skeleton_id` | 绑定 seed 的机器化版式骨架 | 不匹配或缺失时从 seed 复制;大改结构必须换 seed | | `layout_family` | 做 deck 级版式多样性检查 | 相邻页重复时换阅读方向、主视觉位置或信息结构 | | `visual_recipe` | 说明这页为什么值得走 SVG | 从 `svg-recipes.json` 选择,不能自造枚举 | +| `visual_design_contract` | 锁定视觉 thesis、composition archetype、motif 和必须落地的 evidence | 补齐合同,或改 renderer/component report 证明 evidence | | `layout_boxes` | seed 派生的标题、正文、视觉、chart、footer 坐标 | 缺角色或 bbox 非正数时先修布局 | | `content_budget` / `text_capacity` | seed 派生的文本容量预算 | 超量时删内容、拆页或换 seed | | `text_budget_by_role` | seed 派生的 role 级文本预算 | 局部超量时删内容、拆页或换 seed,不用缩字/竖排硬塞 | @@ -445,7 +470,7 @@ comparison >= 4 rows or columns - MUST: 主体元素使用安全区,建议 `safe = x:48 y:40 w:864 h:460`。除全屏背景外,文本、卡片、图表、标签、节点和图例都必须落在安全区内。 - MUST: 多页 deck 应包含明确的 closing slide。8 页以上讲解/汇报型 deck 不要把 roadmap / next-playbook 当作结束页;最后一页应包含 `closing`、`summary`、`Q&A`、`Thanks` 或下一步联系信息。 - MUST: `slides[]` 必须记录 `renderer_id`,且它要对应真实几何结构,而不是 `two-column-1` / `two-column-2` 这种名字变化。10 页以上 deck 至少 5 种 renderer/layout family;不得连续 3 页使用同一 renderer。 -- MUST: `slides[]` 必须记录 `seed_id`、`layout_skeleton_id`、`layout_family`、`layout_boxes`、`content_budget` 或 `text_capacity`、`text_budget_by_role`、`reserved_bands.footer`、`footer_safe_zone`、`vertical_text_policy`、`one_idea` 或 `key_message`、`visual_recipe`、`visual_intent`、`visual_focal_point`、`required_primitives`、`svg_primitives`、`xml_like_risk`、`content_density_contract`、`risk_flags`、`source_policy`。`asset_contract` 应尽量记录;MVP 阶段缺失只 warning。没有 SVG-native recipe 的页面不应走 `slides +create-svg`,应改用普通 Slides XML 或重新选择 SVG recipe。 +- MUST: `slides[]` 必须记录 `seed_id`、`layout_skeleton_id`、`layout_family`、`layout_boxes`、`content_budget` 或 `text_capacity`、`text_budget_by_role`、`reserved_bands.footer`、`footer_safe_zone`、`vertical_text_policy`、`one_idea` 或 `key_message`、`visual_recipe`、`visual_intent`、`visual_focal_point`、`visual_design_contract`、`required_primitives`、`svg_primitives`、`xml_like_risk`、`content_density_contract`、`risk_flags`、`source_policy`。`asset_contract` 应尽量记录;MVP 阶段缺失只 warning。没有 SVG-native recipe 的页面不应走 `slides +create-svg`,应改用普通 Slides XML 或重新选择 SVG recipe。 - MUST: `visual_recipe` 必须来自 catalog,且 `svg_primitives` 必须覆盖该 recipe 的 required primitives。`renderer_id` 不能替代 `visual_recipe`。 - MUST: `seed_id` 必须来自 `svg-seeds.json`,seed 的 `visual_recipe` / `layout_family` 必须和 plan 一致。`route_private` 只隐藏具体 private recipe,不跳过 seed/layout/budget 控制。 - MUST: seed skeleton 是版式合同,不是灵感参考。生成器必须继承 seed 的 `required_layout_box_roles`、`layout_skeleton_id`、`layout_boxes`、`text_budget_by_role`、`footer_safe_zone` 和 `vertical_text_policy`;内容放不下时删减、拆页或换 seed,不得放宽 seed budget 或自由重画骨架。 diff --git a/skills/lark-slides/references/ppt-master-asset-inventory.md b/skills/lark-slides/references/ppt-master-asset-inventory.md deleted file mode 100644 index d22d6dd9b..000000000 --- a/skills/lark-slides/references/ppt-master-asset-inventory.md +++ /dev/null @@ -1,100 +0,0 @@ -# ppt-master Asset Inventory - -This file is generated by `skills/lark-slides/scripts/ppt_master_asset_inventory.py`. -The JSON asset map is the source of truth; this Markdown is a human-readable summary. - -## Safety Defaults - -- Raw ppt-master SVG assets start as `needs_normalization`, `derive_contract_only`, `reference_only`, and `candidate`. -- Icon assets are summarized by library and searchable index metadata; individual icon SVG files are not injected into the main prompt. -- Examples are summarized by project/page/media counts and treated as reference corpus, not runtime assets. -- Production/golden selection must not use rejected, reference-only, or raw unnormalized assets. - -## Counts - -| Kind | Count | -| --- | ---: | -| `brand_preset` | 2 | -| `chart_template` | 71 | -| `deck_template` | 8 | -| `example_media_files` | 259 | -| `example_pages` | 356 | -| `example_project` | 21 | -| `icon_library` | 5 | -| `icon_svg_files` | 11631 | -| `image_palette` | 14 | -| `image_reference_collection` | 3 | -| `image_rendering` | 20 | -| `image_type_template` | 11 | -| `layout_template` | 7 | -| `narrative_mode` | 5 | -| `total_resources` | 191 | -| `visual_style` | 18 | -| `workflow_reference` | 6 | - -## Digests - -| Scope | SHA-256 prefix | -| --- | --- | -| `all_source_files` | `42030551448bb8d1` | -| `brands` | `e43ae292b361bac3` | -| `charts` | `ca05fc31a7a98182` | -| `decks` | `4e5d701c8b5f949e` | -| `examples` | `33acfc6626f61c7e` | -| `icons` | `bf1ee25d54ae5702` | -| `layouts` | `9be264ac4ac3699b` | -| `references` | `f7c3b07574ea660b` | - -## Resource Summary - -| Kind | Example IDs | -| --- | --- | -| `brand_preset` | `brand.anthropic`, `brand.google` | -| `chart_template` | `chart.agenda_list`, `chart.arc_anchored_list`, `chart.area_chart`, `chart.bar_chart`, `chart.basic_table`, `chart.box_plot_chart`, `chart.bubble_chart`, `chart.bullet_chart`, ... (+63) | -| `deck_template` | `deck.中国电信`, `deck.中国电建_常规`, `deck.中国电建_现代`, `deck.中汽研_商务`, `deck.中汽研_常规`, `deck.中汽研_现代`, `deck.招商银行`, `deck.重庆大学` | -| `example_project` | `example.ppt169_attention_is_all_you_need`, `example.ppt169_brutalist_ai_newspaper_2026`, `example.ppt169_building_effective_agents`, `example.ppt169_cangzhuo`, `example.ppt169_fashion_weekly_digest`, `example.ppt169_general_dark_tech_claude_code_auto_mode`, `example.ppt169_glassmorphism_demo`, `example.ppt169_global_ai_capital_2026`, ... (+13) | -| `icon_library` | `icon_library.chunk-filled`, `icon_library.phosphor-duotone`, `icon_library.simple-icons`, `icon_library.tabler-filled`, `icon_library.tabler-outline` | -| `image_palette` | `image_palette.cool-corporate`, `image_palette.dark-cinematic`, `image_palette.duotone`, `image_palette.earthy-dusty`, `image_palette.editorial-classic`, `image_palette.frost-ice`, `image_palette.jewel-tone`, `image_palette.macaron`, ... (+6) | -| `image_reference_collection` | `image_reference_collection.palette`, `image_reference_collection.rendering`, `image_reference_collection.type` | -| `image_rendering` | `image_rendering.3d-isometric`, `image_rendering.blueprint`, `image_rendering.chalkboard`, `image_rendering.corporate-photo`, `image_rendering.digital-dashboard`, `image_rendering.editorial`, `image_rendering.fantasy-animation`, `image_rendering.flat`, ... (+12) | -| `image_type_template` | `image_type.comparison`, `image_type.cycle`, `image_type.flowchart`, `image_type.framework`, `image_type.funnel`, `image_type.infographic`, `image_type.map`, `image_type.matrix`, ... (+3) | -| `layout_template` | `layout.academic_defense`, `layout.ai_ops`, `layout.government_blue`, `layout.government_red`, `layout.medical_university`, `layout.pixel_retro`, `layout.psychology_attachment` | -| `narrative_mode` | `mode.briefing`, `mode.instructional`, `mode.narrative`, `mode.pyramid`, `mode.showcase` | -| `visual_style` | `visual_style.blueprint`, `visual_style.brutalist`, `visual_style.chalkboard`, `visual_style.dark-tech`, `visual_style.data-journalism`, `visual_style.editorial`, `visual_style.glassmorphism`, `visual_style.ink-notes`, ... (+10) | -| `workflow_reference` | `workflow.executor-base`, `workflow.image-layout-patterns`, `workflow.image-layout-spec`, `workflow.shared-standards`, `workflow.strategist`, `workflow.visual-review` | - -## Example Corpus - -| Example | Pages | Media | Base64 | Digest | -| --- | ---: | ---: | ---: | --- | -| `example.ppt169_attention_is_all_you_need` | 16 | 11 | 11 | `38bc99f26cc47232` | -| `example.ppt169_brutalist_ai_newspaper_2026` | 10 | 5 | 5 | `4d2b449dd8110214` | -| `example.ppt169_building_effective_agents` | 12 | 8 | 8 | `fb5afb97ba4c1718` | -| `example.ppt169_cangzhuo` | 14 | 6 | 6 | `578f91a97e1c81b3` | -| `example.ppt169_fashion_weekly_digest` | 16 | 23 | 22 | `ba01eb82f8842691` | -| `example.ppt169_general_dark_tech_claude_code_auto_mode` | 10 | 4 | 4 | `e9d6f351c60915cb` | -| `example.ppt169_glassmorphism_demo` | 12 | 8 | 8 | `bb17f9a2001bcaca` | -| `example.ppt169_global_ai_capital_2026` | 20 | 3 | 3 | `3377a4a832f87b5f` | -| `example.ppt169_high_rise_renewal` | 15 | 25 | 21 | `ba8a732dc29cb2c1` | -| `example.ppt169_home_design_trends_2026` | 12 | 24 | 22 | `f1dc9774224cf00e` | -| `example.ppt169_image_text_showcase` | 20 | 22 | 22 | `e077d46f669fac80` | -| `example.ppt169_indie_bookstore_zine_guide` | 18 | 10 | 10 | `53d75c7fc3ff0b14` | -| `example.ppt169_kimsoong_loyalty_programme` | 10 | 4 | 3 | `ebc6c414ab46be63` | -| `example.ppt169_kubernetes_blueprint_2026` | 10 | 0 | 0 | `132c3d1a12c190c2` | -| `example.ppt169_lin_huiyin_architect` | 10 | 14 | 10 | `20b9855195eb7b96` | -| `example.ppt169_lin_huiyin_architect_revised` | 9 | 13 | 10 | `cf255646e3ac6c65` | -| `example.ppt169_liziqi_plant_dye_colors` | 12 | 32 | 16 | `f6b0d492a8afb107` | -| `example.ppt169_lora_hu_2021` | 15 | 10 | 10 | `5e0b0e096424d588` | -| `example.ppt169_pritzker_2026` | 11 | 25 | 20 | `f629a8386f3c896c` | -| `example.ppt169_sugar_rush_memphis` | 14 | 9 | 9 | `bcdeafc36693f36d` | -| `example.ppt169_swiss_grid_systems` | 14 | 3 | 3 | `1823080b6fef9ef4` | - -## Icon Libraries - -| Library | Icons | Policy | -| --- | ---: | --- | -| `icon_library.chunk-filled` | 640 | library_summary_only | -| `icon_library.phosphor-duotone` | 1248 | library_summary_only | -| `icon_library.simple-icons` | 3651 | library_summary_only | -| `icon_library.tabler-filled` | 1053 | library_summary_only | -| `icon_library.tabler-outline` | 5039 | library_summary_only | diff --git a/skills/lark-slides/references/style-presets.json b/skills/lark-slides/references/style-presets.json index 9eecacb54..fecff7c13 100644 --- a/skills/lark-slides/references/style-presets.json +++ b/skills/lark-slides/references/style-presets.json @@ -114,7 +114,7 @@ "style_id": "data_journalism_editorial", "display_name": "Data Journalism Editorial", "group": "Restrained", - "source_token": "ppt-master/global_ai_capital_2026", + "source_token": "svglide/editorial_ai_capital_2026", "formality": "high", "vibe": ["editorial", "data_journalism", "financial", "cinematic"], "best_for": ["hero_typography", "infographic_scorecard", "path_flow", "geometric_composition", "icon_capability_map"], diff --git a/skills/lark-slides/references/style-presets.md b/skills/lark-slides/references/style-presets.md index c5f38685f..5f3b2167f 100644 --- a/skills/lark-slides/references/style-presets.md +++ b/skills/lark-slides/references/style-presets.md @@ -1,6 +1,6 @@ # SVGlide Style Presets -`style-presets.json` is the runtime source of truth for the 35 `beautiful-feishu-whiteboard` style presets plus the SVGlide `data_journalism_editorial` preset distilled from ppt-master. This Markdown file is only a human-readable guide. +`style-presets.json` is the runtime source of truth for the 35 `beautiful-feishu-whiteboard` style presets plus the SVGlide `data_journalism_editorial` preset. This Markdown file is only a human-readable guide. ## Boundary @@ -57,9 +57,9 @@ Use `visual_plan` as a nested container when useful. `svg_preflight.py` accepts 3. Keep the semantic plan stable. Switching from `raw_grid` to `reading_room` should change visual treatment, not invent new facts or rearrange the story. 4. Pick page-level overrides only for cover, section divider, or poster-like moments. Most slides should inherit the deck-level `style_preset`. -## PPT Master Editorial Preset +## SVGlide Editorial Preset -Use `data_journalism_editorial` when a deck needs the dark, data-journalism feel seen in ppt-master's `ppt169_global_ai_capital_2026` example. Translate the reference into SVGlide-safe parts: dark graphite ground, large editorial title hierarchy, thin chart rules, small source/footer text, restrained red numeric emphasis, and real chart geometry. Do not copy ppt-master SVG paths, images, or PPTX export assumptions. +Use `data_journalism_editorial` when a deck needs a dark data-journalism feel. Translate the preset into SVGlide-safe parts: dark graphite ground, large editorial title hierarchy, thin chart rules, small source/footer text, restrained red numeric emphasis, and real chart geometry. Do not copy raw SVG paths, images, or PPTX export assumptions. ## SVGlide-Safe Translation diff --git a/skills/lark-slides/references/svg-aesthetic-review.md b/skills/lark-slides/references/svg-aesthetic-review.md index 048cb0b99..f7eff556a 100644 --- a/skills/lark-slides/references/svg-aesthetic-review.md +++ b/skills/lark-slides/references/svg-aesthetic-review.md @@ -86,10 +86,9 @@ Project runner 中的 `svg_preview_lint.py` 负责缺 preview、破损 SVG、明 - 图表/流程/表格选择是否适合它要表达的关系? - 颜色和强调方式是否和整套 deck 保持一致? -## PPT Master / AI Capital Archetype Review +## SVGlide Archetype Review -从 `ppt-master` 样张和本地 AI Capital 多轮 preview 得到的经验要落在渲染后审查, -而不是只写进 prompt。 +从 SVGlide archetype 样张和本地多轮 preview 得到的经验要落在渲染后审查,而不是只写进 prompt。 - 第一眼必须看出页型:cover、contents、section、bubble chart、donut chart、 sankey/flow、hub-spoke、table、closing 等不能互相退化成同一种卡片页。 diff --git a/skills/lark-slides/references/svg-seeds.json b/skills/lark-slides/references/svg-seeds.json index d0917e1d9..d09f8863e 100644 --- a/skills/lark-slides/references/svg-seeds.json +++ b/skills/lark-slides/references/svg-seeds.json @@ -383,7 +383,7 @@ "body": 180, "footer": 34, "max_text_boxes": 8, - "min_text_boxes": 4 + "min_text_boxes": 3 }, "default_text_capacity": { "max_visible_chars": 260, @@ -391,7 +391,7 @@ "body": 180, "footer": 34, "max_text_boxes": 8, - "min_text_boxes": 4 + "min_text_boxes": 3 }, "reserved_bands": { "footer": { @@ -677,7 +677,7 @@ }, { "id": "left-panel", - "role": "body", + "role": "visual", "x": 72, "y": 136, "width": 384, @@ -685,7 +685,7 @@ }, { "id": "right-panel", - "role": "body", + "role": "visual", "x": 504, "y": 136, "width": 384, @@ -739,7 +739,6 @@ ], "required_layout_box_roles": [ "title", - "body", "table", "footer" ], @@ -752,13 +751,11 @@ "id": "comparison_two_column_decision_skeleton", "reading_order": [ "title", - "body", "table", "footer" ], "locked_roles": [ "title", - "body", "table", "footer" ], @@ -800,6 +797,18 @@ "max_boxes": 2, "min_font_px": 12 }, + "table": { + "max_chars": 180, + "max_lines": 2, + "max_boxes": 9, + "min_font_px": 11 + }, + "visual": { + "max_chars": 180, + "max_lines": 2, + "max_boxes": 9, + "min_font_px": 11 + }, "footer": { "max_chars": 36, "max_lines": 1, @@ -1128,10 +1137,16 @@ "min_font_px": 12 }, "metric": { - "max_chars": 80, + "max_chars": 110, "max_lines": 3, - "max_boxes": 1, - "min_font_px": 12 + "max_boxes": 7, + "min_font_px": 9 + }, + "chart": { + "max_chars": 40, + "max_lines": 1, + "max_boxes": 4, + "min_font_px": 8 }, "footer": { "max_chars": 34, @@ -1357,6 +1372,14 @@ "width": 548, "height": 128 }, + { + "id": "secondary-metric-grid", + "role": "metric", + "x": 348, + "y": 106, + "width": 548, + "height": 128 + }, { "id": "chart-row", "role": "chart", @@ -1481,10 +1504,16 @@ "min_font_px": 12 }, "metric": { - "max_chars": 80, + "max_chars": 110, "max_lines": 3, - "max_boxes": 1, - "min_font_px": 12 + "max_boxes": 7, + "min_font_px": 9 + }, + "chart": { + "max_chars": 40, + "max_lines": 1, + "max_boxes": 4, + "min_font_px": 8 }, "footer": { "max_chars": 36, diff --git a/skills/lark-slides/references/svglide-craft.md b/skills/lark-slides/references/svglide-craft.md index 7ba236cb4..4bb7cc756 100644 --- a/skills/lark-slides/references/svglide-craft.md +++ b/skills/lark-slides/references/svglide-craft.md @@ -38,9 +38,9 @@ choose style_preset 每页 plan 必须有 `seed_id`、`layout_skeleton_id`、`layout_boxes`、`content_budget` 或 `text_capacity`、`text_budget_by_role`、`one_idea` 或 `key_message`、`reserved_bands.footer`、`footer_safe_zone`、`vertical_text_policy`。如果内容放不进 seed,不要从空白画布重画;先删内容、拆页,或换一个更合适的 seed。 -## PPT Master Lessons +## SVGlide Design Pattern Lessons -`ppt-master` 好看的关键是页型先行和节奏先行。SVGlide 只能借鉴流程与结构合同,不复制 PPTX 导出、DrawingML 限制或 raw SVG path。 +SVGlide 内置设计模式的关键是页型先行和节奏先行。生成时只能借鉴流程与结构合同,不复制 PPTX 导出、DrawingML 限制或 raw SVG path。 每页先锁定: @@ -51,7 +51,7 @@ choose style_preset "chart_type": "bar_chart | sankey_chart | hub_spoke | ...", "main_visual_anchor": "the visible chart/scene/motif that makes this page memorable", "annotation_zone": {"role": "right_observation", "x": 690, "y": 126, "width": 206, "height": 246}, - "reference_asset": {"source": "ppt-master", "asset_id": "chart.bar_chart", "usage": "geometry pattern only"} + "reference_asset": {"source": "svglide_design_pattern", "asset_id": "chart.bar_chart", "usage": "geometry pattern only"} } ``` diff --git a/skills/lark-slides/references/svglide-design-pattern-inventory.md b/skills/lark-slides/references/svglide-design-pattern-inventory.md new file mode 100644 index 000000000..eecd0e016 --- /dev/null +++ b/skills/lark-slides/references/svglide-design-pattern-inventory.md @@ -0,0 +1,53 @@ +# SVGlide Design Pattern Inventory + +This file describes internal SVGlide design patterns used by the SVG generation pipeline. +The CLI must not read an external slide-generation project at runtime; all usable patterns here are abstracted into SVGlide-owned ids, contracts, and renderer inputs. + +## Policy + +- Runtime dependency: none. +- Source SVG/PPTX assets are not copied into output. +- Patterns represent page rhythm, layout archetypes, chart geometry, style cues, image composition, and review heuristics. +- Production use still requires a normalized SVGlide renderer, clear asset/license status, and quality gate evidence. + +## Counts + +- `brand_preset`: 2 +- `chart_template`: 71 +- `deck_template`: 8 +- `example_media_files`: 259 +- `example_pages`: 356 +- `example_project`: 21 +- `icon_library`: 5 +- `icon_svg_files`: 11631 +- `image_palette`: 14 +- `image_reference_collection`: 3 +- `image_rendering`: 20 +- `image_type_template`: 11 +- `layout_template`: 7 +- `narrative_mode`: 5 +- `total_resources`: 191 +- `visual_style`: 18 +- `workflow_reference`: 6 + +## Pattern Kinds + +- `brand_preset`: `brand.anthropic`, `brand.google` +- `chart_template`: `chart.agenda_list`, `chart.arc_anchored_list`, `chart.area_chart`, `chart.bar_chart`, `chart.basic_table`, `chart.box_plot_chart`, `chart.bubble_chart`, `chart.bullet_chart` (+63) +- `deck_template`: `deck.中国电信`, `deck.中国电建_常规`, `deck.中国电建_现代`, `deck.中汽研_商务`, `deck.中汽研_常规`, `deck.中汽研_现代`, `deck.招商银行`, `deck.重庆大学` +- `example_project`: `example.svglide_16x9_attention_is_all_you_need`, `example.svglide_16x9_brutalist_ai_newspaper_2026`, `example.svglide_16x9_building_effective_agents`, `example.svglide_16x9_cangzhuo`, `example.svglide_16x9_fashion_weekly_digest`, `example.svglide_16x9_general_dark_tech_claude_code_auto_mode`, `example.svglide_16x9_glassmorphism_demo`, `example.svglide_16x9_editorial_ai_capital_2026` (+13) +- `icon_library`: `icon_library.chunk-filled`, `icon_library.phosphor-duotone`, `icon_library.simple-icons`, `icon_library.tabler-filled`, `icon_library.tabler-outline` +- `image_palette`: `image_palette.cool-corporate`, `image_palette.dark-cinematic`, `image_palette.duotone`, `image_palette.earthy-dusty`, `image_palette.editorial-classic`, `image_palette.frost-ice`, `image_palette.jewel-tone`, `image_palette.macaron` (+6) +- `image_reference_collection`: `image_reference_collection.palette`, `image_reference_collection.rendering`, `image_reference_collection.type` +- `image_rendering`: `image_rendering.3d-isometric`, `image_rendering.blueprint`, `image_rendering.chalkboard`, `image_rendering.corporate-photo`, `image_rendering.digital-dashboard`, `image_rendering.editorial`, `image_rendering.fantasy-animation`, `image_rendering.flat` (+12) +- `image_type_template`: `image_type.comparison`, `image_type.cycle`, `image_type.flowchart`, `image_type.framework`, `image_type.funnel`, `image_type.infographic`, `image_type.map`, `image_type.matrix` (+3) +- `layout_template`: `layout.academic_defense`, `layout.ai_ops`, `layout.government_blue`, `layout.government_red`, `layout.medical_university`, `layout.pixel_retro`, `layout.psychology_attachment` +- `narrative_mode`: `mode.briefing`, `mode.instructional`, `mode.narrative`, `mode.pyramid`, `mode.showcase` +- `visual_style`: `visual_style.blueprint`, `visual_style.brutalist`, `visual_style.chalkboard`, `visual_style.dark-tech`, `visual_style.data-journalism`, `visual_style.editorial`, `visual_style.glassmorphism`, `visual_style.ink-notes` (+10) +- `workflow_reference`: `workflow.executor-base`, `workflow.image-layout-patterns`, `workflow.image-layout-spec`, `workflow.shared-standards`, `workflow.strategist`, `workflow.visual-review` + +## Runtime Contract + +- Select patterns through `design_pattern_selection.selected_assets`. +- Prove emitted usage through `receipts/design-pattern-usage.json`. +- Store page-level evidence in `page_usages[].component_ids` and `source_trace`. diff --git a/skills/lark-slides/references/ppt-master-asset-map.json b/skills/lark-slides/references/svglide-design-pattern-map.json similarity index 91% rename from skills/lark-slides/references/ppt-master-asset-map.json rename to skills/lark-slides/references/svglide-design-pattern-map.json index d6e0d3f4b..f333c53d9 100644 --- a/skills/lark-slides/references/ppt-master-asset-map.json +++ b/skills/lark-slides/references/svglide-design-pattern-map.json @@ -1,6 +1,6 @@ { - "schema_version": "svglide-ppt-master-asset-map/v1", - "generated_from": "ppt-master", + "schema_version": "svglide-design-pattern-map/v1", + "generated_from": "svglide_internal_design_patterns", "generation_policy": { "runtime_dependency": "none", "raw_svg_default": { @@ -33,22 +33,21 @@ "example_media_files": 259 }, "digests": { - "all_source_files": "42030551448bb8d1", + "all_source_files": "1963903edc7b8e2f", "brands": "e43ae292b361bac3", "layouts": "9be264ac4ac3699b", "decks": "4e5d701c8b5f949e", "charts": "ca05fc31a7a98182", "icons": "bf1ee25d54ae5702", "references": "f7c3b07574ea660b", - "examples": "33acfc6626f61c7e" + "examples": "da027942beb4521d" } }, "resources": [ { "id": "brand.anthropic", - "source_path": "ppt-master/skills/ppt-master/templates/brands/anthropic", + "source_path": "svglide-design-patterns/brand_preset/brand.anthropic", "kind": "brand_preset", - "ppt_master_role": "brand identity preset", "svglide_target": "style-presets.json/brand-visual-tokens", "protocol_compatibility": "needs_normalization", "conversion_strategy": "Extract color, typography, spacing, and brand-tone contracts; do not copy brand marks into runtime output.", @@ -96,13 +95,13 @@ "svg_file_count": 3, "image_file_count": 0, "file_count": 4 - } + }, + "design_role": "brand identity preset" }, { "id": "brand.google", - "source_path": "ppt-master/skills/ppt-master/templates/brands/google", + "source_path": "svglide-design-patterns/brand_preset/brand.google", "kind": "brand_preset", - "ppt_master_role": "brand identity preset", "svglide_target": "style-presets.json/brand-visual-tokens", "protocol_compatibility": "needs_normalization", "conversion_strategy": "Extract color, typography, spacing, and brand-tone contracts; do not copy brand marks into runtime output.", @@ -155,13 +154,13 @@ "svg_file_count": 2, "image_file_count": 0, "file_count": 3 - } + }, + "design_role": "brand identity preset" }, { "id": "chart.agenda_list", - "source_path": "ppt-master/skills/ppt-master/templates/charts/agenda_list.svg", + "source_path": "svglide-design-patterns/chart_template/chart.agenda_list", "kind": "chart_template", - "ppt_master_role": "visualization template", "svglide_target": "svg-recipes.json/component-catalog", "protocol_compatibility": "needs_normalization", "conversion_strategy": "Extract geometry slots and chart role contract; do not copy final SVG as a page template.", @@ -228,13 +227,13 @@ "metadata": { "summary": "Pick for table of contents, meeting agendas, or presentation roadmap — numbered items + brief description + duration / owner per row. Skip for substantive content lists (use vertical_list) or single-page section dividers (use a cover layout).", "view_box": "0 0 1280 720" - } + }, + "design_role": "visualization template" }, { "id": "chart.arc_anchored_list", - "source_path": "ppt-master/skills/ppt-master/templates/charts/arc_anchored_list.svg", + "source_path": "svglide-design-patterns/chart_template/chart.arc_anchored_list", "kind": "chart_template", - "ppt_master_role": "visualization template", "svglide_target": "svg-recipes.json/component-catalog", "protocol_compatibility": "needs_normalization", "conversion_strategy": "Extract geometry slots and chart role contract; do not copy final SVG as a page template.", @@ -301,13 +300,13 @@ "metadata": { "summary": "Pick for 3-5 parallel tracks/pillars unified by a left-edge radial fan as visual anchor (each lane = numbered wedge + title + body + right-end color square). Skip for plain numbered enumeration without unifying core (use vertical_list or numbered_steps), or sequential phases (use process_flow).", "view_box": "0 0 1280 720" - } + }, + "design_role": "visualization template" }, { "id": "chart.area_chart", - "source_path": "ppt-master/skills/ppt-master/templates/charts/area_chart.svg", + "source_path": "svglide-design-patterns/chart_template/chart.area_chart", "kind": "chart_template", - "ppt_master_role": "visualization template", "svglide_target": "svg-recipes.json/component-catalog", "protocol_compatibility": "needs_normalization", "conversion_strategy": "Extract geometry slots and chart role contract; do not copy final SVG as a page template.", @@ -365,13 +364,13 @@ "metadata": { "summary": "Pick for 1-2 cumulative trend series emphasizing volume over time. Skip for >=3 series (use stacked_area_chart) or pure direction without volume (use line_chart).", "view_box": "0 0 1280 720" - } + }, + "design_role": "visualization template" }, { "id": "chart.bar_chart", - "source_path": "ppt-master/skills/ppt-master/templates/charts/bar_chart.svg", + "source_path": "svglide-design-patterns/chart_template/chart.bar_chart", "kind": "chart_template", - "ppt_master_role": "visualization template", "svglide_target": "svg-recipes.json/component-catalog", "protocol_compatibility": "needs_normalization", "conversion_strategy": "Extract geometry slots and chart role contract; do not copy final SVG as a page template.", @@ -429,13 +428,13 @@ "metadata": { "summary": "Pick for single-series category value comparison, 3-8 categories. Skip for >12 long-label items (use horizontal_bar_chart) or multi-series (use grouped_bar_chart).", "view_box": "0 0 1280 720" - } + }, + "design_role": "visualization template" }, { "id": "chart.basic_table", - "source_path": "ppt-master/skills/ppt-master/templates/charts/basic_table.svg", + "source_path": "svglide-design-patterns/chart_template/chart.basic_table", "kind": "chart_template", - "ppt_master_role": "visualization template", "svglide_target": "svg-recipes.json/component-catalog", "protocol_compatibility": "needs_normalization", "conversion_strategy": "Extract geometry slots and chart role contract; do not copy final SVG as a page template.", @@ -496,13 +495,13 @@ "metadata": { "summary": "Pick for plain tabular text/number grid, 3-8 columns. Skip if cells need visual bars (use consulting_table) or qualitative scores (use harvey_balls_table).", "view_box": "0 0 1280 720" - } + }, + "design_role": "visualization template" }, { "id": "chart.box_plot_chart", - "source_path": "ppt-master/skills/ppt-master/templates/charts/box_plot_chart.svg", + "source_path": "svglide-design-patterns/chart_template/chart.box_plot_chart", "kind": "chart_template", - "ppt_master_role": "visualization template", "svglide_target": "svg-recipes.json/component-catalog", "protocol_compatibility": "needs_normalization", "conversion_strategy": "Extract geometry slots and chart role contract; do not copy final SVG as a page template.", @@ -558,13 +557,13 @@ "metadata": { "summary": "Pick for distribution shape per group (median, quartiles, outliers). Skip if only the average per group matters (use bar_chart).", "view_box": "0 0 1280 720" - } + }, + "design_role": "visualization template" }, { "id": "chart.bubble_chart", - "source_path": "ppt-master/skills/ppt-master/templates/charts/bubble_chart.svg", + "source_path": "svglide-design-patterns/chart_template/chart.bubble_chart", "kind": "chart_template", - "ppt_master_role": "visualization template", "svglide_target": "svg-recipes.json/component-catalog", "protocol_compatibility": "needs_normalization", "conversion_strategy": "Extract geometry slots and chart role contract; do not copy final SVG as a page template.", @@ -616,13 +615,13 @@ "metadata": { "summary": "Pick for 3-axis data (x position, y position, size). Skip for plain x-y correlation (use scatter_chart).", "view_box": "0 0 1280 720" - } + }, + "design_role": "visualization template" }, { "id": "chart.bullet_chart", - "source_path": "ppt-master/skills/ppt-master/templates/charts/bullet_chart.svg", + "source_path": "svglide-design-patterns/chart_template/chart.bullet_chart", "kind": "chart_template", - "ppt_master_role": "visualization template", "svglide_target": "svg-recipes.json/component-catalog", "protocol_compatibility": "needs_normalization", "conversion_strategy": "Extract geometry slots and chart role contract; do not copy final SVG as a page template.", @@ -684,13 +683,13 @@ "metadata": { "summary": "Pick for 3-7 KPIs each with explicit target + actual. Skip for single metric (use gauge_chart) or items with only completion % and no target baseline (use progress_bar_chart).", "view_box": "0 0 1280 720" - } + }, + "design_role": "visualization template" }, { "id": "chart.butterfly_chart", - "source_path": "ppt-master/skills/ppt-master/templates/charts/butterfly_chart.svg", + "source_path": "svglide-design-patterns/chart_template/chart.butterfly_chart", "kind": "chart_template", - "ppt_master_role": "visualization template", "svglide_target": "svg-recipes.json/component-catalog", "protocol_compatibility": "needs_normalization", "conversion_strategy": "Extract geometry slots and chart role contract; do not copy final SVG as a page template.", @@ -749,13 +748,13 @@ "metadata": { "summary": "Pick for two mirrored datasets sharing a common axis (age pyramid, A/B, income vs expense). Skip for >2 sides (use grouped_bar_chart).", "view_box": "0 0 1280 720" - } + }, + "design_role": "visualization template" }, { "id": "chart.chevron_chain_with_tail", - "source_path": "ppt-master/skills/ppt-master/templates/charts/chevron_chain_with_tail.svg", + "source_path": "svglide-design-patterns/chart_template/chart.chevron_chain_with_tail", "kind": "chart_template", - "ppt_master_role": "visualization template", "svglide_target": "svg-recipes.json/component-catalog", "protocol_compatibility": "needs_normalization", "conversion_strategy": "Extract geometry slots and chart role contract; do not copy final SVG as a page template.", @@ -823,13 +822,13 @@ "metadata": { "summary": "Pick for 4-6 sequential chevron blocks plus a final wedge representing aggregate outcome — Porter's value chain (primary + support activities mapped to margin), process leading to a summary deliverable, contribution chain to a result. Skip for plain chevron flow without summary tail (use chevron_process) or non-chevron pipeline (use pipeline_with_stages).", "view_box": "0 0 1280 720" - } + }, + "design_role": "visualization template" }, { "id": "chart.chevron_process", - "source_path": "ppt-master/skills/ppt-master/templates/charts/chevron_process.svg", + "source_path": "svglide-design-patterns/chart_template/chart.chevron_process", "kind": "chart_template", - "ppt_master_role": "visualization template", "svglide_target": "svg-recipes.json/component-catalog", "protocol_compatibility": "needs_normalization", "conversion_strategy": "Extract geometry slots and chart role contract; do not copy final SVG as a page template.", @@ -897,13 +896,13 @@ "metadata": { "summary": "Pick for 3-6 phase methodology with chunky arrow-chain progression and deliverables per phase. Skip for <=2 phases or non-linear flow (use process_flow), or chain ending in an aggregate outcome wedge (use chevron_chain_with_tail).", "view_box": "0 0 1280 720" - } + }, + "design_role": "visualization template" }, { "id": "chart.circular_stages", - "source_path": "ppt-master/skills/ppt-master/templates/charts/circular_stages.svg", + "source_path": "svglide-design-patterns/chart_template/chart.circular_stages", "kind": "chart_template", - "ppt_master_role": "visualization template", "svglide_target": "svg-recipes.json/component-catalog", "protocol_compatibility": "needs_normalization", "conversion_strategy": "Extract geometry slots and chart role contract; do not copy final SVG as a page template.", @@ -971,13 +970,13 @@ "metadata": { "summary": "Pick for 4-6 stage closed loop where stages compose a cycle — PDCA, flywheel compounding loops (Attract → Engage → Delight), lifecycle, continuous improvement. Skip for linear flow (use process_flow), one-shot sequence (use numbered_steps), or wedge-based central topic (use segmented_wheel).", "view_box": "0 0 1280 720" - } + }, + "design_role": "visualization template" }, { "id": "chart.client_server_flow", - "source_path": "ppt-master/skills/ppt-master/templates/charts/client_server_flow.svg", + "source_path": "svglide-design-patterns/chart_template/chart.client_server_flow", "kind": "chart_template", - "ppt_master_role": "visualization template", "svglide_target": "svg-recipes.json/component-catalog", "protocol_compatibility": "needs_normalization", "conversion_strategy": "Extract geometry slots and chart role contract; do not copy final SVG as a page template.", @@ -1045,13 +1044,13 @@ "metadata": { "summary": "Pick for left-side clients + right-side servers with labeled bidirectional arrows for key interactions (request/response/push). Each module = name + 1-line description; each arrow must have an action label. Skip for non-distributed flows (use process_flow).", "view_box": "0 0 1280 720" - } + }, + "design_role": "visualization template" }, { "id": "chart.comparison_columns", - "source_path": "ppt-master/skills/ppt-master/templates/charts/comparison_columns.svg", + "source_path": "svglide-design-patterns/chart_template/chart.comparison_columns", "kind": "chart_template", - "ppt_master_role": "visualization template", "svglide_target": "svg-recipes.json/component-catalog", "protocol_compatibility": "needs_normalization", "conversion_strategy": "Extract geometry slots and chart role contract; do not copy final SVG as a page template.", @@ -1108,13 +1107,13 @@ "metadata": { "summary": "Pick for 2-4 pricing/service tier cards in side-by-side columns (marketing layout). Skip for dense feature comparison (use comparison_table).", "view_box": "0 0 1280 720" - } + }, + "design_role": "visualization template" }, { "id": "chart.comparison_table", - "source_path": "ppt-master/skills/ppt-master/templates/charts/comparison_table.svg", + "source_path": "svglide-design-patterns/chart_template/chart.comparison_table", "kind": "chart_template", - "ppt_master_role": "visualization template", "svglide_target": "svg-recipes.json/component-catalog", "protocol_compatibility": "needs_normalization", "conversion_strategy": "Extract geometry slots and chart role contract; do not copy final SVG as a page template.", @@ -1173,13 +1172,13 @@ "metadata": { "summary": "Pick for 2-4 plans/products compared across many feature rows (dense matrix). Skip for pricing-tier marketing layout (use comparison_columns).", "view_box": "0 0 1280 720" - } + }, + "design_role": "visualization template" }, { "id": "chart.concentric_circles", - "source_path": "ppt-master/skills/ppt-master/templates/charts/concentric_circles.svg", + "source_path": "svglide-design-patterns/chart_template/chart.concentric_circles", "kind": "chart_template", - "ppt_master_role": "visualization template", "svglide_target": "svg-recipes.json/component-catalog", "protocol_compatibility": "needs_normalization", "conversion_strategy": "Extract geometry slots and chart role contract; do not copy final SVG as a page template.", @@ -1246,13 +1245,13 @@ "metadata": { "summary": "Pick for 3-5 priority rings from core to periphery — bullseye/onion model, sphere of influence, stakeholder rings, core-to-ecosystem priority layers, target-vs-extended audience. Skip for non-prioritized layers (use pyramid_chart).", "view_box": "0 0 1280 720" - } + }, + "design_role": "visualization template" }, { "id": "chart.consulting_table", - "source_path": "ppt-master/skills/ppt-master/templates/charts/consulting_table.svg", + "source_path": "svglide-design-patterns/chart_template/chart.consulting_table", "kind": "chart_template", - "ppt_master_role": "visualization template", "svglide_target": "svg-recipes.json/component-catalog", "protocol_compatibility": "needs_normalization", "conversion_strategy": "Extract geometry slots and chart role contract; do not copy final SVG as a page template.", @@ -1309,13 +1308,13 @@ "metadata": { "summary": "Pick for high-density tables with embedded micro bar visuals (consulting/financial reports). Skip for plain text data (use basic_table).", "view_box": "0 0 1280 720" - } + }, + "design_role": "visualization template" }, { "id": "chart.donut_chart", - "source_path": "ppt-master/skills/ppt-master/templates/charts/donut_chart.svg", + "source_path": "svglide-design-patterns/chart_template/chart.donut_chart", "kind": "chart_template", - "ppt_master_role": "visualization template", "svglide_target": "svg-recipes.json/component-catalog", "protocol_compatibility": "needs_normalization", "conversion_strategy": "Extract geometry slots and chart role contract; do not copy final SVG as a page template.", @@ -1374,13 +1373,13 @@ "metadata": { "summary": "Pick for 3-6 part proportions where a center KPI/total deserves emphasis. Skip if no center value to feature (use pie_chart).", "view_box": "0 0 1280 720" - } + }, + "design_role": "visualization template" }, { "id": "chart.dual_axis_line_chart", - "source_path": "ppt-master/skills/ppt-master/templates/charts/dual_axis_line_chart.svg", + "source_path": "svglide-design-patterns/chart_template/chart.dual_axis_line_chart", "kind": "chart_template", - "ppt_master_role": "visualization template", "svglide_target": "svg-recipes.json/component-catalog", "protocol_compatibility": "needs_normalization", "conversion_strategy": "Extract geometry slots and chart role contract; do not copy final SVG as a page template.", @@ -1441,13 +1440,13 @@ "metadata": { "summary": "Pick when 2 metrics with different units/scales must be compared over time. Skip if both metrics share the same unit (use line_chart).", "view_box": "0 0 1280 720" - } + }, + "design_role": "visualization template" }, { "id": "chart.dumbbell_chart", - "source_path": "ppt-master/skills/ppt-master/templates/charts/dumbbell_chart.svg", + "source_path": "svglide-design-patterns/chart_template/chart.dumbbell_chart", "kind": "chart_template", - "ppt_master_role": "visualization template", "svglide_target": "svg-recipes.json/component-catalog", "protocol_compatibility": "needs_normalization", "conversion_strategy": "Extract geometry slots and chart role contract; do not copy final SVG as a page template.", @@ -1505,13 +1504,13 @@ "metadata": { "summary": "Pick for before-vs-after or two-state difference across 5-10 items. Skip for single snapshot (use bar_chart) or 3+ states (use grouped_bar_chart).", "view_box": "0 0 1280 720" - } + }, + "design_role": "visualization template" }, { "id": "chart.feature_matrix_table", - "source_path": "ppt-master/skills/ppt-master/templates/charts/feature_matrix_table.svg", + "source_path": "svglide-design-patterns/chart_template/chart.feature_matrix_table", "kind": "chart_template", - "ppt_master_role": "visualization template", "svglide_target": "svg-recipes.json/component-catalog", "protocol_compatibility": "needs_normalization", "conversion_strategy": "Extract geometry slots and chart role contract; do not copy final SVG as a page template.", @@ -1571,13 +1570,13 @@ "metadata": { "summary": "Pick for competitive feature checklist with checkmarks across products. Skip for qualitative scores (use harvey_balls_table) or pricing tier marketing (use comparison_columns).", "view_box": "0 0 1280 720" - } + }, + "design_role": "visualization template" }, { "id": "chart.financial_statement_table", - "source_path": "ppt-master/skills/ppt-master/templates/charts/financial_statement_table.svg", + "source_path": "svglide-design-patterns/chart_template/chart.financial_statement_table", "kind": "chart_template", - "ppt_master_role": "visualization template", "svglide_target": "svg-recipes.json/component-catalog", "protocol_compatibility": "needs_normalization", "conversion_strategy": "Extract geometry slots and chart role contract; do not copy final SVG as a page template.", @@ -1637,13 +1636,13 @@ "metadata": { "summary": "Pick for income statement / balance sheet style with right-aligned numbers and strong total rows. Skip for non-financial tabular data (use basic_table).", "view_box": "0 0 1280 720" - } + }, + "design_role": "visualization template" }, { "id": "chart.fishbone_diagram", - "source_path": "ppt-master/skills/ppt-master/templates/charts/fishbone_diagram.svg", + "source_path": "svglide-design-patterns/chart_template/chart.fishbone_diagram", "kind": "chart_template", - "ppt_master_role": "visualization template", "svglide_target": "svg-recipes.json/component-catalog", "protocol_compatibility": "needs_normalization", "conversion_strategy": "Extract geometry slots and chart role contract; do not copy final SVG as a page template.", @@ -1700,13 +1699,13 @@ "metadata": { "summary": "Pick for cause-and-effect root cause analysis with 4-6 cause branches (Ishikawa, 6M). Skip for sequential flow (use process_flow).", "view_box": "0 0 1280 720" - } + }, + "design_role": "visualization template" }, { "id": "chart.funnel_chart", - "source_path": "ppt-master/skills/ppt-master/templates/charts/funnel_chart.svg", + "source_path": "svglide-design-patterns/chart_template/chart.funnel_chart", "kind": "chart_template", - "ppt_master_role": "visualization template", "svglide_target": "svg-recipes.json/component-catalog", "protocol_compatibility": "needs_normalization", "conversion_strategy": "Extract geometry slots and chart role contract; do not copy final SVG as a page template.", @@ -1769,13 +1768,13 @@ "metadata": { "summary": "Pick for 3-5 sequential conversion stages with monotonic drop-off. Skip if flow branches/merges (use sankey_chart) or steps don't entail loss (use process_flow).", "view_box": "0 0 1280 720" - } + }, + "design_role": "visualization template" }, { "id": "chart.gantt_chart", - "source_path": "ppt-master/skills/ppt-master/templates/charts/gantt_chart.svg", + "source_path": "svglide-design-patterns/chart_template/chart.gantt_chart", "kind": "chart_template", - "ppt_master_role": "visualization template", "svglide_target": "svg-recipes.json/component-catalog", "protocol_compatibility": "needs_normalization", "conversion_strategy": "Extract geometry slots and chart role contract; do not copy final SVG as a page template.", @@ -1834,13 +1833,13 @@ "metadata": { "summary": "Pick for project schedule with 6-12 tasks, durations, and dependencies. Skip for simple milestones without duration (use timeline) or vertical roadmap (use roadmap_vertical).", "view_box": "0 0 1280 720" - } + }, + "design_role": "visualization template" }, { "id": "chart.gauge_chart", - "source_path": "ppt-master/skills/ppt-master/templates/charts/gauge_chart.svg", + "source_path": "svglide-design-patterns/chart_template/chart.gauge_chart", "kind": "chart_template", - "ppt_master_role": "visualization template", "svglide_target": "svg-recipes.json/component-catalog", "protocol_compatibility": "needs_normalization", "conversion_strategy": "Extract geometry slots and chart role contract; do not copy final SVG as a page template.", @@ -1898,13 +1897,13 @@ "metadata": { "summary": "Pick for one hero metric's goal achievement rate. Skip for multiple metrics (use bullet_chart for target+actual or progress_bar_chart for completion %).", "view_box": "0 0 1280 720" - } + }, + "design_role": "visualization template" }, { "id": "chart.grouped_bar_chart", - "source_path": "ppt-master/skills/ppt-master/templates/charts/grouped_bar_chart.svg", + "source_path": "svglide-design-patterns/chart_template/chart.grouped_bar_chart", "kind": "chart_template", - "ppt_master_role": "visualization template", "svglide_target": "svg-recipes.json/component-catalog", "protocol_compatibility": "needs_normalization", "conversion_strategy": "Extract geometry slots and chart role contract; do not copy final SVG as a page template.", @@ -1967,13 +1966,13 @@ "metadata": { "summary": "Pick for 2-4 series side-by-side across the same categories (e.g. YoY/QoQ). Skip if showing composition within each category (use stacked_bar_chart).", "view_box": "0 0 1280 720" - } + }, + "design_role": "visualization template" }, { "id": "chart.harvey_balls_table", - "source_path": "ppt-master/skills/ppt-master/templates/charts/harvey_balls_table.svg", + "source_path": "svglide-design-patterns/chart_template/chart.harvey_balls_table", "kind": "chart_template", - "ppt_master_role": "visualization template", "svglide_target": "svg-recipes.json/component-catalog", "protocol_compatibility": "needs_normalization", "conversion_strategy": "Extract geometry slots and chart role contract; do not copy final SVG as a page template.", @@ -2034,13 +2033,13 @@ "metadata": { "summary": "Pick for qualitative scoring grid using 0-100% Harvey balls (vendor/skill assessment). Skip for binary checkmarks (use feature_matrix_table) or numeric values (use basic_table).", "view_box": "0 0 1280 720" - } + }, + "design_role": "visualization template" }, { "id": "chart.heatmap_chart", - "source_path": "ppt-master/skills/ppt-master/templates/charts/heatmap_chart.svg", + "source_path": "svglide-design-patterns/chart_template/chart.heatmap_chart", "kind": "chart_template", - "ppt_master_role": "visualization template", "svglide_target": "svg-recipes.json/component-catalog", "protocol_compatibility": "needs_normalization", "conversion_strategy": "Extract geometry slots and chart role contract; do not copy final SVG as a page template.", @@ -2097,13 +2096,13 @@ "metadata": { "summary": "Pick for 2D matrix where each cell is an intensity value (activity grid, correlation matrix). Skip for ranked categories (use bar_chart).", "view_box": "0 0 1280 720" - } + }, + "design_role": "visualization template" }, { "id": "chart.horizontal_bar_chart", - "source_path": "ppt-master/skills/ppt-master/templates/charts/horizontal_bar_chart.svg", + "source_path": "svglide-design-patterns/chart_template/chart.horizontal_bar_chart", "kind": "chart_template", - "ppt_master_role": "visualization template", "svglide_target": "svg-recipes.json/component-catalog", "protocol_compatibility": "needs_normalization", "conversion_strategy": "Extract geometry slots and chart role contract; do not copy final SVG as a page template.", @@ -2158,13 +2157,13 @@ "metadata": { "summary": "Pick for ranking 5-12 items, especially with long labels. Skip if <=8 short-label items (use bar_chart).", "view_box": "0 0 1280 720" - } + }, + "design_role": "visualization template" }, { "id": "chart.hub_inward_arrows", - "source_path": "ppt-master/skills/ppt-master/templates/charts/hub_inward_arrows.svg", + "source_path": "svglide-design-patterns/chart_template/chart.hub_inward_arrows", "kind": "chart_template", - "ppt_master_role": "visualization template", "svglide_target": "svg-recipes.json/component-catalog", "protocol_compatibility": "needs_normalization", "conversion_strategy": "Extract geometry slots and chart role contract; do not copy final SVG as a page template.", @@ -2232,13 +2231,13 @@ "metadata": { "summary": "Pick for 1 center subject + 4-5 surrounding forces with arrows pointing inward — Porter's Five Forces (central rivalry, threat of new entrants/substitutes, bargaining power of buyers/suppliers), threat-model with central asset, pressure-on-core diagrams. Skip for outward radiation from center capability (use hub_spoke) or non-directional cluster (use mind_map).", "view_box": "0 0 1280 720" - } + }, + "design_role": "visualization template" }, { "id": "chart.hub_spoke", - "source_path": "ppt-master/skills/ppt-master/templates/charts/hub_spoke.svg", + "source_path": "svglide-design-patterns/chart_template/chart.hub_spoke", "kind": "chart_template", - "ppt_master_role": "visualization template", "svglide_target": "svg-recipes.json/component-catalog", "protocol_compatibility": "needs_normalization", "conversion_strategy": "Extract geometry slots and chart role contract; do not copy final SVG as a page template.", @@ -2306,13 +2305,13 @@ "metadata": { "summary": "Pick for 1 core capability + 4-8 surrounding capabilities (platform/ecosystem); each spoke = title or title + 1-2 line description. Skip if center is a system containing parts with their own descriptions (use module_composition), or surroundings exert inward pressure on the center (use hub_inward_arrows).", "view_box": "0 0 1280 720" - } + }, + "design_role": "visualization template" }, { "id": "chart.icon_grid", - "source_path": "ppt-master/skills/ppt-master/templates/charts/icon_grid.svg", + "source_path": "svglide-design-patterns/chart_template/chart.icon_grid", "kind": "chart_template", - "ppt_master_role": "visualization template", "svglide_target": "svg-recipes.json/component-catalog", "protocol_compatibility": "needs_normalization", "conversion_strategy": "Extract geometry slots and chart role contract; do not copy final SVG as a page template.", @@ -2380,13 +2379,13 @@ "metadata": { "summary": "Pick for 4-9 parallel features/capabilities/services as icon cards — feature grid, service lineup, benefits matrix, brand values, product highlights. Skip for sequential ordering (use numbered_steps) or hierarchical layers (use pyramid_chart).", "view_box": "0 0 1280 720" - } + }, + "design_role": "visualization template" }, { "id": "chart.isometric_stairs", - "source_path": "ppt-master/skills/ppt-master/templates/charts/isometric_stairs.svg", + "source_path": "svglide-design-patterns/chart_template/chart.isometric_stairs", "kind": "chart_template", - "ppt_master_role": "visualization template", "svglide_target": "svg-recipes.json/component-catalog", "protocol_compatibility": "needs_normalization", "conversion_strategy": "Extract geometry slots and chart role contract; do not copy final SVG as a page template.", @@ -2446,13 +2445,13 @@ "metadata": { "summary": "Pick for 4-7 ascending stages emphasizing growth/maturity progression visually. Skip for flat sequential steps (use numbered_steps) or formal hierarchy (use pyramid_chart).", "view_box": "0 0 1280 720" - } + }, + "design_role": "visualization template" }, { "id": "chart.journey_map", - "source_path": "ppt-master/skills/ppt-master/templates/charts/journey_map.svg", + "source_path": "svglide-design-patterns/chart_template/chart.journey_map", "kind": "chart_template", - "ppt_master_role": "visualization template", "svglide_target": "svg-recipes.json/component-catalog", "protocol_compatibility": "needs_normalization", "conversion_strategy": "Extract geometry slots and chart role contract; do not copy final SVG as a page template.", @@ -2510,13 +2509,13 @@ "metadata": { "summary": "Pick for multi-phase customer experience matrix with actions, emotion curve, and pain points per phase. Skip for simple linear funnel (use funnel_chart).", "view_box": "0 0 1280 720" - } + }, + "design_role": "visualization template" }, { "id": "chart.kpi_cards", - "source_path": "ppt-master/skills/ppt-master/templates/charts/kpi_cards.svg", + "source_path": "svglide-design-patterns/chart_template/chart.kpi_cards", "kind": "chart_template", - "ppt_master_role": "visualization template", "svglide_target": "svg-recipes.json/component-catalog", "protocol_compatibility": "needs_normalization", "conversion_strategy": "Extract geometry slots and chart role contract; do not copy final SVG as a page template.", @@ -2584,13 +2583,13 @@ "metadata": { "summary": "Pick for 4-8 standalone numeric metrics shown as overview cards (2x2 or 1x4) — exec summary opener, dashboard headline, quarterly recap, results-at-a-glance. Skip if metrics have target baselines (use bullet_chart) or single hero number (use gauge_chart).", "view_box": "0 0 1280 720" - } + }, + "design_role": "visualization template" }, { "id": "chart.labeled_card", - "source_path": "ppt-master/skills/ppt-master/templates/charts/labeled_card.svg", + "source_path": "svglide-design-patterns/chart_template/chart.labeled_card", "kind": "chart_template", - "ppt_master_role": "visualization template", "svglide_target": "svg-recipes.json/component-catalog", "protocol_compatibility": "needs_normalization", "conversion_strategy": "Extract geometry slots and chart role contract; do not copy final SVG as a page template.", @@ -2658,13 +2657,13 @@ "metadata": { "summary": "Pick for 3-4 parallel aspects of one subject with per-aspect titles + short body (self-introduction, four-pillar overview, capability quadrant). Skip for plain feature lists (use icon_grid), sequential steps (use numbered_steps), or strategic quadrants (use quadrant_text_bullets / matrix_2x2).", "view_box": "0 0 1280 720" - } + }, + "design_role": "visualization template" }, { "id": "chart.layered_architecture", - "source_path": "ppt-master/skills/ppt-master/templates/charts/layered_architecture.svg", + "source_path": "svglide-design-patterns/chart_template/chart.layered_architecture", "kind": "chart_template", - "ppt_master_role": "visualization template", "svglide_target": "svg-recipes.json/component-catalog", "protocol_compatibility": "needs_normalization", "conversion_strategy": "Extract geometry slots and chart role contract; do not copy final SVG as a page template.", @@ -2732,13 +2731,13 @@ "metadata": { "summary": "Pick for 3-4 horizontal architecture layers (presentation/service/data), 2-4 module cards per layer, each card = title + 1-line description (description required, even if source brief). Skip if no per-module descriptions (use icon_grid) or no horizontal layering (use module_composition).", "view_box": "0 0 1280 720" - } + }, + "design_role": "visualization template" }, { "id": "chart.line_chart", - "source_path": "ppt-master/skills/ppt-master/templates/charts/line_chart.svg", + "source_path": "svglide-design-patterns/chart_template/chart.line_chart", "kind": "chart_template", - "ppt_master_role": "visualization template", "svglide_target": "svg-recipes.json/component-catalog", "protocol_compatibility": "needs_normalization", "conversion_strategy": "Extract geometry slots and chart role contract; do not copy final SVG as a page template.", @@ -2799,13 +2798,13 @@ "metadata": { "summary": "Pick for 1-3 time-series on a continuous axis showing direction. Skip if cumulative volume matters (use area_chart) or different units (use dual_axis_line_chart).", "view_box": "0 0 1280 720" - } + }, + "design_role": "visualization template" }, { "id": "chart.matrix_2x2", - "source_path": "ppt-master/skills/ppt-master/templates/charts/matrix_2x2.svg", + "source_path": "svglide-design-patterns/chart_template/chart.matrix_2x2", "kind": "chart_template", - "ppt_master_role": "visualization template", "svglide_target": "svg-recipes.json/component-catalog", "protocol_compatibility": "needs_normalization", "conversion_strategy": "Extract geometry slots and chart role contract; do not copy final SVG as a page template.", @@ -2869,13 +2868,13 @@ "metadata": { "summary": "Pick for items plotted on Impact x Effort or similar generic 2-axis prioritization. Skip for named-quadrant text frameworks (use quadrant_text_bullets) or bubble-sized portfolios (use quadrant_bubble_scatter).", "view_box": "0 0 1280 720" - } + }, + "design_role": "visualization template" }, { "id": "chart.mind_map", - "source_path": "ppt-master/skills/ppt-master/templates/charts/mind_map.svg", + "source_path": "svglide-design-patterns/chart_template/chart.mind_map", "kind": "chart_template", - "ppt_master_role": "visualization template", "svglide_target": "svg-recipes.json/component-catalog", "protocol_compatibility": "needs_normalization", "conversion_strategy": "Extract geometry slots and chart role contract; do not copy final SVG as a page template.", @@ -2937,13 +2936,13 @@ "metadata": { "summary": "Pick for one core topic radiating into 3-6 brainstorm branches. Skip for capability hub-and-spoke (use hub_spoke) or strict hierarchy (use top_down_tree).", "view_box": "0 0 1280 720" - } + }, + "design_role": "visualization template" }, { "id": "chart.module_composition", - "source_path": "ppt-master/skills/ppt-master/templates/charts/module_composition.svg", + "source_path": "svglide-design-patterns/chart_template/chart.module_composition", "kind": "chart_template", - "ppt_master_role": "visualization template", "svglide_target": "svg-recipes.json/component-catalog", "protocol_compatibility": "needs_normalization", "conversion_strategy": "Extract geometry slots and chart role contract; do not copy final SVG as a page template.", @@ -3011,13 +3010,13 @@ "metadata": { "summary": "Pick for one parent container wrapping 3-N child module cards, each = title + 2-3 bullets — fits 'Feature X contains 3 parts, each with its own description'. Skip if source has only labels without descriptions (use numbered_steps or icon_grid).", "view_box": "0 0 1280 720" - } + }, + "design_role": "visualization template" }, { "id": "chart.numbered_steps", - "source_path": "ppt-master/skills/ppt-master/templates/charts/numbered_steps.svg", + "source_path": "svglide-design-patterns/chart_template/chart.numbered_steps", "kind": "chart_template", - "ppt_master_role": "visualization template", "svglide_target": "svg-recipes.json/component-catalog", "protocol_compatibility": "needs_normalization", "conversion_strategy": "Extract geometry slots and chart role contract; do not copy final SVG as a page template.", @@ -3085,13 +3084,13 @@ "metadata": { "summary": "Pick for 3-6 horizontal sequential steps with numeric emphasis — how-it-works section, getting-started guide, methodology overview, implementation phases. Skip if steps need connector arrows (use process_flow) or named output artifacts (use pipeline_with_stages).", "view_box": "0 0 1280 720" - } + }, + "design_role": "visualization template" }, { "id": "chart.pareto_chart", - "source_path": "ppt-master/skills/ppt-master/templates/charts/pareto_chart.svg", + "source_path": "svglide-design-patterns/chart_template/chart.pareto_chart", "kind": "chart_template", - "ppt_master_role": "visualization template", "svglide_target": "svg-recipes.json/component-catalog", "protocol_compatibility": "needs_normalization", "conversion_strategy": "Extract geometry slots and chart role contract; do not copy final SVG as a page template.", @@ -3146,13 +3145,13 @@ "metadata": { "summary": "Pick to show 80/20 contribution: descending bars + cumulative line. Skip if cumulative line is not the message (use bar_chart).", "view_box": "0 0 1280 720" - } + }, + "design_role": "visualization template" }, { "id": "chart.pie_chart", - "source_path": "ppt-master/skills/ppt-master/templates/charts/pie_chart.svg", + "source_path": "svglide-design-patterns/chart_template/chart.pie_chart", "kind": "chart_template", - "ppt_master_role": "visualization template", "svglide_target": "svg-recipes.json/component-catalog", "protocol_compatibility": "needs_normalization", "conversion_strategy": "Extract geometry slots and chart role contract; do not copy final SVG as a page template.", @@ -3209,13 +3208,13 @@ "metadata": { "summary": "Pick for simple 3-6 part proportions of one whole. Skip for >=7 parts (use donut_chart or treemap_chart) or hierarchical composition (use treemap_chart).", "view_box": "0 0 1280 720" - } + }, + "design_role": "visualization template" }, { "id": "chart.pipeline_with_stages", - "source_path": "ppt-master/skills/ppt-master/templates/charts/pipeline_with_stages.svg", + "source_path": "svglide-design-patterns/chart_template/chart.pipeline_with_stages", "kind": "chart_template", - "ppt_master_role": "visualization template", "svglide_target": "svg-recipes.json/component-catalog", "protocol_compatibility": "needs_normalization", "conversion_strategy": "Extract geometry slots and chart role contract; do not copy final SVG as a page template.", @@ -3283,13 +3282,13 @@ "metadata": { "summary": "Pick for 3-5 horizontal pipeline stages, each = title + 1-line description + output artifact, connected by arrows (data pipelines, ETL, build pipelines). Skip if any stage lacks an artifact (use process_flow or numbered_steps).", "view_box": "0 0 1280 720" - } + }, + "design_role": "visualization template" }, { "id": "chart.process_flow", - "source_path": "ppt-master/skills/ppt-master/templates/charts/process_flow.svg", + "source_path": "svglide-design-patterns/chart_template/chart.process_flow", "kind": "chart_template", - "ppt_master_role": "visualization template", "svglide_target": "svg-recipes.json/component-catalog", "protocol_compatibility": "needs_normalization", "conversion_strategy": "Extract geometry slots and chart role contract; do not copy final SVG as a page template.", @@ -3357,13 +3356,13 @@ "metadata": { "summary": "Pick for 3-8 sequential steps connected by simple arrows — approval workflows, customer onboarding, request handling, lifecycle stages. Skip if cyclical (use circular_stages) or stages produce named outputs (use pipeline_with_stages).", "view_box": "0 0 1280 720" - } + }, + "design_role": "visualization template" }, { "id": "chart.progress_bar_chart", - "source_path": "ppt-master/skills/ppt-master/templates/charts/progress_bar_chart.svg", + "source_path": "svglide-design-patterns/chart_template/chart.progress_bar_chart", "kind": "chart_template", - "ppt_master_role": "visualization template", "svglide_target": "svg-recipes.json/component-catalog", "protocol_compatibility": "needs_normalization", "conversion_strategy": "Extract geometry slots and chart role contract; do not copy final SVG as a page template.", @@ -3420,13 +3419,13 @@ "metadata": { "summary": "Pick for 3-8 items each with a completion %. Skip if items have explicit target+actual values (use bullet_chart).", "view_box": "0 0 1280 720" - } + }, + "design_role": "visualization template" }, { "id": "chart.project_schedule_table", - "source_path": "ppt-master/skills/ppt-master/templates/charts/project_schedule_table.svg", + "source_path": "svglide-design-patterns/chart_template/chart.project_schedule_table", "kind": "chart_template", - "ppt_master_role": "visualization template", "svglide_target": "svg-recipes.json/component-catalog", "protocol_compatibility": "needs_normalization", "conversion_strategy": "Extract geometry slots and chart role contract; do not copy final SVG as a page template.", @@ -3485,13 +3484,13 @@ "metadata": { "summary": "Pick for table-style task tracker (task / owner / status / timeline). Skip for true Gantt with dependencies (use gantt_chart) or schedule without ownership (use roadmap_vertical).", "view_box": "0 0 1280 720" - } + }, + "design_role": "visualization template" }, { "id": "chart.pros_cons_chart", - "source_path": "ppt-master/skills/ppt-master/templates/charts/pros_cons_chart.svg", + "source_path": "svglide-design-patterns/chart_template/chart.pros_cons_chart", "kind": "chart_template", - "ppt_master_role": "visualization template", "svglide_target": "svg-recipes.json/component-catalog", "protocol_compatibility": "needs_normalization", "conversion_strategy": "Extract geometry slots and chart role contract; do not copy final SVG as a page template.", @@ -3552,13 +3551,13 @@ "metadata": { "summary": "Pick for bilateral pros/cons list, 2-5 items per side. Skip for full feature comparison (use comparison_table) or numeric A/B mirror data (use butterfly_chart).", "view_box": "0 0 1280 720" - } + }, + "design_role": "visualization template" }, { "id": "chart.pyramid_chart", - "source_path": "ppt-master/skills/ppt-master/templates/charts/pyramid_chart.svg", + "source_path": "svglide-design-patterns/chart_template/chart.pyramid_chart", "kind": "chart_template", - "ppt_master_role": "visualization template", "svglide_target": "svg-recipes.json/component-catalog", "protocol_compatibility": "needs_normalization", "conversion_strategy": "Extract geometry slots and chart role contract; do not copy final SVG as a page template.", @@ -3626,13 +3625,13 @@ "metadata": { "summary": "Pick for 3-6 stratified hierarchy layers in flat 2D side-view — Maslow's hierarchy, maturity models, value hierarchy, capability tiers, market segments, audience pyramid. Skip for dramatic tone (use pyramid_isometric), flat priority list (use vertical_list), or org reporting (use top_down_tree).", "view_box": "0 0 1280 720" - } + }, + "design_role": "visualization template" }, { "id": "chart.pyramid_isometric", - "source_path": "ppt-master/skills/ppt-master/templates/charts/pyramid_isometric.svg", + "source_path": "svglide-design-patterns/chart_template/chart.pyramid_isometric", "kind": "chart_template", - "ppt_master_role": "visualization template", "svglide_target": "svg-recipes.json/component-catalog", "protocol_compatibility": "needs_normalization", "conversion_strategy": "Extract geometry slots and chart role contract; do not copy final SVG as a page template.", @@ -3696,13 +3695,13 @@ "metadata": { "summary": "Pick for 4-tier maturity/achievement model with presentation drama (review reports, capability tiers). Tier count fixed at 4 (hand-tuned vertices). Skip for documentary tone (use pyramid_chart) or tier count != 4.", "view_box": "0 0 1280 720" - } + }, + "design_role": "visualization template" }, { "id": "chart.quadrant_bubble_scatter", - "source_path": "ppt-master/skills/ppt-master/templates/charts/quadrant_bubble_scatter.svg", + "source_path": "svglide-design-patterns/chart_template/chart.quadrant_bubble_scatter", "kind": "chart_template", - "ppt_master_role": "visualization template", "svglide_target": "svg-recipes.json/component-catalog", "protocol_compatibility": "needs_normalization", "conversion_strategy": "Extract geometry slots and chart role contract; do not copy final SVG as a page template.", @@ -3769,13 +3768,13 @@ "metadata": { "summary": "Pick for 2×2 portfolio where items are plotted as bubbles sized by a third metric — BCG Growth × Share with Stars/Cash Cows/Question Marks/Dogs, revenue-weighted portfolio matrix. Skip for textual named quadrants (use quadrant_text_bullets) or generic prioritization without bubble size (use matrix_2x2).", "view_box": "0 0 1280 720" - } + }, + "design_role": "visualization template" }, { "id": "chart.quadrant_text_bullets", - "source_path": "ppt-master/skills/ppt-master/templates/charts/quadrant_text_bullets.svg", + "source_path": "svglide-design-patterns/chart_template/chart.quadrant_text_bullets", "kind": "chart_template", - "ppt_master_role": "visualization template", "svglide_target": "svg-recipes.json/component-catalog", "protocol_compatibility": "needs_normalization", "conversion_strategy": "Extract geometry slots and chart role contract; do not copy final SVG as a page template.", @@ -3843,13 +3842,13 @@ "metadata": { "summary": "Pick for any 2×2 framework where each quadrant holds a titled bullet list — SWOT (Strengths/Weaknesses/Opportunities/Threats, internal-external × helpful-harmful), Ansoff (Existing/New Markets × Existing/New Products), or any named two-axis matrix with text content. Skip for items plotted as points (use matrix_2x2) or bubble-sized portfolios (use quadrant_bubble_scatter).", "view_box": "0 0 1280 720" - } + }, + "design_role": "visualization template" }, { "id": "chart.radar_chart", - "source_path": "ppt-master/skills/ppt-master/templates/charts/radar_chart.svg", + "source_path": "svglide-design-patterns/chart_template/chart.radar_chart", "kind": "chart_template", - "ppt_master_role": "visualization template", "svglide_target": "svg-recipes.json/component-catalog", "protocol_compatibility": "needs_normalization", "conversion_strategy": "Extract geometry slots and chart role contract; do not copy final SVG as a page template.", @@ -3905,13 +3904,13 @@ "metadata": { "summary": "Pick for 4-8 capability dimensions scored across 1-3 entities. Skip for >3 entities (becomes unreadable; use grouped_bar_chart) or <4 dimensions.", "view_box": "0 0 1280 720" - } + }, + "design_role": "visualization template" }, { "id": "chart.roadmap_vertical", - "source_path": "ppt-master/skills/ppt-master/templates/charts/roadmap_vertical.svg", + "source_path": "svglide-design-patterns/chart_template/chart.roadmap_vertical", "kind": "chart_template", - "ppt_master_role": "visualization template", "svglide_target": "svg-recipes.json/component-catalog", "protocol_compatibility": "needs_normalization", "conversion_strategy": "Extract geometry slots and chart role contract; do not copy final SVG as a page template.", @@ -3970,13 +3969,13 @@ "metadata": { "summary": "Pick for 4-8 milestones on a vertical timeline with status indicators. Skip for horizontal time emphasis (use timeline) or tasks with durations (use gantt_chart).", "view_box": "0 0 1280 720" - } + }, + "design_role": "visualization template" }, { "id": "chart.sankey_chart", - "source_path": "ppt-master/skills/ppt-master/templates/charts/sankey_chart.svg", + "source_path": "svglide-design-patterns/chart_template/chart.sankey_chart", "kind": "chart_template", - "ppt_master_role": "visualization template", "svglide_target": "svg-recipes.json/component-catalog", "protocol_compatibility": "needs_normalization", "conversion_strategy": "Extract geometry slots and chart role contract; do not copy final SVG as a page template.", @@ -4032,13 +4031,13 @@ "metadata": { "summary": "Pick for 3-stage flow with magnitude (sources -> nodes -> sinks). Skip for simple linear conversion without splits (use funnel_chart).", "view_box": "0 0 1280 720" - } + }, + "design_role": "visualization template" }, { "id": "chart.scatter_chart", - "source_path": "ppt-master/skills/ppt-master/templates/charts/scatter_chart.svg", + "source_path": "svglide-design-patterns/chart_template/chart.scatter_chart", "kind": "chart_template", - "ppt_master_role": "visualization template", "svglide_target": "svg-recipes.json/component-catalog", "protocol_compatibility": "needs_normalization", "conversion_strategy": "Extract geometry slots and chart role contract; do not copy final SVG as a page template.", @@ -4094,13 +4093,13 @@ "metadata": { "summary": "Pick for x-y correlation, cluster, or outlier scan. Skip if a size dimension also matters (use bubble_chart).", "view_box": "0 0 1280 720" - } + }, + "design_role": "visualization template" }, { "id": "chart.segmented_wheel", - "source_path": "ppt-master/skills/ppt-master/templates/charts/segmented_wheel.svg", + "source_path": "svglide-design-patterns/chart_template/chart.segmented_wheel", "kind": "chart_template", - "ppt_master_role": "visualization template", "svglide_target": "svg-recipes.json/component-catalog", "protocol_compatibility": "needs_normalization", "conversion_strategy": "Extract geometry slots and chart role contract; do not copy final SVG as a page template.", @@ -4167,13 +4166,13 @@ "metadata": { "summary": "Pick for one central topic split into 4-8 equally-weighted parallel dimensions (5W1H, 4P, 5S, 6-aspect review), each wedge paired with a description card. Skip for sequential cycles (use circular_stages), proportional shares (use pie_chart), or hub with non-wedge spokes (use hub_spoke).", "view_box": "0 0 1280 720" - } + }, + "design_role": "visualization template" }, { "id": "chart.snake_flow", - "source_path": "ppt-master/skills/ppt-master/templates/charts/snake_flow.svg", + "source_path": "svglide-design-patterns/chart_template/chart.snake_flow", "kind": "chart_template", - "ppt_master_role": "visualization template", "svglide_target": "svg-recipes.json/component-catalog", "protocol_compatibility": "needs_normalization", "conversion_strategy": "Extract geometry slots and chart role contract; do not copy final SVG as a page template.", @@ -4231,13 +4230,13 @@ "metadata": { "summary": "Pick for 6-10 winding sequential steps fitting a long journey/lifecycle on one slide. Skip for <=5 steps (use numbered_steps).", "view_box": "0 0 1280 720" - } + }, + "design_role": "visualization template" }, { "id": "chart.stacked_area_chart", - "source_path": "ppt-master/skills/ppt-master/templates/charts/stacked_area_chart.svg", + "source_path": "svglide-design-patterns/chart_template/chart.stacked_area_chart", "kind": "chart_template", - "ppt_master_role": "visualization template", "svglide_target": "svg-recipes.json/component-catalog", "protocol_compatibility": "needs_normalization", "conversion_strategy": "Extract geometry slots and chart role contract; do not copy final SVG as a page template.", @@ -4296,13 +4295,13 @@ "metadata": { "summary": "Pick for 2-4 series showing both total and composition over time. Skip if individual series matter more than total (use line_chart).", "view_box": "0 0 1280 720" - } + }, + "design_role": "visualization template" }, { "id": "chart.stacked_bar_chart", - "source_path": "ppt-master/skills/ppt-master/templates/charts/stacked_bar_chart.svg", + "source_path": "svglide-design-patterns/chart_template/chart.stacked_bar_chart", "kind": "chart_template", - "ppt_master_role": "visualization template", "svglide_target": "svg-recipes.json/component-catalog", "protocol_compatibility": "needs_normalization", "conversion_strategy": "Extract geometry slots and chart role contract; do not copy final SVG as a page template.", @@ -4361,13 +4360,13 @@ "metadata": { "summary": "Pick when each category splits into 2-4 internal parts and total still matters. Skip if only comparing totals (use bar_chart).", "view_box": "0 0 1280 720" - } + }, + "design_role": "visualization template" }, { "id": "chart.team_roster", - "source_path": "ppt-master/skills/ppt-master/templates/charts/team_roster.svg", + "source_path": "svglide-design-patterns/chart_template/chart.team_roster", "kind": "chart_template", - "ppt_master_role": "visualization template", "svglide_target": "svg-recipes.json/component-catalog", "protocol_compatibility": "needs_normalization", "conversion_strategy": "Extract geometry slots and chart role contract; do not copy final SVG as a page template.", @@ -4426,13 +4425,13 @@ "metadata": { "summary": "Pick for 3-12 leadership/team profile cards (photo + name + title + short bio). Skip for reporting hierarchy (use top_down_tree).", "view_box": "0 0 1280 720" - } + }, + "design_role": "visualization template" }, { "id": "chart.timeline", - "source_path": "ppt-master/skills/ppt-master/templates/charts/timeline.svg", + "source_path": "svglide-design-patterns/chart_template/chart.timeline", "kind": "chart_template", - "ppt_master_role": "visualization template", "svglide_target": "svg-recipes.json/component-catalog", "protocol_compatibility": "needs_normalization", "conversion_strategy": "Extract geometry slots and chart role contract; do not copy final SVG as a page template.", @@ -4495,13 +4494,13 @@ "metadata": { "summary": "Pick for 3-8 milestone events on a horizontal time axis (no duration). Skip for tasks with start/end ranges (use gantt_chart) or vertical layout (use roadmap_vertical).", "view_box": "0 0 1280 720" - } + }, + "design_role": "visualization template" }, { "id": "chart.top_down_tree", - "source_path": "ppt-master/skills/ppt-master/templates/charts/top_down_tree.svg", + "source_path": "svglide-design-patterns/chart_template/chart.top_down_tree", "kind": "chart_template", - "ppt_master_role": "visualization template", "svglide_target": "svg-recipes.json/component-catalog", "protocol_compatibility": "needs_normalization", "conversion_strategy": "Extract geometry slots and chart role contract; do not copy final SVG as a page template.", @@ -4569,13 +4568,13 @@ "metadata": { "summary": "Pick for hierarchical top-down tree 2-4 levels deep with parent→children reporting/decomposition lines — org charts (CEO → VPs → Directors), OKR cascades (Objective → Key Results → Initiatives), WBS decomposition. Skip for non-hierarchical brainstorm (use mind_map) or flat team showcase (use team_roster).", "view_box": "0 0 1280 720" - } + }, + "design_role": "visualization template" }, { "id": "chart.treemap_chart", - "source_path": "ppt-master/skills/ppt-master/templates/charts/treemap_chart.svg", + "source_path": "svglide-design-patterns/chart_template/chart.treemap_chart", "kind": "chart_template", - "ppt_master_role": "visualization template", "svglide_target": "svg-recipes.json/component-catalog", "protocol_compatibility": "needs_normalization", "conversion_strategy": "Extract geometry slots and chart role contract; do not copy final SVG as a page template.", @@ -4633,13 +4632,13 @@ "metadata": { "summary": "Pick for hierarchical/grouped area proportion with many parts of varying size. Skip for flat 3-6 parts (use pie_chart or donut_chart).", "view_box": "0 0 1280 720" - } + }, + "design_role": "visualization template" }, { "id": "chart.venn_diagram", - "source_path": "ppt-master/skills/ppt-master/templates/charts/venn_diagram.svg", + "source_path": "svglide-design-patterns/chart_template/chart.venn_diagram", "kind": "chart_template", - "ppt_master_role": "visualization template", "svglide_target": "svg-recipes.json/component-catalog", "protocol_compatibility": "needs_normalization", "conversion_strategy": "Extract geometry slots and chart role contract; do not copy final SVG as a page template.", @@ -4697,13 +4696,13 @@ "metadata": { "summary": "Pick for 2-3 overlapping sets where the intersection is the message. Skip if no overlap exists (use icon_grid).", "view_box": "0 0 1280 720" - } + }, + "design_role": "visualization template" }, { "id": "chart.vertical_list", - "source_path": "ppt-master/skills/ppt-master/templates/charts/vertical_list.svg", + "source_path": "svglide-design-patterns/chart_template/chart.vertical_list", "kind": "chart_template", - "ppt_master_role": "visualization template", "svglide_target": "svg-recipes.json/component-catalog", "protocol_compatibility": "needs_normalization", "conversion_strategy": "Extract geometry slots and chart role contract; do not copy final SVG as a page template.", @@ -4771,13 +4770,13 @@ "metadata": { "summary": "Pick for 3-6 numbered key points each with a short description — design principles, core tenets, action items, key takeaways, recommendations, executive summary points. Skip for icon-style cards (use icon_grid) or sequential steps (use numbered_steps).", "view_box": "0 0 1280 720" - } + }, + "design_role": "visualization template" }, { "id": "chart.vertical_pillars", - "source_path": "ppt-master/skills/ppt-master/templates/charts/vertical_pillars.svg", + "source_path": "svglide-design-patterns/chart_template/chart.vertical_pillars", "kind": "chart_template", - "ppt_master_role": "visualization template", "svglide_target": "svg-recipes.json/component-catalog", "protocol_compatibility": "needs_normalization", "conversion_strategy": "Extract geometry slots and chart role contract; do not copy final SVG as a page template.", @@ -4845,13 +4844,13 @@ "metadata": { "summary": "Pick for 1×3 / 1×4 / 1×5 vertical column layout where each pillar = one independent category with title + bullets — PEST (Political/Economic/Social/Technological), four-pillar strategy overview, side-by-side independent categories. Skip for 2×2 quadrant (use quadrant_text_bullets), pricing tiers (use comparison_columns), or 2×2 parallel aspects (use labeled_card).", "view_box": "0 0 1280 720" - } + }, + "design_role": "visualization template" }, { "id": "chart.waterfall_chart", - "source_path": "ppt-master/skills/ppt-master/templates/charts/waterfall_chart.svg", + "source_path": "svglide-design-patterns/chart_template/chart.waterfall_chart", "kind": "chart_template", - "ppt_master_role": "visualization template", "svglide_target": "svg-recipes.json/component-catalog", "protocol_compatibility": "needs_normalization", "conversion_strategy": "Extract geometry slots and chart role contract; do not copy final SVG as a page template.", @@ -4909,13 +4908,13 @@ "metadata": { "summary": "Pick for stepwise additive/subtractive breakdown bridging a starting value to an ending value. Skip if no running total (use bar_chart).", "view_box": "0 0 1280 720" - } + }, + "design_role": "visualization template" }, { "id": "chart.word_cloud", - "source_path": "ppt-master/skills/ppt-master/templates/charts/word_cloud.svg", + "source_path": "svglide-design-patterns/chart_template/chart.word_cloud", "kind": "chart_template", - "ppt_master_role": "visualization template", "svglide_target": "svg-recipes.json/component-catalog", "protocol_compatibility": "needs_normalization", "conversion_strategy": "Extract geometry slots and chart role contract; do not copy final SVG as a page template.", @@ -4973,13 +4972,13 @@ "metadata": { "summary": "Pick for 20-30 keywords sized by frequency/weight. Skip if individual keyword position carries meaning (use icon_grid).", "view_box": "0 0 1280 720" - } + }, + "design_role": "visualization template" }, { "id": "deck.中国电信", - "source_path": "ppt-master/skills/ppt-master/templates/decks/中国电信", + "source_path": "svglide-design-patterns/deck_template/deck.中国电信", "kind": "deck_template", - "ppt_master_role": "finished deck style and page-sequence template", "svglide_target": "deck-rhythm-pack/style-pack reference", "protocol_compatibility": "needs_normalization", "conversion_strategy": "Extract page sequence, rhythm, and style rules; do not copy finished deck SVG or logos.", @@ -5025,18 +5024,18 @@ "summary": "China Telecom related briefings, 政企数字化方案, 转型规划, 内部汇报.", "metadata": { "summary": "China Telecom related briefings, 政企数字化方案, 转型规划, 内部汇报.", - "canvas_format": "ppt169", + "canvas_format": "svglide_16x9", "page_count": 5, "primary_color": "#C00000", "svg_file_count": 5, "image_file_count": 6 - } + }, + "design_role": "finished deck style and page-sequence template" }, { "id": "deck.中国电建_常规", - "source_path": "ppt-master/skills/ppt-master/templates/decks/中国电建_常规", + "source_path": "svglide-design-patterns/deck_template/deck.中国电建_常规", "kind": "deck_template", - "ppt_master_role": "finished deck style and page-sequence template", "svglide_target": "deck-rhythm-pack/style-pack reference", "protocol_compatibility": "needs_normalization", "conversion_strategy": "Extract page sequence, rhythm, and style rules; do not copy finished deck SVG or logos.", @@ -5087,18 +5086,18 @@ "summary": "Engineering project reports, technical proposal presentations, business negotiations, corporate promotion, annual summaries.", "metadata": { "summary": "Engineering project reports, technical proposal presentations, business negotiations, corporate promotion, annual summaries.", - "canvas_format": "ppt169", + "canvas_format": "svglide_16x9", "page_count": 5, "primary_color": "#00418D", "svg_file_count": 5, "image_file_count": 4 - } + }, + "design_role": "finished deck style and page-sequence template" }, { "id": "deck.中国电建_现代", - "source_path": "ppt-master/skills/ppt-master/templates/decks/中国电建_现代", + "source_path": "svglide-design-patterns/deck_template/deck.中国电建_现代", "kind": "deck_template", - "ppt_master_role": "finished deck style and page-sequence template", "svglide_target": "deck-rhythm-pack/style-pack reference", "protocol_compatibility": "needs_normalization", "conversion_strategy": "Extract page sequence, rhythm, and style rules; do not copy finished deck SVG or logos.", @@ -5150,18 +5149,18 @@ "summary": "Major engineering reports, international market promotion, technology achievement showcases, high-end business negotiations.", "metadata": { "summary": "Major engineering reports, international market promotion, technology achievement showcases, high-end business negotiations.", - "canvas_format": "ppt169", + "canvas_format": "svglide_16x9", "page_count": 5, "primary_color": "#00418D", "svg_file_count": 5, "image_file_count": 4 - } + }, + "design_role": "finished deck style and page-sequence template" }, { "id": "deck.中汽研_商务", - "source_path": "ppt-master/skills/ppt-master/templates/decks/中汽研_商务", + "source_path": "svglide-design-patterns/deck_template/deck.中汽研_商务", "kind": "deck_template", - "ppt_master_role": "finished deck style and page-sequence template", "svglide_target": "deck-rhythm-pack/style-pack reference", "protocol_compatibility": "needs_normalization", "conversion_strategy": "Extract page sequence, rhythm, and style rules; do not copy finished deck SVG or logos.", @@ -5212,18 +5211,18 @@ "summary": "Product certification display, evaluation presentations, technology promotion, high-end business reporting.", "metadata": { "summary": "Product certification display, evaluation presentations, technology promotion, high-end business reporting.", - "canvas_format": "ppt169", + "canvas_format": "svglide_16x9", "page_count": 5, "primary_color": "#003366", "svg_file_count": 5, "image_file_count": 2 - } + }, + "design_role": "finished deck style and page-sequence template" }, { "id": "deck.中汽研_常规", - "source_path": "ppt-master/skills/ppt-master/templates/decks/中汽研_常规", + "source_path": "svglide-design-patterns/deck_template/deck.中汽研_常规", "kind": "deck_template", - "ppt_master_role": "finished deck style and page-sequence template", "svglide_target": "deck-rhythm-pack/style-pack reference", "protocol_compatibility": "needs_normalization", "conversion_strategy": "Extract page sequence, rhythm, and style rules; do not copy finished deck SVG or logos.", @@ -5272,18 +5271,18 @@ "summary": "Product certification display, evaluation presentations, technology promotion, business visits.", "metadata": { "summary": "Product certification display, evaluation presentations, technology promotion, business visits.", - "canvas_format": "ppt169", + "canvas_format": "svglide_16x9", "page_count": 5, "primary_color": "#004098", "svg_file_count": 5, "image_file_count": 2 - } + }, + "design_role": "finished deck style and page-sequence template" }, { "id": "deck.中汽研_现代", - "source_path": "ppt-master/skills/ppt-master/templates/decks/中汽研_现代", + "source_path": "svglide-design-patterns/deck_template/deck.中汽研_现代", "kind": "deck_template", - "ppt_master_role": "finished deck style and page-sequence template", "svglide_target": "deck-rhythm-pack/style-pack reference", "protocol_compatibility": "needs_normalization", "conversion_strategy": "Extract page sequence, rhythm, and style rules; do not copy finished deck SVG or logos.", @@ -5334,18 +5333,18 @@ "summary": "Forward-looking technology showcases, strategic releases, high-end business reporting.", "metadata": { "summary": "Forward-looking technology showcases, strategic releases, high-end business reporting.", - "canvas_format": "ppt169", + "canvas_format": "svglide_16x9", "page_count": 5, "primary_color": "#001529", "svg_file_count": 5, "image_file_count": 2 - } + }, + "design_role": "finished deck style and page-sequence template" }, { "id": "deck.招商银行", - "source_path": "ppt-master/skills/ppt-master/templates/decks/招商银行", + "source_path": "svglide-design-patterns/deck_template/deck.招商银行", "kind": "deck_template", - "ppt_master_role": "finished deck style and page-sequence template", "svglide_target": "deck-rhythm-pack/style-pack reference", "protocol_compatibility": "needs_normalization", "conversion_strategy": "Extract page sequence, rhythm, and style rules; do not copy finished deck SVG or logos.", @@ -5388,18 +5387,18 @@ "summary": "交易银行产品介绍、销售收款方案汇报、客户案例拆解、分行培训材料.", "metadata": { "summary": "交易银行产品介绍、销售收款方案汇报、客户案例拆解、分行培训材料.", - "canvas_format": "ppt169", + "canvas_format": "svglide_16x9", "page_count": 5, "primary_color": "#C8152D", "svg_file_count": 5, "image_file_count": 6 - } + }, + "design_role": "finished deck style and page-sequence template" }, { "id": "deck.重庆大学", - "source_path": "ppt-master/skills/ppt-master/templates/decks/重庆大学", + "source_path": "svglide-design-patterns/deck_template/deck.重庆大学", "kind": "deck_template", - "ppt_master_role": "finished deck style and page-sequence template", "svglide_target": "deck-rhythm-pack/style-pack reference", "protocol_compatibility": "needs_normalization", "conversion_strategy": "Extract page sequence, rhythm, and style rules; do not copy finished deck SVG or logos.", @@ -5446,18 +5445,18 @@ "summary": "Academic defense, research reports, teaching presentations, scholarly exchange.", "metadata": { "summary": "Academic defense, research reports, teaching presentations, scholarly exchange.", - "canvas_format": "ppt169", + "canvas_format": "svglide_16x9", "page_count": 5, "primary_color": "#006BB7", "svg_file_count": 5, "image_file_count": 2 - } + }, + "design_role": "finished deck style and page-sequence template" }, { - "id": "example.ppt169_attention_is_all_you_need", - "source_path": "ppt-master/examples/ppt169_attention_is_all_you_need", + "id": "example.svglide_16x9_attention_is_all_you_need", + "source_path": "svglide-design-patterns/example_project/example.svglide_16x9_attention_is_all_you_need", "kind": "example_project", - "ppt_master_role": "finished example deck and mining corpus", "svglide_target": "golden/negative corpus and style-mining source", "protocol_compatibility": "reference_only", "conversion_strategy": "Mine page rhythm, style signals, and failure cases; do not copy finished pages, images, base64, or PPTX exports.", @@ -5489,7 +5488,7 @@ "multi", "need", "overview", - "ppt169", + "svglide_16x9", "product", "rnn", "scaled", @@ -5555,13 +5554,13 @@ "sublayer_block.png" ], "project_digest": "38bc99f26cc47232" - } + }, + "design_role": "finished example deck and mining corpus" }, { - "id": "example.ppt169_brutalist_ai_newspaper_2026", - "source_path": "ppt-master/examples/ppt169_brutalist_ai_newspaper_2026", + "id": "example.svglide_16x9_brutalist_ai_newspaper_2026", + "source_path": "svglide-design-patterns/example_project/example.svglide_16x9_brutalist_ai_newspaper_2026", "kind": "example_project", - "ppt_master_role": "finished example deck and mining corpus", "svglide_target": "golden/negative corpus and style-mining source", "protocol_compatibility": "reference_only", "conversion_strategy": "Mine page rhythm, style signals, and failure cases; do not copy finished pages, images, base64, or PPTX exports.", @@ -5592,7 +5591,7 @@ "issue", "league", "newspaper", - "ppt169", + "svglide_16x9", "revenue", "rulebooks", "saturated", @@ -5624,9 +5623,9 @@ "golden_example_required": false, "normalized_fixture": "", "source_digest": "4d2b449dd8110214", - "summary": "brutalist_ai_newspaper_2026_ppt169_20260521", + "summary": "brutalist_ai_newspaper_2026_svglide_16x9_20260521", "metadata": { - "summary": "brutalist_ai_newspaper_2026_ppt169_20260521", + "summary": "brutalist_ai_newspaper_2026_svglide_16x9_20260521", "page_count": 10, "svg_final_count": 10, "svg_output_count": 10, @@ -5654,13 +5653,13 @@ "p08_regulation_table.png" ], "project_digest": "4d2b449dd8110214" - } + }, + "design_role": "finished example deck and mining corpus" }, { - "id": "example.ppt169_building_effective_agents", - "source_path": "ppt-master/examples/ppt169_building_effective_agents", + "id": "example.svglide_16x9_building_effective_agents", + "source_path": "svglide-design-patterns/example_project/example.svglide_16x9_building_effective_agents", "kind": "example_project", - "ppt_master_role": "finished example deck and mining corpus", "svglide_target": "golden/negative corpus and style-mining source", "protocol_compatibility": "reference_only", "conversion_strategy": "Mine page rhythm, style signals, and failure cases; do not copy finished pages, images, base64, or PPTX exports.", @@ -5685,7 +5684,7 @@ "llm", "orchestrator", "parallelization", - "ppt169", + "svglide_16x9", "prompt", "routing", "to", @@ -5751,13 +5750,13 @@ "image_7.png" ], "project_digest": "fb5afb97ba4c1718" - } + }, + "design_role": "finished example deck and mining corpus" }, { - "id": "example.ppt169_cangzhuo", - "source_path": "ppt-master/examples/ppt169_cangzhuo", + "id": "example.svglide_16x9_cangzhuo", + "source_path": "svglide-design-patterns/example_project/example.svglide_16x9_cangzhuo", "kind": "example_project", - "ppt_master_role": "finished example deck and mining corpus", "svglide_target": "golden/negative corpus and style-mining source", "protocol_compatibility": "reference_only", "conversion_strategy": "Mine page rhythm, style signals, and failure cases; do not copy finished pages, images, base64, or PPTX exports.", @@ -5774,7 +5773,7 @@ "cangzhuo", "cover", "example", - "ppt169", + "svglide_16x9", "什么是藏拙", "会藏拙的人怎么做", "年轻人最易犯的错", @@ -5838,13 +5837,13 @@ "zengguofan_lamp.png" ], "project_digest": "578f91a97e1c81b3" - } + }, + "design_role": "finished example deck and mining corpus" }, { - "id": "example.ppt169_fashion_weekly_digest", - "source_path": "ppt-master/examples/ppt169_fashion_weekly_digest", + "id": "example.svglide_16x9_fashion_weekly_digest", + "source_path": "svglide-design-patterns/example_project/example.svglide_16x9_fashion_weekly_digest", "kind": "example_project", - "ppt_master_role": "finished example deck and mining corpus", "svglide_target": "golden/negative corpus and style-mining source", "protocol_compatibility": "reference_only", "conversion_strategy": "Mine page rhythm, style signals, and failure cases; do not copy finished pages, images, base64, or PPTX exports.", @@ -5861,7 +5860,7 @@ "digest", "example", "fashion", - "ppt169", + "svglide_16x9", "weekly", "古驰记忆", "封面", @@ -5933,13 +5932,13 @@ "dior_fashion.jpg" ], "project_digest": "ba01eb82f8842691" - } + }, + "design_role": "finished example deck and mining corpus" }, { - "id": "example.ppt169_general_dark_tech_claude_code_auto_mode", - "source_path": "ppt-master/examples/ppt169_general_dark_tech_claude_code_auto_mode", + "id": "example.svglide_16x9_general_dark_tech_claude_code_auto_mode", + "source_path": "svglide-design-patterns/example_project/example.svglide_16x9_general_dark_tech_claude_code_auto_mode", "kind": "example_project", - "ppt_master_role": "finished example deck and mining corpus", "svglide_target": "golden/negative corpus and style-mining source", "protocol_compatibility": "reference_only", "conversion_strategy": "Mine page rhythm, style signals, and failure cases; do not copy finished pages, images, base64, or PPTX exports.", @@ -5967,7 +5966,7 @@ "mode", "model", "permissions", - "ppt169", + "svglide_16x9", "results", "tech", "threat" @@ -5996,9 +5995,9 @@ "golden_example_required": false, "normalized_fixture": "", "source_digest": "e9d6f351c60915cb", - "summary": "ppt169_general_dark_tech_claude_code_auto_mode", + "summary": "svglide_16x9_general_dark_tech_claude_code_auto_mode", "metadata": { - "summary": "ppt169_general_dark_tech_claude_code_auto_mode", + "summary": "svglide_16x9_general_dark_tech_claude_code_auto_mode", "page_count": 10, "svg_final_count": 10, "svg_output_count": 10, @@ -6025,13 +6024,13 @@ "image_3.png" ], "project_digest": "e9d6f351c60915cb" - } + }, + "design_role": "finished example deck and mining corpus" }, { - "id": "example.ppt169_glassmorphism_demo", - "source_path": "ppt-master/examples/ppt169_glassmorphism_demo", + "id": "example.svglide_16x9_glassmorphism_demo", + "source_path": "svglide-design-patterns/example_project/example.svglide_16x9_glassmorphism_demo", "kind": "example_project", - "ppt_master_role": "finished example deck and mining corpus", "svglide_target": "golden/negative corpus and style-mining source", "protocol_compatibility": "reference_only", "conversion_strategy": "Mine page rhythm, style signals, and failure cases; do not copy finished pages, images, base64, or PPTX exports.", @@ -6059,7 +6058,7 @@ "modes", "orchestration", "pains", - "ppt169", + "svglide_16x9", "prod", "three", "to", @@ -6122,13 +6121,13 @@ "12_cta_horizon.png" ], "project_digest": "bb17f9a2001bcaca" - } + }, + "design_role": "finished example deck and mining corpus" }, { - "id": "example.ppt169_global_ai_capital_2026", - "source_path": "ppt-master/examples/ppt169_global_ai_capital_2026", + "id": "example.svglide_16x9_editorial_ai_capital_2026", + "source_path": "svglide-design-patterns/example_project/example.svglide_16x9_editorial_ai_capital_2026", "kind": "example_project", - "ppt_master_role": "finished example deck and mining corpus", "svglide_target": "golden/negative corpus and style-mining source", "protocol_compatibility": "reference_only", "conversion_strategy": "Mine page rhythm, style signals, and failure cases; do not copy finished pages, images, base64, or PPTX exports.", @@ -6161,7 +6160,7 @@ "leap", "note", "openai", - "ppt169", + "svglide_16x9", "q1", "revenue", "three", @@ -6191,9 +6190,9 @@ "golden_example_required": false, "normalized_fixture": "", "source_digest": "3377a4a832f87b5f", - "summary": "global_ai_capital_2026_ppt169_20260516", + "summary": "editorial_ai_capital_2026_svglide_16x9_20260516", "metadata": { - "summary": "global_ai_capital_2026_ppt169_20260516", + "summary": "editorial_ai_capital_2026_svglide_16x9_20260516", "page_count": 20, "svg_final_count": 20, "svg_output_count": 20, @@ -6219,13 +6218,13 @@ "nvidia_circular.png" ], "project_digest": "3377a4a832f87b5f" - } + }, + "design_role": "finished example deck and mining corpus" }, { - "id": "example.ppt169_high_rise_renewal", - "source_path": "ppt-master/examples/ppt169_high_rise_renewal", + "id": "example.svglide_16x9_high_rise_renewal", + "source_path": "svglide-design-patterns/example_project/example.svglide_16x9_high_rise_renewal", "kind": "example_project", - "ppt_master_role": "finished example deck and mining corpus", "svglide_target": "golden/negative corpus and style-mining source", "protocol_compatibility": "reference_only", "conversion_strategy": "Mine page rhythm, style signals, and failure cases; do not copy finished pages, images, base64, or PPTX exports.", @@ -6252,7 +6251,7 @@ "highrise", "igoe", "ii", - "ppt169", + "svglide_16x9", "pruitt", "quarter", "quay", @@ -6284,9 +6283,9 @@ "golden_example_required": false, "normalized_fixture": "", "source_digest": "ba8a732dc29cb2c1", - "summary": "high_rise_renewal_ppt169_20260516", + "summary": "high_rise_renewal_svglide_16x9_20260516", "metadata": { - "summary": "high_rise_renewal_ppt169_20260516", + "summary": "high_rise_renewal_svglide_16x9_20260516", "page_count": 15, "svg_final_count": 15, "svg_output_count": 15, @@ -6319,13 +6318,13 @@ "640_17.png" ], "project_digest": "ba8a732dc29cb2c1" - } + }, + "design_role": "finished example deck and mining corpus" }, { - "id": "example.ppt169_home_design_trends_2026", - "source_path": "ppt-master/examples/ppt169_home_design_trends_2026", + "id": "example.svglide_16x9_home_design_trends_2026", + "source_path": "svglide-design-patterns/example_project/example.svglide_16x9_home_design_trends_2026", "kind": "example_project", - "ppt_master_role": "finished example deck and mining corpus", "svglide_target": "golden/negative corpus and style-mining source", "protocol_compatibility": "reference_only", "conversion_strategy": "Mine page rhythm, style signals, and failure cases; do not copy finished pages, images, base64, or PPTX exports.", @@ -6344,7 +6343,7 @@ "design", "example", "home", - "ppt169", + "svglide_16x9", "trends", "从网红风到人的尺度", "体系", @@ -6414,13 +6413,13 @@ "cmt_overview.png" ], "project_digest": "f1dc9774224cf00e" - } + }, + "design_role": "finished example deck and mining corpus" }, { - "id": "example.ppt169_image_text_showcase", - "source_path": "ppt-master/examples/ppt169_image_text_showcase", + "id": "example.svglide_16x9_image_text_showcase", + "source_path": "svglide-design-patterns/example_project/example.svglide_16x9_image_text_showcase", "kind": "example_project", - "ppt_master_role": "finished example deck and mining corpus", "svglide_target": "golden/negative corpus and style-mining source", "protocol_compatibility": "reference_only", "conversion_strategy": "Mine page rhythm, style signals, and failure cases; do not copy finished pages, images, base64, or PPTX exports.", @@ -6436,7 +6435,7 @@ "08", "example", "image", - "ppt169", + "svglide_16x9", "showcase", "text", "z", @@ -6508,13 +6507,13 @@ "p10_bg_texture.png" ], "project_digest": "e077d46f669fac80" - } + }, + "design_role": "finished example deck and mining corpus" }, { - "id": "example.ppt169_indie_bookstore_zine_guide", - "source_path": "ppt-master/examples/ppt169_indie_bookstore_zine_guide", + "id": "example.svglide_16x9_indie_bookstore_zine_guide", + "source_path": "svglide-design-patterns/example_project/example.svglide_16x9_indie_bookstore_zine_guide", "kind": "example_project", - "ppt_master_role": "finished example deck and mining corpus", "svglide_target": "golden/negative corpus and style-mining source", "protocol_compatibility": "reference_only", "conversion_strategy": "Mine page rhythm, style signals, and failure cases; do not copy finished pages, images, base64, or PPTX exports.", @@ -6538,7 +6537,7 @@ "history", "indie", "origin", - "ppt169", + "svglide_16x9", "process", "risograph", "toc", @@ -6570,9 +6569,9 @@ "golden_example_required": false, "normalized_fixture": "", "source_digest": "53d75c7fc3ff0b14", - "summary": "ppt169_indie_bookstore_zine_guide_20260517", + "summary": "svglide_16x9_indie_bookstore_zine_guide_20260517", "metadata": { - "summary": "ppt169_indie_bookstore_zine_guide_20260517", + "summary": "svglide_16x9_indie_bookstore_zine_guide_20260517", "page_count": 18, "svg_final_count": 18, "svg_output_count": 18, @@ -6605,13 +6604,13 @@ "zine_history_collage.png" ], "project_digest": "53d75c7fc3ff0b14" - } + }, + "design_role": "finished example deck and mining corpus" }, { - "id": "example.ppt169_kimsoong_loyalty_programme", - "source_path": "ppt-master/examples/ppt169_kimsoong_loyalty_programme", + "id": "example.svglide_16x9_kimsoong_loyalty_programme", + "source_path": "svglide-design-patterns/example_project/example.svglide_16x9_kimsoong_loyalty_programme", "kind": "example_project", - "ppt_master_role": "finished example deck and mining corpus", "svglide_target": "golden/negative corpus and style-mining source", "protocol_compatibility": "reference_only", "conversion_strategy": "Mine page rhythm, style signals, and failure cases; do not copy finished pages, images, base64, or PPTX exports.", @@ -6637,7 +6636,7 @@ "kimsoong", "loyalty", "pillars", - "ppt169", + "svglide_16x9", "priorities", "profile", "programme", @@ -6698,13 +6697,13 @@ "roadmap_bg.png" ], "project_digest": "ebc6c414ab46be63" - } + }, + "design_role": "finished example deck and mining corpus" }, { - "id": "example.ppt169_kubernetes_blueprint_2026", - "source_path": "ppt-master/examples/ppt169_kubernetes_blueprint_2026", + "id": "example.svglide_16x9_kubernetes_blueprint_2026", + "source_path": "svglide-design-patterns/example_project/example.svglide_16x9_kubernetes_blueprint_2026", "kind": "example_project", - "ppt_master_role": "finished example deck and mining corpus", "svglide_target": "golden/negative corpus and style-mining source", "protocol_compatibility": "reference_only", "conversion_strategy": "Mine page rhythm, style signals, and failure cases; do not copy finished pages, images, base64, or PPTX exports.", @@ -6730,7 +6729,7 @@ "plane", "planes", "pod", - "ppt169", + "svglide_16x9", "service", "storage", "topology", @@ -6760,9 +6759,9 @@ "golden_example_required": false, "normalized_fixture": "", "source_digest": "132c3d1a12c190c2", - "summary": "ppt169_kubernetes_blueprint", + "summary": "svglide_16x9_kubernetes_blueprint", "metadata": { - "summary": "ppt169_kubernetes_blueprint", + "summary": "svglide_16x9_kubernetes_blueprint", "page_count": 10, "svg_final_count": 10, "svg_output_count": 10, @@ -6784,13 +6783,13 @@ ], "media_samples": [], "project_digest": "132c3d1a12c190c2" - } + }, + "design_role": "finished example deck and mining corpus" }, { - "id": "example.ppt169_lin_huiyin_architect", - "source_path": "ppt-master/examples/ppt169_lin_huiyin_architect", + "id": "example.svglide_16x9_lin_huiyin_architect", + "source_path": "svglide-design-patterns/example_project/example.svglide_16x9_lin_huiyin_architect", "kind": "example_project", - "ppt_master_role": "finished example deck and mining corpus", "svglide_target": "golden/negative corpus and style-mining source", "protocol_compatibility": "reference_only", "conversion_strategy": "Mine page rhythm, style signals, and failure cases; do not copy finished pages, images, base64, or PPTX exports.", @@ -6808,7 +6807,7 @@ "example", "huiyin", "lin", - "ppt169", + "svglide_16x9", "人生轨迹", "国之重器", "学术贡献一览", @@ -6879,13 +6878,13 @@ "lin_survey.png" ], "project_digest": "20b9855195eb7b96" - } + }, + "design_role": "finished example deck and mining corpus" }, { - "id": "example.ppt169_lin_huiyin_architect_revised", - "source_path": "ppt-master/examples/ppt169_lin_huiyin_architect_revised", + "id": "example.svglide_16x9_lin_huiyin_architect_revised", + "source_path": "svglide-design-patterns/example_project/example.svglide_16x9_lin_huiyin_architect_revised", "kind": "example_project", - "ppt_master_role": "finished example deck and mining corpus", "svglide_target": "golden/negative corpus and style-mining source", "protocol_compatibility": "reference_only", "conversion_strategy": "Mine page rhythm, style signals, and failure cases; do not copy finished pages, images, base64, or PPTX exports.", @@ -6903,7 +6902,7 @@ "example", "huiyin", "lin", - "ppt169", + "svglide_16x9", "revised", "为什么今天要重识林徽因", "作品现场", @@ -6978,13 +6977,13 @@ "lin_tomb.png" ], "project_digest": "cf255646e3ac6c65" - } + }, + "design_role": "finished example deck and mining corpus" }, { - "id": "example.ppt169_liziqi_plant_dye_colors", - "source_path": "ppt-master/examples/ppt169_liziqi_plant_dye_colors", + "id": "example.svglide_16x9_liziqi_plant_dye_colors", + "source_path": "svglide-design-patterns/example_project/example.svglide_16x9_liziqi_plant_dye_colors", "kind": "example_project", - "ppt_master_role": "finished example deck and mining corpus", "svglide_target": "golden/negative corpus and style-mining source", "protocol_compatibility": "reference_only", "conversion_strategy": "Mine page rhythm, style signals, and failure cases; do not copy finished pages, images, base64, or PPTX exports.", @@ -7011,7 +7010,7 @@ "natural", "plant", "poetic", - "ppt169", + "svglide_16x9", "zhanpao" ], "copy_policy": "derive_contract_only", @@ -7073,13 +7072,13 @@ "640_15.png" ], "project_digest": "f6b0d492a8afb107" - } + }, + "design_role": "finished example deck and mining corpus" }, { - "id": "example.ppt169_lora_hu_2021", - "source_path": "ppt-master/examples/ppt169_lora_hu_2021", + "id": "example.svglide_16x9_lora_hu_2021", + "source_path": "svglide-design-patterns/example_project/example.svglide_16x9_lora_hu_2021", "kind": "example_project", - "ppt_master_role": "finished example deck and mining corpus", "svglide_target": "golden/negative corpus and style-mining source", "protocol_compatibility": "reference_only", "conversion_strategy": "Mine page rhythm, style signals, and failure cases; do not copy finished pages, images, base64, or PPTX exports.", @@ -7103,7 +7102,7 @@ "limitations", "lora", "method", - "ppt169", + "svglide_16x9", "problem", "transformer" ], @@ -7166,13 +7165,13 @@ "subspace_heatmap.png" ], "project_digest": "5e0b0e096424d588" - } + }, + "design_role": "finished example deck and mining corpus" }, { - "id": "example.ppt169_pritzker_2026", - "source_path": "ppt-master/examples/ppt169_pritzker_2026", + "id": "example.svglide_16x9_pritzker_2026", + "source_path": "svglide-design-patterns/example_project/example.svglide_16x9_pritzker_2026", "kind": "example_project", - "ppt_master_role": "finished example deck and mining corpus", "svglide_target": "golden/negative corpus and style-mining source", "protocol_compatibility": "reference_only", "conversion_strategy": "Mine page rhythm, style signals, and failure cases; do not copy finished pages, images, base64, or PPTX exports.", @@ -7195,7 +7194,7 @@ "foster", "oma", "overview", - "ppt169", + "svglide_16x9", "pritzker", "sanaa", "test", @@ -7261,13 +7260,13 @@ "gehry_abu_dhabi_render1.png" ], "project_digest": "f629a8386f3c896c" - } + }, + "design_role": "finished example deck and mining corpus" }, { - "id": "example.ppt169_sugar_rush_memphis", - "source_path": "ppt-master/examples/ppt169_sugar_rush_memphis", + "id": "example.svglide_16x9_sugar_rush_memphis", + "source_path": "svglide-design-patterns/example_project/example.svglide_16x9_sugar_rush_memphis", "kind": "example_project", - "ppt_master_role": "finished example deck and mining corpus", "svglide_target": "golden/negative corpus and style-mining source", "protocol_compatibility": "reference_only", "conversion_strategy": "Mine page rhythm, style signals, and failure cases; do not copy finished pages, images, base64, or PPTX exports.", @@ -7287,7 +7286,7 @@ "headliners", "memphis", "numbers", - "ppt169", + "svglide_16x9", "principles", "rush", "sugar", @@ -7353,13 +7352,13 @@ "market.png" ], "project_digest": "bcdeafc36693f36d" - } + }, + "design_role": "finished example deck and mining corpus" }, { - "id": "example.ppt169_swiss_grid_systems", - "source_path": "ppt-master/examples/ppt169_swiss_grid_systems", + "id": "example.svglide_16x9_swiss_grid_systems", + "source_path": "svglide-design-patterns/example_project/example.svglide_16x9_swiss_grid_systems", "kind": "example_project", - "ppt_master_role": "finished example deck and mining corpus", "svglide_target": "golden/negative corpus and style-mining source", "protocol_compatibility": "reference_only", "conversion_strategy": "Mine page rhythm, style signals, and failure cases; do not copy finished pages, images, base64, or PPTX exports.", @@ -7380,7 +7379,7 @@ "figures", "grid", "origin", - "ppt169", + "svglide_16x9", "principles", "quote", "swiss", @@ -7412,9 +7411,9 @@ "golden_example_required": false, "normalized_fixture": "", "source_digest": "1823080b6fef9ef4", - "summary": "swiss_grid_systems_ppt169_20260517", + "summary": "swiss_grid_systems_svglide_16x9_20260517", "metadata": { - "summary": "swiss_grid_systems_ppt169_20260517", + "summary": "swiss_grid_systems_svglide_16x9_20260517", "page_count": 14, "svg_final_count": 14, "svg_output_count": 14, @@ -7440,13 +7439,13 @@ "negative_space.png" ], "project_digest": "1823080b6fef9ef4" - } + }, + "design_role": "finished example deck and mining corpus" }, { "id": "icon_library.chunk-filled", - "source_path": "ppt-master/skills/ppt-master/templates/icons/chunk-filled", + "source_path": "svglide-design-patterns/icon_library/icon_library.chunk-filled", "kind": "icon_library", - "ppt_master_role": "icon library summary and searchable index", "svglide_target": "icon-registry/style-binding", "protocol_compatibility": "needs_normalization", "conversion_strategy": "Expose a library-level summary and searchable index; select one style library per deck and never inject every SVG into the prompt.", @@ -7526,13 +7525,13 @@ "angle-right" ], "index_policy": "library_summary_only" - } + }, + "design_role": "icon library summary and searchable index" }, { "id": "icon_library.phosphor-duotone", - "source_path": "ppt-master/skills/ppt-master/templates/icons/phosphor-duotone", + "source_path": "svglide-design-patterns/icon_library/icon_library.phosphor-duotone", "kind": "icon_library", - "ppt_master_role": "icon library summary and searchable index", "svglide_target": "icon-registry/style-binding", "protocol_compatibility": "needs_normalization", "conversion_strategy": "Expose a library-level summary and searchable index; select one style library per deck and never inject every SVG into the prompt.", @@ -7612,13 +7611,13 @@ "align-right" ], "index_policy": "library_summary_only" - } + }, + "design_role": "icon library summary and searchable index" }, { "id": "icon_library.simple-icons", - "source_path": "ppt-master/skills/ppt-master/templates/icons/simple-icons", + "source_path": "svglide-design-patterns/icon_library/icon_library.simple-icons", "kind": "icon_library", - "ppt_master_role": "icon library summary and searchable index", "svglide_target": "icon-registry/style-binding", "protocol_compatibility": "needs_normalization", "conversion_strategy": "Expose a library-level summary and searchable index; select one style library per deck and never inject every SVG into the prompt.", @@ -7698,13 +7697,13 @@ "abbrobotstudio" ], "index_policy": "library_summary_only" - } + }, + "design_role": "icon library summary and searchable index" }, { "id": "icon_library.tabler-filled", - "source_path": "ppt-master/skills/ppt-master/templates/icons/tabler-filled", + "source_path": "svglide-design-patterns/icon_library/icon_library.tabler-filled", "kind": "icon_library", - "ppt_master_role": "icon library summary and searchable index", "svglide_target": "icon-registry/style-binding", "protocol_compatibility": "needs_normalization", "conversion_strategy": "Expose a library-level summary and searchable index; select one style library per deck and never inject every SVG into the prompt.", @@ -7785,13 +7784,13 @@ "align-box-bottom-center" ], "index_policy": "library_summary_only" - } + }, + "design_role": "icon library summary and searchable index" }, { "id": "icon_library.tabler-outline", - "source_path": "ppt-master/skills/ppt-master/templates/icons/tabler-outline", + "source_path": "svglide-design-patterns/icon_library/icon_library.tabler-outline", "kind": "icon_library", - "ppt_master_role": "icon library summary and searchable index", "svglide_target": "icon-registry/style-binding", "protocol_compatibility": "needs_normalization", "conversion_strategy": "Expose a library-level summary and searchable index; select one style library per deck and never inject every SVG into the prompt.", @@ -7865,13 +7864,13 @@ "adjustments-alt" ], "index_policy": "library_summary_only" - } + }, + "design_role": "icon library summary and searchable index" }, { "id": "image_palette.cool-corporate", - "source_path": "ppt-master/skills/ppt-master/references/image-palettes/cool-corporate.md", + "source_path": "svglide-design-patterns/image_palette/image_palette.cool-corporate", "kind": "image_palette", - "ppt_master_role": "image palette reference", "svglide_target": "asset_strategy/image-palette-binding", "protocol_compatibility": "documentation_reference", "conversion_strategy": "Derive palette tags and image-planning hints; do not copy generated images.", @@ -7901,13 +7900,13 @@ "metadata": { "summary": "Palette: cool-corporate", "bytes": 1697 - } + }, + "design_role": "image palette reference" }, { "id": "image_palette.dark-cinematic", - "source_path": "ppt-master/skills/ppt-master/references/image-palettes/dark-cinematic.md", + "source_path": "svglide-design-patterns/image_palette/image_palette.dark-cinematic", "kind": "image_palette", - "ppt_master_role": "image palette reference", "svglide_target": "asset_strategy/image-palette-binding", "protocol_compatibility": "documentation_reference", "conversion_strategy": "Derive palette tags and image-planning hints; do not copy generated images.", @@ -7937,13 +7936,13 @@ "metadata": { "summary": "Palette: dark-cinematic", "bytes": 2013 - } + }, + "design_role": "image palette reference" }, { "id": "image_palette.duotone", - "source_path": "ppt-master/skills/ppt-master/references/image-palettes/duotone.md", + "source_path": "svglide-design-patterns/image_palette/image_palette.duotone", "kind": "image_palette", - "ppt_master_role": "image palette reference", "svglide_target": "asset_strategy/image-palette-binding", "protocol_compatibility": "documentation_reference", "conversion_strategy": "Derive palette tags and image-planning hints; do not copy generated images.", @@ -7972,13 +7971,13 @@ "metadata": { "summary": "Palette: duotone", "bytes": 1495 - } + }, + "design_role": "image palette reference" }, { "id": "image_palette.earthy-dusty", - "source_path": "ppt-master/skills/ppt-master/references/image-palettes/earthy-dusty.md", + "source_path": "svglide-design-patterns/image_palette/image_palette.earthy-dusty", "kind": "image_palette", - "ppt_master_role": "image palette reference", "svglide_target": "asset_strategy/image-palette-binding", "protocol_compatibility": "documentation_reference", "conversion_strategy": "Derive palette tags and image-planning hints; do not copy generated images.", @@ -8008,13 +8007,13 @@ "metadata": { "summary": "Palette: earthy-dusty", "bytes": 1897 - } + }, + "design_role": "image palette reference" }, { "id": "image_palette.editorial-classic", - "source_path": "ppt-master/skills/ppt-master/references/image-palettes/editorial-classic.md", + "source_path": "svglide-design-patterns/image_palette/image_palette.editorial-classic", "kind": "image_palette", - "ppt_master_role": "image palette reference", "svglide_target": "asset_strategy/image-palette-binding", "protocol_compatibility": "documentation_reference", "conversion_strategy": "Derive palette tags and image-planning hints; do not copy generated images.", @@ -8044,13 +8043,13 @@ "metadata": { "summary": "Palette: editorial-classic", "bytes": 1470 - } + }, + "design_role": "image palette reference" }, { "id": "image_palette.frost-ice", - "source_path": "ppt-master/skills/ppt-master/references/image-palettes/frost-ice.md", + "source_path": "svglide-design-patterns/image_palette/image_palette.frost-ice", "kind": "image_palette", - "ppt_master_role": "image palette reference", "svglide_target": "asset_strategy/image-palette-binding", "protocol_compatibility": "documentation_reference", "conversion_strategy": "Derive palette tags and image-planning hints; do not copy generated images.", @@ -8080,13 +8079,13 @@ "metadata": { "summary": "Palette: frost-ice", "bytes": 1673 - } + }, + "design_role": "image palette reference" }, { "id": "image_palette.jewel-tone", - "source_path": "ppt-master/skills/ppt-master/references/image-palettes/jewel-tone.md", + "source_path": "svglide-design-patterns/image_palette/image_palette.jewel-tone", "kind": "image_palette", - "ppt_master_role": "image palette reference", "svglide_target": "asset_strategy/image-palette-binding", "protocol_compatibility": "documentation_reference", "conversion_strategy": "Derive palette tags and image-planning hints; do not copy generated images.", @@ -8116,13 +8115,13 @@ "metadata": { "summary": "Palette: jewel-tone", "bytes": 1793 - } + }, + "design_role": "image palette reference" }, { "id": "image_palette.macaron", - "source_path": "ppt-master/skills/ppt-master/references/image-palettes/macaron.md", + "source_path": "svglide-design-patterns/image_palette/image_palette.macaron", "kind": "image_palette", - "ppt_master_role": "image palette reference", "svglide_target": "asset_strategy/image-palette-binding", "protocol_compatibility": "documentation_reference", "conversion_strategy": "Derive palette tags and image-planning hints; do not copy generated images.", @@ -8151,13 +8150,13 @@ "metadata": { "summary": "Palette: macaron", "bytes": 1676 - } + }, + "design_role": "image palette reference" }, { "id": "image_palette.mono-ink", - "source_path": "ppt-master/skills/ppt-master/references/image-palettes/mono-ink.md", + "source_path": "svglide-design-patterns/image_palette/image_palette.mono-ink", "kind": "image_palette", - "ppt_master_role": "image palette reference", "svglide_target": "asset_strategy/image-palette-binding", "protocol_compatibility": "documentation_reference", "conversion_strategy": "Derive palette tags and image-planning hints; do not copy generated images.", @@ -8187,13 +8186,13 @@ "metadata": { "summary": "Palette: mono-ink", "bytes": 2584 - } + }, + "design_role": "image palette reference" }, { "id": "image_palette.nature-organic", - "source_path": "ppt-master/skills/ppt-master/references/image-palettes/nature-organic.md", + "source_path": "svglide-design-patterns/image_palette/image_palette.nature-organic", "kind": "image_palette", - "ppt_master_role": "image palette reference", "svglide_target": "asset_strategy/image-palette-binding", "protocol_compatibility": "documentation_reference", "conversion_strategy": "Derive palette tags and image-planning hints; do not copy generated images.", @@ -8223,13 +8222,13 @@ "metadata": { "summary": "Palette: nature-organic", "bytes": 1335 - } + }, + "design_role": "image palette reference" }, { "id": "image_palette.sunset-gradient", - "source_path": "ppt-master/skills/ppt-master/references/image-palettes/sunset-gradient.md", + "source_path": "svglide-design-patterns/image_palette/image_palette.sunset-gradient", "kind": "image_palette", - "ppt_master_role": "image palette reference", "svglide_target": "asset_strategy/image-palette-binding", "protocol_compatibility": "documentation_reference", "conversion_strategy": "Derive palette tags and image-planning hints; do not copy generated images.", @@ -8259,13 +8258,13 @@ "metadata": { "summary": "Palette: sunset-gradient", "bytes": 2193 - } + }, + "design_role": "image palette reference" }, { "id": "image_palette.tech-neon", - "source_path": "ppt-master/skills/ppt-master/references/image-palettes/tech-neon.md", + "source_path": "svglide-design-patterns/image_palette/image_palette.tech-neon", "kind": "image_palette", - "ppt_master_role": "image palette reference", "svglide_target": "asset_strategy/image-palette-binding", "protocol_compatibility": "documentation_reference", "conversion_strategy": "Derive palette tags and image-planning hints; do not copy generated images.", @@ -8295,13 +8294,13 @@ "metadata": { "summary": "Palette: tech-neon", "bytes": 1550 - } + }, + "design_role": "image palette reference" }, { "id": "image_palette.vivid-launch", - "source_path": "ppt-master/skills/ppt-master/references/image-palettes/vivid-launch.md", + "source_path": "svglide-design-patterns/image_palette/image_palette.vivid-launch", "kind": "image_palette", - "ppt_master_role": "image palette reference", "svglide_target": "asset_strategy/image-palette-binding", "protocol_compatibility": "documentation_reference", "conversion_strategy": "Derive palette tags and image-planning hints; do not copy generated images.", @@ -8331,13 +8330,13 @@ "metadata": { "summary": "Palette: vivid-launch", "bytes": 1478 - } + }, + "design_role": "image palette reference" }, { "id": "image_palette.warm-earth", - "source_path": "ppt-master/skills/ppt-master/references/image-palettes/warm-earth.md", + "source_path": "svglide-design-patterns/image_palette/image_palette.warm-earth", "kind": "image_palette", - "ppt_master_role": "image palette reference", "svglide_target": "asset_strategy/image-palette-binding", "protocol_compatibility": "documentation_reference", "conversion_strategy": "Derive palette tags and image-planning hints; do not copy generated images.", @@ -8367,13 +8366,13 @@ "metadata": { "summary": "Palette: warm-earth", "bytes": 1686 - } + }, + "design_role": "image palette reference" }, { "id": "image_reference_collection.palette", - "source_path": "ppt-master/skills/ppt-master/references/ai-image-comparison/palette", + "source_path": "svglide-design-patterns/image_reference_collection/image_reference_collection.palette", "kind": "image_reference_collection", - "ppt_master_role": "image_palette_visual_references", "svglide_target": "asset_strategy/reference-oracle", "protocol_compatibility": "reference_only", "conversion_strategy": "Summarize bitmap reference coverage; do not copy images into CLI runtime.", @@ -8444,13 +8443,13 @@ "sunset-gradient.png", "tech-neon.png" ] - } + }, + "design_role": "image_palette_visual_references" }, { "id": "image_reference_collection.rendering", - "source_path": "ppt-master/skills/ppt-master/references/ai-image-comparison/rendering", + "source_path": "svglide-design-patterns/image_reference_collection/image_reference_collection.rendering", "kind": "image_reference_collection", - "ppt_master_role": "image_rendering_visual_references", "svglide_target": "asset_strategy/reference-oracle", "protocol_compatibility": "reference_only", "conversion_strategy": "Summarize bitmap reference coverage; do not copy images into CLI runtime.", @@ -8524,13 +8523,13 @@ "minimalist-swiss.png", "nature.png" ] - } + }, + "design_role": "image_rendering_visual_references" }, { "id": "image_reference_collection.type", - "source_path": "ppt-master/skills/ppt-master/references/ai-image-comparison/type", + "source_path": "svglide-design-patterns/image_reference_collection/image_reference_collection.type", "kind": "image_reference_collection", - "ppt_master_role": "image_type_visual_references", "svglide_target": "asset_strategy/reference-oracle", "protocol_compatibility": "reference_only", "conversion_strategy": "Summarize bitmap reference coverage; do not copy images into CLI runtime.", @@ -8585,13 +8584,13 @@ "scene.png", "timeline.png" ] - } + }, + "design_role": "image_type_visual_references" }, { "id": "image_rendering.3d-isometric", - "source_path": "ppt-master/skills/ppt-master/references/image-renderings/3d-isometric.md", + "source_path": "svglide-design-patterns/image_rendering/image_rendering.3d-isometric", "kind": "image_rendering", - "ppt_master_role": "image rendering preset", "svglide_target": "asset_strategy/rendering-preset", "protocol_compatibility": "documentation_reference", "conversion_strategy": "Derive rendering tags and fallback policy for image planning.", @@ -8621,13 +8620,13 @@ "metadata": { "summary": "Rendering: 3d-isometric", "bytes": 2738 - } + }, + "design_role": "image rendering preset" }, { "id": "image_rendering.blueprint", - "source_path": "ppt-master/skills/ppt-master/references/image-renderings/blueprint.md", + "source_path": "svglide-design-patterns/image_rendering/image_rendering.blueprint", "kind": "image_rendering", - "ppt_master_role": "image rendering preset", "svglide_target": "asset_strategy/rendering-preset", "protocol_compatibility": "documentation_reference", "conversion_strategy": "Derive rendering tags and fallback policy for image planning.", @@ -8656,13 +8655,13 @@ "metadata": { "summary": "Rendering: blueprint", "bytes": 2667 - } + }, + "design_role": "image rendering preset" }, { "id": "image_rendering.chalkboard", - "source_path": "ppt-master/skills/ppt-master/references/image-renderings/chalkboard.md", + "source_path": "svglide-design-patterns/image_rendering/image_rendering.chalkboard", "kind": "image_rendering", - "ppt_master_role": "image rendering preset", "svglide_target": "asset_strategy/rendering-preset", "protocol_compatibility": "documentation_reference", "conversion_strategy": "Derive rendering tags and fallback policy for image planning.", @@ -8691,13 +8690,13 @@ "metadata": { "summary": "Rendering: chalkboard", "bytes": 2566 - } + }, + "design_role": "image rendering preset" }, { "id": "image_rendering.corporate-photo", - "source_path": "ppt-master/skills/ppt-master/references/image-renderings/corporate-photo.md", + "source_path": "svglide-design-patterns/image_rendering/image_rendering.corporate-photo", "kind": "image_rendering", - "ppt_master_role": "image rendering preset", "svglide_target": "asset_strategy/rendering-preset", "protocol_compatibility": "documentation_reference", "conversion_strategy": "Derive rendering tags and fallback policy for image planning.", @@ -8727,13 +8726,13 @@ "metadata": { "summary": "Rendering: corporate-photo", "bytes": 3495 - } + }, + "design_role": "image rendering preset" }, { "id": "image_rendering.digital-dashboard", - "source_path": "ppt-master/skills/ppt-master/references/image-renderings/digital-dashboard.md", + "source_path": "svglide-design-patterns/image_rendering/image_rendering.digital-dashboard", "kind": "image_rendering", - "ppt_master_role": "image rendering preset", "svglide_target": "asset_strategy/rendering-preset", "protocol_compatibility": "documentation_reference", "conversion_strategy": "Derive rendering tags and fallback policy for image planning.", @@ -8763,13 +8762,13 @@ "metadata": { "summary": "Rendering: digital-dashboard", "bytes": 2652 - } + }, + "design_role": "image rendering preset" }, { "id": "image_rendering.editorial", - "source_path": "ppt-master/skills/ppt-master/references/image-renderings/editorial.md", + "source_path": "svglide-design-patterns/image_rendering/image_rendering.editorial", "kind": "image_rendering", - "ppt_master_role": "image rendering preset", "svglide_target": "asset_strategy/rendering-preset", "protocol_compatibility": "documentation_reference", "conversion_strategy": "Derive rendering tags and fallback policy for image planning.", @@ -8798,13 +8797,13 @@ "metadata": { "summary": "Rendering: editorial", "bytes": 2606 - } + }, + "design_role": "image rendering preset" }, { "id": "image_rendering.fantasy-animation", - "source_path": "ppt-master/skills/ppt-master/references/image-renderings/fantasy-animation.md", + "source_path": "svglide-design-patterns/image_rendering/image_rendering.fantasy-animation", "kind": "image_rendering", - "ppt_master_role": "image rendering preset", "svglide_target": "asset_strategy/rendering-preset", "protocol_compatibility": "documentation_reference", "conversion_strategy": "Derive rendering tags and fallback policy for image planning.", @@ -8834,13 +8833,13 @@ "metadata": { "summary": "Rendering: fantasy-animation", "bytes": 2840 - } + }, + "design_role": "image rendering preset" }, { "id": "image_rendering.flat", - "source_path": "ppt-master/skills/ppt-master/references/image-renderings/flat.md", + "source_path": "svglide-design-patterns/image_rendering/image_rendering.flat", "kind": "image_rendering", - "ppt_master_role": "image rendering preset", "svglide_target": "asset_strategy/rendering-preset", "protocol_compatibility": "documentation_reference", "conversion_strategy": "Derive rendering tags and fallback policy for image planning.", @@ -8869,13 +8868,13 @@ "metadata": { "summary": "Rendering: flat", "bytes": 2379 - } + }, + "design_role": "image rendering preset" }, { "id": "image_rendering.glassmorphism", - "source_path": "ppt-master/skills/ppt-master/references/image-renderings/glassmorphism.md", + "source_path": "svglide-design-patterns/image_rendering/image_rendering.glassmorphism", "kind": "image_rendering", - "ppt_master_role": "image rendering preset", "svglide_target": "asset_strategy/rendering-preset", "protocol_compatibility": "documentation_reference", "conversion_strategy": "Derive rendering tags and fallback policy for image planning.", @@ -8904,13 +8903,13 @@ "metadata": { "summary": "Rendering: glassmorphism", "bytes": 3181 - } + }, + "design_role": "image rendering preset" }, { "id": "image_rendering.ink-notes", - "source_path": "ppt-master/skills/ppt-master/references/image-renderings/ink-notes.md", + "source_path": "svglide-design-patterns/image_rendering/image_rendering.ink-notes", "kind": "image_rendering", - "ppt_master_role": "image rendering preset", "svglide_target": "asset_strategy/rendering-preset", "protocol_compatibility": "documentation_reference", "conversion_strategy": "Derive rendering tags and fallback policy for image planning.", @@ -8940,13 +8939,13 @@ "metadata": { "summary": "Rendering: ink-notes", "bytes": 3468 - } + }, + "design_role": "image rendering preset" }, { "id": "image_rendering.minimalist-swiss", - "source_path": "ppt-master/skills/ppt-master/references/image-renderings/minimalist-swiss.md", + "source_path": "svglide-design-patterns/image_rendering/image_rendering.minimalist-swiss", "kind": "image_rendering", - "ppt_master_role": "image rendering preset", "svglide_target": "asset_strategy/rendering-preset", "protocol_compatibility": "documentation_reference", "conversion_strategy": "Derive rendering tags and fallback policy for image planning.", @@ -8976,13 +8975,13 @@ "metadata": { "summary": "Rendering: minimalist-swiss", "bytes": 2900 - } + }, + "design_role": "image rendering preset" }, { "id": "image_rendering.nature", - "source_path": "ppt-master/skills/ppt-master/references/image-renderings/nature.md", + "source_path": "svglide-design-patterns/image_rendering/image_rendering.nature", "kind": "image_rendering", - "ppt_master_role": "image rendering preset", "svglide_target": "asset_strategy/rendering-preset", "protocol_compatibility": "documentation_reference", "conversion_strategy": "Derive rendering tags and fallback policy for image planning.", @@ -9011,13 +9010,13 @@ "metadata": { "summary": "Rendering: nature", "bytes": 2637 - } + }, + "design_role": "image rendering preset" }, { "id": "image_rendering.paper-cut", - "source_path": "ppt-master/skills/ppt-master/references/image-renderings/paper-cut.md", + "source_path": "svglide-design-patterns/image_rendering/image_rendering.paper-cut", "kind": "image_rendering", - "ppt_master_role": "image rendering preset", "svglide_target": "asset_strategy/rendering-preset", "protocol_compatibility": "documentation_reference", "conversion_strategy": "Derive rendering tags and fallback policy for image planning.", @@ -9047,13 +9046,13 @@ "metadata": { "summary": "Rendering: paper-cut", "bytes": 2759 - } + }, + "design_role": "image rendering preset" }, { "id": "image_rendering.pixel-art", - "source_path": "ppt-master/skills/ppt-master/references/image-renderings/pixel-art.md", + "source_path": "svglide-design-patterns/image_rendering/image_rendering.pixel-art", "kind": "image_rendering", - "ppt_master_role": "image rendering preset", "svglide_target": "asset_strategy/rendering-preset", "protocol_compatibility": "documentation_reference", "conversion_strategy": "Derive rendering tags and fallback policy for image planning.", @@ -9083,13 +9082,13 @@ "metadata": { "summary": "Rendering: pixel-art", "bytes": 2536 - } + }, + "design_role": "image rendering preset" }, { "id": "image_rendering.screen-print", - "source_path": "ppt-master/skills/ppt-master/references/image-renderings/screen-print.md", + "source_path": "svglide-design-patterns/image_rendering/image_rendering.screen-print", "kind": "image_rendering", - "ppt_master_role": "image rendering preset", "svglide_target": "asset_strategy/rendering-preset", "protocol_compatibility": "documentation_reference", "conversion_strategy": "Derive rendering tags and fallback policy for image planning.", @@ -9119,13 +9118,13 @@ "metadata": { "summary": "Rendering: screen-print", "bytes": 2854 - } + }, + "design_role": "image rendering preset" }, { "id": "image_rendering.sketch-notes", - "source_path": "ppt-master/skills/ppt-master/references/image-renderings/sketch-notes.md", + "source_path": "svglide-design-patterns/image_rendering/image_rendering.sketch-notes", "kind": "image_rendering", - "ppt_master_role": "image rendering preset", "svglide_target": "asset_strategy/rendering-preset", "protocol_compatibility": "documentation_reference", "conversion_strategy": "Derive rendering tags and fallback policy for image planning.", @@ -9155,13 +9154,13 @@ "metadata": { "summary": "Rendering: sketch-notes", "bytes": 3467 - } + }, + "design_role": "image rendering preset" }, { "id": "image_rendering.vector-illustration", - "source_path": "ppt-master/skills/ppt-master/references/image-renderings/vector-illustration.md", + "source_path": "svglide-design-patterns/image_rendering/image_rendering.vector-illustration", "kind": "image_rendering", - "ppt_master_role": "image rendering preset", "svglide_target": "asset_strategy/rendering-preset", "protocol_compatibility": "documentation_reference", "conversion_strategy": "Derive rendering tags and fallback policy for image planning.", @@ -9191,13 +9190,13 @@ "metadata": { "summary": "Rendering: vector-illustration", "bytes": 2909 - } + }, + "design_role": "image rendering preset" }, { "id": "image_rendering.vintage-poster", - "source_path": "ppt-master/skills/ppt-master/references/image-renderings/vintage-poster.md", + "source_path": "svglide-design-patterns/image_rendering/image_rendering.vintage-poster", "kind": "image_rendering", - "ppt_master_role": "image rendering preset", "svglide_target": "asset_strategy/rendering-preset", "protocol_compatibility": "documentation_reference", "conversion_strategy": "Derive rendering tags and fallback policy for image planning.", @@ -9227,13 +9226,13 @@ "metadata": { "summary": "Rendering: vintage-poster", "bytes": 2975 - } + }, + "design_role": "image rendering preset" }, { "id": "image_rendering.warm-scene", - "source_path": "ppt-master/skills/ppt-master/references/image-renderings/warm-scene.md", + "source_path": "svglide-design-patterns/image_rendering/image_rendering.warm-scene", "kind": "image_rendering", - "ppt_master_role": "image rendering preset", "svglide_target": "asset_strategy/rendering-preset", "protocol_compatibility": "documentation_reference", "conversion_strategy": "Derive rendering tags and fallback policy for image planning.", @@ -9263,13 +9262,13 @@ "metadata": { "summary": "Rendering: warm-scene", "bytes": 3022 - } + }, + "design_role": "image rendering preset" }, { "id": "image_rendering.watercolor", - "source_path": "ppt-master/skills/ppt-master/references/image-renderings/watercolor.md", + "source_path": "svglide-design-patterns/image_rendering/image_rendering.watercolor", "kind": "image_rendering", - "ppt_master_role": "image rendering preset", "svglide_target": "asset_strategy/rendering-preset", "protocol_compatibility": "documentation_reference", "conversion_strategy": "Derive rendering tags and fallback policy for image planning.", @@ -9298,13 +9297,13 @@ "metadata": { "summary": "Rendering: watercolor", "bytes": 2457 - } + }, + "design_role": "image rendering preset" }, { "id": "image_type.comparison", - "source_path": "ppt-master/skills/ppt-master/references/image-type-templates/comparison.md", + "source_path": "svglide-design-patterns/image_type_template/image_type.comparison", "kind": "image_type_template", - "ppt_master_role": "image composition recipe", "svglide_target": "asset_strategy/image-composition-recipe", "protocol_compatibility": "documentation_reference", "conversion_strategy": "Derive image composition contracts and prompt planning tags.", @@ -9334,13 +9333,13 @@ "metadata": { "summary": "Type: comparison", "bytes": 3532 - } + }, + "design_role": "image composition recipe" }, { "id": "image_type.cycle", - "source_path": "ppt-master/skills/ppt-master/references/image-type-templates/cycle.md", + "source_path": "svglide-design-patterns/image_type_template/image_type.cycle", "kind": "image_type_template", - "ppt_master_role": "image composition recipe", "svglide_target": "asset_strategy/image-composition-recipe", "protocol_compatibility": "documentation_reference", "conversion_strategy": "Derive image composition contracts and prompt planning tags.", @@ -9370,13 +9369,13 @@ "metadata": { "summary": "Type: cycle", "bytes": 3371 - } + }, + "design_role": "image composition recipe" }, { "id": "image_type.flowchart", - "source_path": "ppt-master/skills/ppt-master/references/image-type-templates/flowchart.md", + "source_path": "svglide-design-patterns/image_type_template/image_type.flowchart", "kind": "image_type_template", - "ppt_master_role": "image composition recipe", "svglide_target": "asset_strategy/image-composition-recipe", "protocol_compatibility": "documentation_reference", "conversion_strategy": "Derive image composition contracts and prompt planning tags.", @@ -9406,13 +9405,13 @@ "metadata": { "summary": "Type: flowchart", "bytes": 2919 - } + }, + "design_role": "image composition recipe" }, { "id": "image_type.framework", - "source_path": "ppt-master/skills/ppt-master/references/image-type-templates/framework.md", + "source_path": "svglide-design-patterns/image_type_template/image_type.framework", "kind": "image_type_template", - "ppt_master_role": "image composition recipe", "svglide_target": "asset_strategy/image-composition-recipe", "protocol_compatibility": "documentation_reference", "conversion_strategy": "Derive image composition contracts and prompt planning tags.", @@ -9442,13 +9441,13 @@ "metadata": { "summary": "Type: framework", "bytes": 4727 - } + }, + "design_role": "image composition recipe" }, { "id": "image_type.funnel", - "source_path": "ppt-master/skills/ppt-master/references/image-type-templates/funnel.md", + "source_path": "svglide-design-patterns/image_type_template/image_type.funnel", "kind": "image_type_template", - "ppt_master_role": "image composition recipe", "svglide_target": "asset_strategy/image-composition-recipe", "protocol_compatibility": "documentation_reference", "conversion_strategy": "Derive image composition contracts and prompt planning tags.", @@ -9478,13 +9477,13 @@ "metadata": { "summary": "Type: funnel", "bytes": 3737 - } + }, + "design_role": "image composition recipe" }, { "id": "image_type.infographic", - "source_path": "ppt-master/skills/ppt-master/references/image-type-templates/infographic.md", + "source_path": "svglide-design-patterns/image_type_template/image_type.infographic", "kind": "image_type_template", - "ppt_master_role": "image composition recipe", "svglide_target": "asset_strategy/image-composition-recipe", "protocol_compatibility": "documentation_reference", "conversion_strategy": "Derive image composition contracts and prompt planning tags.", @@ -9514,13 +9513,13 @@ "metadata": { "summary": "Type: infographic", "bytes": 3538 - } + }, + "design_role": "image composition recipe" }, { "id": "image_type.map", - "source_path": "ppt-master/skills/ppt-master/references/image-type-templates/map.md", + "source_path": "svglide-design-patterns/image_type_template/image_type.map", "kind": "image_type_template", - "ppt_master_role": "image composition recipe", "svglide_target": "asset_strategy/image-composition-recipe", "protocol_compatibility": "documentation_reference", "conversion_strategy": "Derive image composition contracts and prompt planning tags.", @@ -9550,13 +9549,13 @@ "metadata": { "summary": "Type: map", "bytes": 3923 - } + }, + "design_role": "image composition recipe" }, { "id": "image_type.matrix", - "source_path": "ppt-master/skills/ppt-master/references/image-type-templates/matrix.md", + "source_path": "svglide-design-patterns/image_type_template/image_type.matrix", "kind": "image_type_template", - "ppt_master_role": "image composition recipe", "svglide_target": "asset_strategy/image-composition-recipe", "protocol_compatibility": "documentation_reference", "conversion_strategy": "Derive image composition contracts and prompt planning tags.", @@ -9586,13 +9585,13 @@ "metadata": { "summary": "Type: matrix", "bytes": 3867 - } + }, + "design_role": "image composition recipe" }, { "id": "image_type.pyramid", - "source_path": "ppt-master/skills/ppt-master/references/image-type-templates/pyramid.md", + "source_path": "svglide-design-patterns/image_type_template/image_type.pyramid", "kind": "image_type_template", - "ppt_master_role": "image composition recipe", "svglide_target": "asset_strategy/image-composition-recipe", "protocol_compatibility": "documentation_reference", "conversion_strategy": "Derive image composition contracts and prompt planning tags.", @@ -9622,13 +9621,13 @@ "metadata": { "summary": "Type: pyramid", "bytes": 3493 - } + }, + "design_role": "image composition recipe" }, { "id": "image_type.scene", - "source_path": "ppt-master/skills/ppt-master/references/image-type-templates/scene.md", + "source_path": "svglide-design-patterns/image_type_template/image_type.scene", "kind": "image_type_template", - "ppt_master_role": "image composition recipe", "svglide_target": "asset_strategy/image-composition-recipe", "protocol_compatibility": "documentation_reference", "conversion_strategy": "Derive image composition contracts and prompt planning tags.", @@ -9658,13 +9657,13 @@ "metadata": { "summary": "Type: scene", "bytes": 3475 - } + }, + "design_role": "image composition recipe" }, { "id": "image_type.timeline", - "source_path": "ppt-master/skills/ppt-master/references/image-type-templates/timeline.md", + "source_path": "svglide-design-patterns/image_type_template/image_type.timeline", "kind": "image_type_template", - "ppt_master_role": "image composition recipe", "svglide_target": "asset_strategy/image-composition-recipe", "protocol_compatibility": "documentation_reference", "conversion_strategy": "Derive image composition contracts and prompt planning tags.", @@ -9694,13 +9693,13 @@ "metadata": { "summary": "Type: timeline", "bytes": 2849 - } + }, + "design_role": "image composition recipe" }, { "id": "layout.academic_defense", - "source_path": "ppt-master/skills/ppt-master/templates/layouts/academic_defense", + "source_path": "svglide-design-patterns/layout_template/layout.academic_defense", "kind": "layout_template", - "ppt_master_role": "page skeleton and rhythm template", "svglide_target": "svg-seeds.json/layout-skeletons", "protocol_compatibility": "needs_normalization", "conversion_strategy": "Extract page slots, density, rhythm, and spacing contracts; regenerate as SVGlide seeds.", @@ -9749,7 +9748,7 @@ "summary": "Thesis defense, academic presentations, research progress reports, grant applications.", "metadata": { "summary": "Thesis defense, academic presentations, research progress reports, grant applications.", - "canvas_format": "ppt169", + "canvas_format": "svglide_16x9", "page_count": 5, "page_types": [ "cover", @@ -9759,13 +9758,13 @@ "ending" ], "svg_file_count": 5 - } + }, + "design_role": "page skeleton and rhythm template" }, { "id": "layout.ai_ops", - "source_path": "ppt-master/skills/ppt-master/templates/layouts/ai_ops", + "source_path": "svglide-design-patterns/layout_template/layout.ai_ops", "kind": "layout_template", - "ppt_master_role": "page skeleton and rhythm template", "svglide_target": "svg-seeds.json/layout-skeletons", "protocol_compatibility": "needs_normalization", "conversion_strategy": "Extract page slots, density, rhythm, and spacing contracts; regenerate as SVGlide seeds.", @@ -9821,7 +9820,7 @@ "summary": "Telecom AI operations architecture, IT system overviews, digital transformation proposals, smart infrastructure reports.", "metadata": { "summary": "Telecom AI operations architecture, IT system overviews, digital transformation proposals, smart infrastructure reports.", - "canvas_format": "ppt169", + "canvas_format": "svglide_16x9", "page_count": 6, "page_types": [ "cover", @@ -9832,13 +9831,13 @@ "reference_style" ], "svg_file_count": 6 - } + }, + "design_role": "page skeleton and rhythm template" }, { "id": "layout.government_blue", - "source_path": "ppt-master/skills/ppt-master/templates/layouts/government_blue", + "source_path": "svglide-design-patterns/layout_template/layout.government_blue", "kind": "layout_template", - "ppt_master_role": "page skeleton and rhythm template", "svglide_target": "svg-seeds.json/layout-skeletons", "protocol_compatibility": "needs_normalization", "conversion_strategy": "Extract page slots, density, rhythm, and spacing contracts; regenerate as SVGlide seeds.", @@ -9893,7 +9892,7 @@ "summary": "Key project briefings, Five-Year Plan presentations, work summaries, investment promotion, policy interpretation.", "metadata": { "summary": "Key project briefings, Five-Year Plan presentations, work summaries, investment promotion, policy interpretation.", - "canvas_format": "ppt169", + "canvas_format": "svglide_16x9", "page_count": 5, "page_types": [ "cover", @@ -9903,13 +9902,13 @@ "ending" ], "svg_file_count": 5 - } + }, + "design_role": "page skeleton and rhythm template" }, { "id": "layout.government_red", - "source_path": "ppt-master/skills/ppt-master/templates/layouts/government_red", + "source_path": "svglide-design-patterns/layout_template/layout.government_red", "kind": "layout_template", - "ppt_master_role": "page skeleton and rhythm template", "svglide_target": "svg-seeds.json/layout-skeletons", "protocol_compatibility": "needs_normalization", "conversion_strategy": "Extract page slots, density, rhythm, and spacing contracts; regenerate as SVGlide seeds.", @@ -9960,7 +9959,7 @@ "summary": "Government briefings, policy interpretation, work summaries, project introductions, investment promotion.", "metadata": { "summary": "Government briefings, policy interpretation, work summaries, project introductions, investment promotion.", - "canvas_format": "ppt169", + "canvas_format": "svglide_16x9", "page_count": 5, "page_types": [ "cover", @@ -9970,13 +9969,13 @@ "ending" ], "svg_file_count": 5 - } + }, + "design_role": "page skeleton and rhythm template" }, { "id": "layout.medical_university", - "source_path": "ppt-master/skills/ppt-master/templates/layouts/medical_university", + "source_path": "svglide-design-patterns/layout_template/layout.medical_university", "kind": "layout_template", - "ppt_master_role": "page skeleton and rhythm template", "svglide_target": "svg-seeds.json/layout-skeletons", "protocol_compatibility": "needs_normalization", "conversion_strategy": "Extract page slots, density, rhythm, and spacing contracts; regenerate as SVGlide seeds.", @@ -10029,7 +10028,7 @@ "summary": "Medical academic reports, case discussions, research presentations, hospital work reports, medical education and training.", "metadata": { "summary": "Medical academic reports, case discussions, research presentations, hospital work reports, medical education and training.", - "canvas_format": "ppt169", + "canvas_format": "svglide_16x9", "page_count": 5, "page_types": [ "cover", @@ -10039,13 +10038,13 @@ "ending" ], "svg_file_count": 5 - } + }, + "design_role": "page skeleton and rhythm template" }, { "id": "layout.pixel_retro", - "source_path": "ppt-master/skills/ppt-master/templates/layouts/pixel_retro", + "source_path": "svglide-design-patterns/layout_template/layout.pixel_retro", "kind": "layout_template", - "ppt_master_role": "page skeleton and rhythm template", "svglide_target": "svg-seeds.json/layout-skeletons", "protocol_compatibility": "needs_normalization", "conversion_strategy": "Extract page slots, density, rhythm, and spacing contracts; regenerate as SVGlide seeds.", @@ -10097,7 +10096,7 @@ "summary": "Tech talks, programming tutorials, game introductions, geek-style showcases.", "metadata": { "summary": "Tech talks, programming tutorials, game introductions, geek-style showcases.", - "canvas_format": "ppt169", + "canvas_format": "svglide_16x9", "page_count": 5, "page_types": [ "cover", @@ -10107,13 +10106,13 @@ "ending" ], "svg_file_count": 5 - } + }, + "design_role": "page skeleton and rhythm template" }, { "id": "layout.psychology_attachment", - "source_path": "ppt-master/skills/ppt-master/templates/layouts/psychology_attachment", + "source_path": "svglide-design-patterns/layout_template/layout.psychology_attachment", "kind": "layout_template", - "ppt_master_role": "page skeleton and rhythm template", "svglide_target": "svg-seeds.json/layout-skeletons", "protocol_compatibility": "needs_normalization", "conversion_strategy": "Extract page slots, density, rhythm, and spacing contracts; regenerate as SVGlide seeds.", @@ -10165,7 +10164,7 @@ "summary": "Psychotherapy training, academic lectures, counseling case analysis, professional sharing.", "metadata": { "summary": "Psychotherapy training, academic lectures, counseling case analysis, professional sharing.", - "canvas_format": "ppt169", + "canvas_format": "svglide_16x9", "page_count": 5, "page_types": [ "cover", @@ -10175,13 +10174,13 @@ "ending" ], "svg_file_count": 5 - } + }, + "design_role": "page skeleton and rhythm template" }, { "id": "mode.briefing", - "source_path": "ppt-master/skills/ppt-master/references/modes/briefing.md", + "source_path": "svglide-design-patterns/narrative_mode/mode.briefing", "kind": "narrative_mode", - "ppt_master_role": "deck-level narrative mode", "svglide_target": "deck intent and page rhythm selector", "protocol_compatibility": "documentation_reference", "conversion_strategy": "Derive narrative mode tags and page rhythm signals.", @@ -10211,13 +10210,13 @@ "metadata": { "summary": "Mode: briefing", "bytes": 2848 - } + }, + "design_role": "deck-level narrative mode" }, { "id": "mode.instructional", - "source_path": "ppt-master/skills/ppt-master/references/modes/instructional.md", + "source_path": "svglide-design-patterns/narrative_mode/mode.instructional", "kind": "narrative_mode", - "ppt_master_role": "deck-level narrative mode", "svglide_target": "deck intent and page rhythm selector", "protocol_compatibility": "documentation_reference", "conversion_strategy": "Derive narrative mode tags and page rhythm signals.", @@ -10247,13 +10246,13 @@ "metadata": { "summary": "Mode: instructional", "bytes": 1932 - } + }, + "design_role": "deck-level narrative mode" }, { "id": "mode.narrative", - "source_path": "ppt-master/skills/ppt-master/references/modes/narrative.md", + "source_path": "svglide-design-patterns/narrative_mode/mode.narrative", "kind": "narrative_mode", - "ppt_master_role": "deck-level narrative mode", "svglide_target": "deck intent and page rhythm selector", "protocol_compatibility": "documentation_reference", "conversion_strategy": "Derive narrative mode tags and page rhythm signals.", @@ -10282,13 +10281,13 @@ "metadata": { "summary": "Mode: narrative", "bytes": 2163 - } + }, + "design_role": "deck-level narrative mode" }, { "id": "mode.pyramid", - "source_path": "ppt-master/skills/ppt-master/references/modes/pyramid.md", + "source_path": "svglide-design-patterns/narrative_mode/mode.pyramid", "kind": "narrative_mode", - "ppt_master_role": "deck-level narrative mode", "svglide_target": "deck intent and page rhythm selector", "protocol_compatibility": "documentation_reference", "conversion_strategy": "Derive narrative mode tags and page rhythm signals.", @@ -10318,13 +10317,13 @@ "metadata": { "summary": "Mode: pyramid", "bytes": 2811 - } + }, + "design_role": "deck-level narrative mode" }, { "id": "mode.showcase", - "source_path": "ppt-master/skills/ppt-master/references/modes/showcase.md", + "source_path": "svglide-design-patterns/narrative_mode/mode.showcase", "kind": "narrative_mode", - "ppt_master_role": "deck-level narrative mode", "svglide_target": "deck intent and page rhythm selector", "protocol_compatibility": "documentation_reference", "conversion_strategy": "Derive narrative mode tags and page rhythm signals.", @@ -10354,13 +10353,13 @@ "metadata": { "summary": "Mode: showcase", "bytes": 1763 - } + }, + "design_role": "deck-level narrative mode" }, { "id": "visual_style.blueprint", - "source_path": "ppt-master/skills/ppt-master/references/visual-styles/blueprint.md", + "source_path": "svglide-design-patterns/visual_style/visual_style.blueprint", "kind": "visual_style", - "ppt_master_role": "visual style guide", "svglide_target": "style-presets.json/quality-oracle", "protocol_compatibility": "documentation_reference", "conversion_strategy": "Derive style tags, visual anti-patterns, and quality-oracle signals.", @@ -10390,13 +10389,13 @@ "metadata": { "summary": "Visual style: blueprint", "bytes": 2106 - } + }, + "design_role": "visual style guide" }, { "id": "visual_style.brutalist", - "source_path": "ppt-master/skills/ppt-master/references/visual-styles/brutalist.md", + "source_path": "svglide-design-patterns/visual_style/visual_style.brutalist", "kind": "visual_style", - "ppt_master_role": "visual style guide", "svglide_target": "style-presets.json/quality-oracle", "protocol_compatibility": "documentation_reference", "conversion_strategy": "Derive style tags, visual anti-patterns, and quality-oracle signals.", @@ -10426,13 +10425,13 @@ "metadata": { "summary": "Visual style: brutalist", "bytes": 2143 - } + }, + "design_role": "visual style guide" }, { "id": "visual_style.chalkboard", - "source_path": "ppt-master/skills/ppt-master/references/visual-styles/chalkboard.md", + "source_path": "svglide-design-patterns/visual_style/visual_style.chalkboard", "kind": "visual_style", - "ppt_master_role": "visual style guide", "svglide_target": "style-presets.json/quality-oracle", "protocol_compatibility": "documentation_reference", "conversion_strategy": "Derive style tags, visual anti-patterns, and quality-oracle signals.", @@ -10462,13 +10461,13 @@ "metadata": { "summary": "Visual style: chalkboard", "bytes": 1582 - } + }, + "design_role": "visual style guide" }, { "id": "visual_style.dark-tech", - "source_path": "ppt-master/skills/ppt-master/references/visual-styles/dark-tech.md", + "source_path": "svglide-design-patterns/visual_style/visual_style.dark-tech", "kind": "visual_style", - "ppt_master_role": "visual style guide", "svglide_target": "style-presets.json/quality-oracle", "protocol_compatibility": "documentation_reference", "conversion_strategy": "Derive style tags, visual anti-patterns, and quality-oracle signals.", @@ -10499,13 +10498,13 @@ "metadata": { "summary": "Visual style: dark-tech", "bytes": 1429 - } + }, + "design_role": "visual style guide" }, { "id": "visual_style.data-journalism", - "source_path": "ppt-master/skills/ppt-master/references/visual-styles/data-journalism.md", + "source_path": "svglide-design-patterns/visual_style/visual_style.data-journalism", "kind": "visual_style", - "ppt_master_role": "visual style guide", "svglide_target": "style-presets.json/quality-oracle", "protocol_compatibility": "documentation_reference", "conversion_strategy": "Derive style tags, visual anti-patterns, and quality-oracle signals.", @@ -10536,13 +10535,13 @@ "metadata": { "summary": "Visual style: data-journalism", "bytes": 2086 - } + }, + "design_role": "visual style guide" }, { "id": "visual_style.editorial", - "source_path": "ppt-master/skills/ppt-master/references/visual-styles/editorial.md", + "source_path": "svglide-design-patterns/visual_style/visual_style.editorial", "kind": "visual_style", - "ppt_master_role": "visual style guide", "svglide_target": "style-presets.json/quality-oracle", "protocol_compatibility": "documentation_reference", "conversion_strategy": "Derive style tags, visual anti-patterns, and quality-oracle signals.", @@ -10572,13 +10571,13 @@ "metadata": { "summary": "Visual style: editorial", "bytes": 1408 - } + }, + "design_role": "visual style guide" }, { "id": "visual_style.glassmorphism", - "source_path": "ppt-master/skills/ppt-master/references/visual-styles/glassmorphism.md", + "source_path": "svglide-design-patterns/visual_style/visual_style.glassmorphism", "kind": "visual_style", - "ppt_master_role": "visual style guide", "svglide_target": "style-presets.json/quality-oracle", "protocol_compatibility": "documentation_reference", "conversion_strategy": "Derive style tags, visual anti-patterns, and quality-oracle signals.", @@ -10608,13 +10607,13 @@ "metadata": { "summary": "Visual style: glassmorphism", "bytes": 1940 - } + }, + "design_role": "visual style guide" }, { "id": "visual_style.ink-notes", - "source_path": "ppt-master/skills/ppt-master/references/visual-styles/ink-notes.md", + "source_path": "svglide-design-patterns/visual_style/visual_style.ink-notes", "kind": "visual_style", - "ppt_master_role": "visual style guide", "svglide_target": "style-presets.json/quality-oracle", "protocol_compatibility": "documentation_reference", "conversion_strategy": "Derive style tags, visual anti-patterns, and quality-oracle signals.", @@ -10645,13 +10644,13 @@ "metadata": { "summary": "Visual style: ink-notes", "bytes": 1859 - } + }, + "design_role": "visual style guide" }, { "id": "visual_style.ink-wash", - "source_path": "ppt-master/skills/ppt-master/references/visual-styles/ink-wash.md", + "source_path": "svglide-design-patterns/visual_style/visual_style.ink-wash", "kind": "visual_style", - "ppt_master_role": "visual style guide", "svglide_target": "style-presets.json/quality-oracle", "protocol_compatibility": "documentation_reference", "conversion_strategy": "Derive style tags, visual anti-patterns, and quality-oracle signals.", @@ -10682,13 +10681,13 @@ "metadata": { "summary": "Visual style: ink-wash", "bytes": 1828 - } + }, + "design_role": "visual style guide" }, { "id": "visual_style.memphis", - "source_path": "ppt-master/skills/ppt-master/references/visual-styles/memphis.md", + "source_path": "svglide-design-patterns/visual_style/visual_style.memphis", "kind": "visual_style", - "ppt_master_role": "visual style guide", "svglide_target": "style-presets.json/quality-oracle", "protocol_compatibility": "documentation_reference", "conversion_strategy": "Derive style tags, visual anti-patterns, and quality-oracle signals.", @@ -10718,13 +10717,13 @@ "metadata": { "summary": "Visual style: memphis", "bytes": 1983 - } + }, + "design_role": "visual style guide" }, { "id": "visual_style.paper-cut", - "source_path": "ppt-master/skills/ppt-master/references/visual-styles/paper-cut.md", + "source_path": "svglide-design-patterns/visual_style/visual_style.paper-cut", "kind": "visual_style", - "ppt_master_role": "visual style guide", "svglide_target": "style-presets.json/quality-oracle", "protocol_compatibility": "documentation_reference", "conversion_strategy": "Derive style tags, visual anti-patterns, and quality-oracle signals.", @@ -10755,13 +10754,13 @@ "metadata": { "summary": "Visual style: paper-cut", "bytes": 1784 - } + }, + "design_role": "visual style guide" }, { "id": "visual_style.photo-editorial", - "source_path": "ppt-master/skills/ppt-master/references/visual-styles/photo-editorial.md", + "source_path": "svglide-design-patterns/visual_style/visual_style.photo-editorial", "kind": "visual_style", - "ppt_master_role": "visual style guide", "svglide_target": "style-presets.json/quality-oracle", "protocol_compatibility": "documentation_reference", "conversion_strategy": "Derive style tags, visual anti-patterns, and quality-oracle signals.", @@ -10792,13 +10791,13 @@ "metadata": { "summary": "Visual style: photo-editorial", "bytes": 1799 - } + }, + "design_role": "visual style guide" }, { "id": "visual_style.pixel-art", - "source_path": "ppt-master/skills/ppt-master/references/visual-styles/pixel-art.md", + "source_path": "svglide-design-patterns/visual_style/visual_style.pixel-art", "kind": "visual_style", - "ppt_master_role": "visual style guide", "svglide_target": "style-presets.json/quality-oracle", "protocol_compatibility": "documentation_reference", "conversion_strategy": "Derive style tags, visual anti-patterns, and quality-oracle signals.", @@ -10829,13 +10828,13 @@ "metadata": { "summary": "Visual style: pixel-art", "bytes": 1722 - } + }, + "design_role": "visual style guide" }, { "id": "visual_style.sketch-notes", - "source_path": "ppt-master/skills/ppt-master/references/visual-styles/sketch-notes.md", + "source_path": "svglide-design-patterns/visual_style/visual_style.sketch-notes", "kind": "visual_style", - "ppt_master_role": "visual style guide", "svglide_target": "style-presets.json/quality-oracle", "protocol_compatibility": "documentation_reference", "conversion_strategy": "Derive style tags, visual anti-patterns, and quality-oracle signals.", @@ -10866,13 +10865,13 @@ "metadata": { "summary": "Visual style: sketch-notes", "bytes": 1638 - } + }, + "design_role": "visual style guide" }, { "id": "visual_style.soft-rounded", - "source_path": "ppt-master/skills/ppt-master/references/visual-styles/soft-rounded.md", + "source_path": "svglide-design-patterns/visual_style/visual_style.soft-rounded", "kind": "visual_style", - "ppt_master_role": "visual style guide", "svglide_target": "style-presets.json/quality-oracle", "protocol_compatibility": "documentation_reference", "conversion_strategy": "Derive style tags, visual anti-patterns, and quality-oracle signals.", @@ -10903,13 +10902,13 @@ "metadata": { "summary": "Visual style: soft-rounded", "bytes": 1486 - } + }, + "design_role": "visual style guide" }, { "id": "visual_style.swiss-minimal", - "source_path": "ppt-master/skills/ppt-master/references/visual-styles/swiss-minimal.md", + "source_path": "svglide-design-patterns/visual_style/visual_style.swiss-minimal", "kind": "visual_style", - "ppt_master_role": "visual style guide", "svglide_target": "style-presets.json/quality-oracle", "protocol_compatibility": "documentation_reference", "conversion_strategy": "Derive style tags, visual anti-patterns, and quality-oracle signals.", @@ -10940,13 +10939,13 @@ "metadata": { "summary": "Visual style: swiss-minimal", "bytes": 1674 - } + }, + "design_role": "visual style guide" }, { "id": "visual_style.vintage-poster", - "source_path": "ppt-master/skills/ppt-master/references/visual-styles/vintage-poster.md", + "source_path": "svglide-design-patterns/visual_style/visual_style.vintage-poster", "kind": "visual_style", - "ppt_master_role": "visual style guide", "svglide_target": "style-presets.json/quality-oracle", "protocol_compatibility": "documentation_reference", "conversion_strategy": "Derive style tags, visual anti-patterns, and quality-oracle signals.", @@ -10977,13 +10976,13 @@ "metadata": { "summary": "Visual style: vintage-poster", "bytes": 1808 - } + }, + "design_role": "visual style guide" }, { "id": "visual_style.zine", - "source_path": "ppt-master/skills/ppt-master/references/visual-styles/zine.md", + "source_path": "svglide-design-patterns/visual_style/visual_style.zine", "kind": "visual_style", - "ppt_master_role": "visual style guide", "svglide_target": "style-presets.json/quality-oracle", "protocol_compatibility": "documentation_reference", "conversion_strategy": "Derive style tags, visual anti-patterns, and quality-oracle signals.", @@ -11013,16 +11012,16 @@ "metadata": { "summary": "Visual style: zine", "bytes": 2057 - } + }, + "design_role": "visual style guide" }, { "id": "workflow.executor-base", - "source_path": "ppt-master/skills/ppt-master/references/executor-base.md", + "source_path": "svglide-design-patterns/workflow_reference/workflow.executor-base", "kind": "workflow_reference", - "ppt_master_role": "pipeline and quality-gate reference", "svglide_target": "runner receipts, quality gate, and asset planning docs", "protocol_compatibility": "documentation_reference", - "conversion_strategy": "Derive process checks, receipt fields, and review heuristics without importing ppt-master as runtime dependency.", + "conversion_strategy": "Derive process checks, receipt fields, and review heuristics without importing svglide-design-patterns as runtime dependency.", "activation_status": "candidate", "selection_tags": [ "base", @@ -11051,16 +11050,16 @@ "metadata": { "summary": "Executor Common Guidelines", "bytes": 34583 - } + }, + "design_role": "pipeline and quality-gate reference" }, { "id": "workflow.image-layout-patterns", - "source_path": "ppt-master/skills/ppt-master/references/image-layout-patterns.md", + "source_path": "svglide-design-patterns/workflow_reference/workflow.image-layout-patterns", "kind": "workflow_reference", - "ppt_master_role": "pipeline and quality-gate reference", "svglide_target": "runner receipts, quality gate, and asset planning docs", "protocol_compatibility": "documentation_reference", - "conversion_strategy": "Derive process checks, receipt fields, and review heuristics without importing ppt-master as runtime dependency.", + "conversion_strategy": "Derive process checks, receipt fields, and review heuristics without importing svglide-design-patterns as runtime dependency.", "activation_status": "candidate", "selection_tags": [ "image", @@ -11089,16 +11088,16 @@ "metadata": { "summary": "Image-Text Layout Patterns", "bytes": 21776 - } + }, + "design_role": "pipeline and quality-gate reference" }, { "id": "workflow.image-layout-spec", - "source_path": "ppt-master/skills/ppt-master/references/image-layout-spec.md", + "source_path": "svglide-design-patterns/workflow_reference/workflow.image-layout-spec", "kind": "workflow_reference", - "ppt_master_role": "pipeline and quality-gate reference", "svglide_target": "runner receipts, quality gate, and asset planning docs", "protocol_compatibility": "documentation_reference", - "conversion_strategy": "Derive process checks, receipt fields, and review heuristics without importing ppt-master as runtime dependency.", + "conversion_strategy": "Derive process checks, receipt fields, and review heuristics without importing svglide-design-patterns as runtime dependency.", "activation_status": "candidate", "selection_tags": [ "common", @@ -11134,16 +11133,16 @@ "metadata": { "summary": "> See shared-standards.md for common technical constraints.", "bytes": 8963 - } + }, + "design_role": "pipeline and quality-gate reference" }, { "id": "workflow.shared-standards", - "source_path": "ppt-master/skills/ppt-master/references/shared-standards.md", + "source_path": "svglide-design-patterns/workflow_reference/workflow.shared-standards", "kind": "workflow_reference", - "ppt_master_role": "pipeline and quality-gate reference", "svglide_target": "runner receipts, quality gate, and asset planning docs", "protocol_compatibility": "documentation_reference", - "conversion_strategy": "Derive process checks, receipt fields, and review heuristics without importing ppt-master as runtime dependency.", + "conversion_strategy": "Derive process checks, receipt fields, and review heuristics without importing svglide-design-patterns as runtime dependency.", "activation_status": "candidate", "selection_tags": [ "shared", @@ -11171,16 +11170,16 @@ "metadata": { "summary": "Shared Technical Standards", "bytes": 39041 - } + }, + "design_role": "pipeline and quality-gate reference" }, { "id": "workflow.strategist", - "source_path": "ppt-master/skills/ppt-master/references/strategist.md", + "source_path": "svglide-design-patterns/workflow_reference/workflow.strategist", "kind": "workflow_reference", - "ppt_master_role": "pipeline and quality-gate reference", "svglide_target": "runner receipts, quality gate, and asset planning docs", "protocol_compatibility": "documentation_reference", - "conversion_strategy": "Derive process checks, receipt fields, and review heuristics without importing ppt-master as runtime dependency.", + "conversion_strategy": "Derive process checks, receipt fields, and review heuristics without importing svglide-design-patterns as runtime dependency.", "activation_status": "candidate", "selection_tags": [ "role", @@ -11207,16 +11206,16 @@ "metadata": { "summary": "Role: Strategist", "bytes": 62729 - } + }, + "design_role": "pipeline and quality-gate reference" }, { "id": "workflow.visual-review", - "source_path": "ppt-master/skills/ppt-master/references/visual-review.md", + "source_path": "svglide-design-patterns/workflow_reference/workflow.visual-review", "kind": "workflow_reference", - "ppt_master_role": "pipeline and quality-gate reference", "svglide_target": "runner receipts, quality gate, and asset planning docs", "protocol_compatibility": "documentation_reference", - "conversion_strategy": "Derive process checks, receipt fields, and review heuristics without importing ppt-master as runtime dependency.", + "conversion_strategy": "Derive process checks, receipt fields, and review heuristics without importing svglide-design-patterns as runtime dependency.", "activation_status": "candidate", "selection_tags": [ "review", @@ -11244,7 +11243,8 @@ "metadata": { "summary": "Visual Review Rubric", "bytes": 15423 - } + }, + "design_role": "pipeline and quality-gate reference" } ] } diff --git a/skills/lark-slides/references/svglide-project-pipeline.md b/skills/lark-slides/references/svglide-project-pipeline.md index d4f82fee0..171b99cce 100644 --- a/skills/lark-slides/references/svglide-project-pipeline.md +++ b/skills/lark-slides/references/svglide-project-pipeline.md @@ -147,16 +147,16 @@ Disallowed P0 behavior: - mutate authoring `pages/*.svg` - replace images or tokens outside the existing `slides +create-svg` transport path -## PPT Master Asset Receipt +## SVGlide Design Pattern Receipt -`ppt-master` references are allowed only as structure, rhythm, chart geometry, +SVGlide design pattern references are allowed only as structure, rhythm, chart geometry, style, and review inspiration. They are not runtime dependencies and raw SVG/PPTX assets must not be copied into SVGlide output. For quality lanes, `selected_assets` means "actually used by the generated pages", not "interesting candidates found during research". Candidate assets stay in research notes. Used assets must be proven by -`receipts/ppt-master-asset-usage.json` with page-level trace entries that point +`receipts/design-pattern-usage.json` with page-level trace entries that point to the SVG evidence. The quality gate must fail when `selected_assets` includes an asset that is not present in the usage receipt. @@ -195,9 +195,12 @@ without `preview/preview.html`, but it must not proceed to guarded live creation or production/golden delivery until preview lint and quality gate have passed. `quality_gate` reads the latest preflight receipt, preview lint receipt, raster -report, allowlist, asset selection, and component report evidence. During P0 -migration, authoring/debug dry-run may use an unexpired legacy component waiver. -Production, golden, and live lanes must not use that waiver. +report, allowlist, asset selection, visual design contract, and component report +evidence. If a slide declares `visual_design_contract.required_visual_evidence`, +the same page in `receipts/emitted_components.json` must prove those evidence +tokens through component `effects`, `primitives`, `renderer_id`, or component id. +During P0 migration, authoring/debug dry-run may use an unexpired legacy +component waiver. Production, golden, and live lanes must not use that waiver. ## PPE Proof @@ -265,5 +268,5 @@ asset, or protocol values. - Do not introduce a fourth structure catalog. - Do not modify Open Design for this rollout. - Do not rewrite Go CLI code in P0. -- Do not copy `ppt-master` runtime or assets. +- Do not copy external runtime or raw assets. - Do not treat local preview as a replacement for live readback. diff --git a/skills/lark-slides/references/validation-checklist.md b/skills/lark-slides/references/validation-checklist.md index 5e86f8e5e..6c6df3f61 100644 --- a/skills/lark-slides/references/validation-checklist.md +++ b/skills/lark-slides/references/validation-checklist.md @@ -65,7 +65,9 @@ python3 skills/lark-slides/scripts/svg_preflight.py \ - public `visual_recipe` 必须存在于 `references/svg-recipes.json`;研究文档里的 dotted recipe 名称不能直接写入 `slide_plan.json`。 - `style_selection_reason` 必须说明为什么这个 preset 适合当前 deck。 - `style_system` 必须包含 palette、typography、background strategy 和 motif。 +- 多页 deck 必须声明 `page_rhythm` / `deck_rhythm`,用于检查节奏、密度变化和重复页风险;authoring profile 下缺失会给 warning,`validation_profile=golden` 或 strict profile 下为 error。 - 每页必须包含 `seed_id`、`layout_skeleton_id`、`layout_boxes`、`content_budget` 或 `text_capacity`、`text_budget_by_role`、`one_idea` 或 `key_message`、`reserved_bands.footer`、`footer_safe_zone`、`vertical_text_policy`、`visual_recipe`、`visual_signature`、`svg_effects`、`required_primitives`、`svg_primitives`、`xml_like_risk`、`content_density_contract`、`risk_flags`、`source_policy`。 +- Strategist contract 字段必须可检查:每页声明 `page_type`、可定位到 layout box / SVG element / component / bbox 的 `main_visual_anchor`;`reference_asset` 不能只写描述性文字,必须是 `{source, asset_id/id}` 或带 source/license/path 的资产元数据。 - declared `svg_effects` 和 `required_primitives` 必须能在对应 SVG source 中命中。 - 可见 slide 文本不得泄漏 preset 名称、source token、prompt、tool name 或本地文件路径。 @@ -96,6 +98,11 @@ python3 skills/lark-slides/scripts/svg_preflight.py \ | `plan_missing_visual_signature` | 页面没有声明 SVG 视觉记忆点 | 写清这页相对普通 PPT/XML 模板的独特视觉结构 | | `plan_missing_svg_effects` | 页面没有声明 SVG 表达能力 | 声明真实会绘制的 `path`、`connector_flow`、`gradient`、`texture`、`chart_geometry` 等 | | `plan_svg_effect_not_found` | plan 声明的 effect 没在 SVG source 中出现 | 修改 SVG source,或删除不真实的 effect 声明 | +| `plan_missing_page_rhythm` | 多页 deck 没有声明节奏合同 | 添加 deck-level `page_rhythm`,说明封面/章节/内容/总结节奏和重复页约束 | +| `plan_missing_page_type` | 页面缺少可检查页型 | 添加 `page_type`,并让 renderer/layout/visual_recipe 与页型一致 | +| `plan_missing_main_visual_anchor` | 主视觉锚点缺失或只是自然语言 | 指向 layout box role、`#svg-element-id`、component_id,或写明确 bbox | +| `plan_main_visual_anchor_not_met` | SVG source 没有在主视觉锚点区域生成可见几何 | 调整 source SVG,把主视觉放回声明的锚点区域,或先更新 plan anchor | +| `plan_reference_asset_unstructured` | `reference_asset` 是纯文字或缺少 source/id/path | 改成结构化 source metadata;没有参考资产时显式写 no_asset | | `plan_style_preset_visible_leak` | 可见文本泄漏 preset 名/source token | 仅在 plan metadata 中保留 preset 信息,画面只写用户主题内容 | ## SVGlide Aesthetic Preview Review @@ -116,14 +123,22 @@ SVGlide 项目必须同时检查计划、执行 manifest、SVG source 和 receip preflight 只检查已传入的子集。 - `prepare` receipt 在 plan、manifest 或 source SVG 变化后必须失效;后续 `preflight`、`preview_lint`、`quality_gate`、`dry_run` 不得复用旧 prepare。 -- 声明 `chart_type` 或 ppt-master chart 参考时,SVG 必须命中对应几何合同: +- 声明 `chart_type` 或 SVGlide design pattern chart 参考时,SVG 必须命中对应几何合同: `bubble_chart` 至少有多枚圆形节点,`donut_chart` 至少有环形/圆形结构和中心 文本,`bar_chart` 至少有可识别轴/条形/数值区域。不能把图表页退化成普通 卡片、closing 或 bullet list。 -- `ppt_master_asset_selection.selected_assets` 只放真正启用并落地的参考资产; +- `design_pattern_selection.selected_assets` 只放真正启用并落地的参考资产; `enabled:false` 可作为候选保留,但不进入 quality gate。启用资产必须由 - `receipts/ppt-master-asset-usage.json` 的 page-level trace 证明。 -- `ppt-master` 参考只允许变成 SVGlide-safe 的页型、图表几何、节奏、色彩纪律 + `receipts/design-pattern-usage.json` 的 page-level trace 证明。 +- `visual_design_contract.required_visual_evidence` 必须由 + `receipts/emitted_components.json` 的 page-level component `effects`、 + `primitives`、`renderer_id` 或 component id 证明。缺少 evidence 时 + `quality_gate` 失败;这类问题不能只改 plan 字段,必须修 renderer 或 SVG。 +- `quality_gate` 会把 preflight 中的 Strategist contract issue codes 写入 + `strategist_contract` 摘要,并把 visual design contract 证明写入 + `visual_design_contract` 摘要;`validation_profile=golden` 要求零 warning、结构化 + component report,以及正式 schema 的 design-pattern usage receipt。 +- SVGlide design pattern 参考只允许变成 SVGlide-safe 的页型、图表几何、节奏、色彩纪律 和审查规则;不要复制 raw SVG、图片或 PPTX/DrawingML 导出实现。 通过标准: diff --git a/skills/lark-slides/scripts/ppt_master_asset_inventory.py b/skills/lark-slides/scripts/ppt_master_asset_inventory.py deleted file mode 100644 index 9679dcc01..000000000 --- a/skills/lark-slides/scripts/ppt_master_asset_inventory.py +++ /dev/null @@ -1,882 +0,0 @@ -#!/usr/bin/env python3 -# Copyright (c) 2026 Lark Technologies Pte. Ltd. -# SPDX-License-Identifier: MIT - -from __future__ import annotations - -import argparse -import hashlib -import json -import re -from collections import Counter -from pathlib import Path -from typing import Iterable - - -SCHEMA_VERSION = "svglide-ppt-master-asset-map/v1" -REQUIRED_FIELDS = { - "id", - "source_path", - "kind", - "ppt_master_role", - "svglide_target", - "protocol_compatibility", - "conversion_strategy", - "activation_status", - "selection_tags", - "copy_policy", - "license_status", - "granularity", - "unsupported_features", - "normalized_primitives", - "non_migratable_reason", - "risk_flags", - "golden_example_required", -} -REQUIRED_KINDS = { - "brand_preset", - "layout_template", - "deck_template", - "chart_template", - "icon_library", - "visual_style", - "image_palette", - "image_rendering", - "image_type_template", - "narrative_mode", - "example_project", - "workflow_reference", -} -RAW_SVG_NORMALIZED_PRIMITIVES = [ - "960x540_canvas", - "slide_role_shape", - "foreignObject_text", - "primitive_shapes", -] -IMAGE_SUFFIXES = {".png", ".jpg", ".jpeg", ".webp", ".gif"} -TOKEN_RE = re.compile(r"[a-z0-9]+|[\u4e00-\u9fff]+", re.IGNORECASE) - - -class InventoryError(ValueError): - """Raised when the local ppt-master tree cannot produce a valid inventory.""" - - -def script_path() -> Path: - return Path(__file__).resolve() - - -def cli_worktree_root() -> Path: - return script_path().parents[3] - - -def default_source_root() -> Path: - return cli_worktree_root().parents[1] / "ppt-master" - - -def read_json(path: Path) -> dict: - return json.loads(path.read_text(encoding="utf-8")) - - -def file_digest(path: Path) -> str: - digest = hashlib.sha256() - with path.open("rb") as handle: - for chunk in iter(lambda: handle.read(1024 * 1024), b""): - digest.update(chunk) - return digest.hexdigest()[:16] - - -def digest_paths(source_root: Path, paths: Iterable[Path]) -> str: - digest = hashlib.sha256() - for path in sorted({p for p in paths if p.exists() and p.is_file()}): - rel = path.relative_to(source_root).as_posix() - digest.update(rel.encode("utf-8")) - digest.update(b"\0") - digest.update(str(path.stat().st_size).encode("ascii")) - digest.update(b"\0") - digest.update(file_digest(path).encode("ascii")) - digest.update(b"\0") - return digest.hexdigest()[:16] - - -def rel_source(path: Path, source_root: Path) -> str: - path = path.resolve() - source_root = source_root.resolve() - if path == source_root: - return source_root.name - return f"{source_root.name}/{path.relative_to(source_root).as_posix()}" - - -def slug(value: str) -> str: - normalized = re.sub(r"[^0-9A-Za-z_\-\u4e00-\u9fff]+", "_", value.strip()) - normalized = re.sub(r"_+", "_", normalized).strip("_").lower() - return normalized or "item" - - -def tokenize(*values: object) -> list[str]: - tokens: set[str] = set() - for value in values: - if value is None: - continue - if isinstance(value, (list, tuple, set)): - tokens.update(tokenize(*value)) - continue - tokens.update(match.group(0).lower() for match in TOKEN_RE.finditer(str(value))) - return sorted(tokens) - - -def limited_tags(*values: object, extra: Iterable[str] = ()) -> list[str]: - tags = tokenize(*values) - for tag in extra: - tags.extend(tokenize(tag)) - return sorted(set(tags))[:32] - - -def files_under(path: Path) -> list[Path]: - if not path.exists(): - return [] - return sorted(p for p in path.rglob("*") if p.is_file()) - - -def direct_md_files(path: Path) -> list[Path]: - return sorted(p for p in path.glob("*.md") if p.name != "_index.md") - - -def direct_dirs(path: Path) -> list[Path]: - return sorted(p for p in path.iterdir() if p.is_dir()) if path.exists() else [] - - -def svg_files(path: Path) -> list[Path]: - if path.is_file() and path.suffix.lower() == ".svg": - return [path] - return sorted(p for p in path.rglob("*.svg") if p.is_file()) if path.exists() else [] - - -def image_files(path: Path) -> list[Path]: - if not path.exists(): - return [] - return sorted(p for p in path.rglob("*") if p.is_file() and p.suffix.lower() in IMAGE_SUFFIXES) - - -def count_base64_images(paths: Iterable[Path]) -> int: - count = 0 - for path in paths: - try: - count += path.read_text(encoding="utf-8", errors="ignore").count("data:image") - except OSError: - continue - return count - - -def detect_svg_unsupported(paths: Iterable[Path]) -> list[str]: - features: set[str] = set() - for path in paths: - try: - text = path.read_text(encoding="utf-8", errors="ignore") - except OSError: - continue - lowered = text.lower() - if " dict: - source_files = files_under(source_path) if source_path.is_dir() else [source_path] - resource = { - "id": resource_id, - "source_path": rel_source(source_path, source_root), - "kind": kind, - "ppt_master_role": ppt_master_role, - "svglide_target": svglide_target, - "protocol_compatibility": protocol_compatibility, - "conversion_strategy": conversion_strategy, - "activation_status": activation_status, - "selection_tags": sorted(set(selection_tags)), - "copy_policy": copy_policy, - "license_status": license_status, - "granularity": granularity, - "unsupported_features": sorted(set(unsupported_features)), - "normalized_primitives": sorted(set(normalized_primitives)), - "non_migratable_reason": non_migratable_reason, - "risk_flags": sorted(set(risk_flags)), - "golden_example_required": golden_example_required, - "normalized_fixture": "", - "source_digest": digest_paths(source_root, source_files), - } - if summary: - resource["summary"] = summary - if metadata: - resource["metadata"] = metadata - return resource - - -def build_brand_resources(source_root: Path) -> list[dict]: - index_path = source_root / "skills/ppt-master/templates/brands/brands_index.json" - brands = read_json(index_path) - resources = [] - for name, data in sorted(brands.items()): - root = index_path.parent / name - summary = data.get("summary", "") - svgs = svg_files(root) - resources.append( - make_resource( - resource_id=f"brand.{slug(name)}", - source_path=root, - source_root=source_root, - kind="brand_preset", - ppt_master_role="brand identity preset", - svglide_target="style-presets.json/brand-visual-tokens", - protocol_compatibility="needs_normalization", - conversion_strategy="Extract color, typography, spacing, and brand-tone contracts; do not copy brand marks into runtime output.", - selection_tags=limited_tags("brand", name, summary), - granularity="template_directory", - unsupported_features=sorted(set(detect_svg_unsupported(svgs) + ["brand_logo_assets"])), - normalized_primitives=["color_tokens", "typography_rules", "logo_exclusion_zone"], - non_migratable_reason="Brand marks and logos require explicit authorization before production use.", - risk_flags=["brand_trademark", "third_party_logo", "requires_license_review"], - metadata={ - "summary": summary, - "primary_color": data.get("primary_color", ""), - "svg_file_count": len(svgs), - "image_file_count": len(image_files(root)), - "file_count": len(files_under(root)), - }, - summary=summary, - ) - ) - return resources - - -def build_layout_resources(source_root: Path) -> list[dict]: - index_path = source_root / "skills/ppt-master/templates/layouts/layouts_index.json" - layouts = read_json(index_path) - resources = [] - for name, data in sorted(layouts.items()): - root = index_path.parent / name - summary = data.get("summary", "") - svgs = svg_files(root) - resources.append( - make_resource( - resource_id=f"layout.{slug(name)}", - source_path=root, - source_root=source_root, - kind="layout_template", - ppt_master_role="page skeleton and rhythm template", - svglide_target="svg-seeds.json/layout-skeletons", - protocol_compatibility="needs_normalization", - conversion_strategy="Extract page slots, density, rhythm, and spacing contracts; regenerate as SVGlide seeds.", - selection_tags=limited_tags("layout", name, summary, data.get("page_types", [])), - granularity="template_directory", - unsupported_features=detect_svg_unsupported(svgs), - normalized_primitives=RAW_SVG_NORMALIZED_PRIMITIVES + ["layout_boxes", "content_budget"], - non_migratable_reason="Raw 1280x720 SVG templates lack SVGlide slide roles and must be re-authored before activation.", - risk_flags=["pptx_export_bias", "requires_svglide_normalization"], - metadata={ - "summary": summary, - "canvas_format": data.get("canvas_format", ""), - "page_count": data.get("page_count", len(svgs)), - "page_types": data.get("page_types", []), - "svg_file_count": len(svgs), - }, - summary=summary, - ) - ) - return resources - - -def build_deck_resources(source_root: Path) -> list[dict]: - index_path = source_root / "skills/ppt-master/templates/decks/decks_index.json" - decks = read_json(index_path) - resources = [] - for name, data in sorted(decks.items()): - root = index_path.parent / name - summary = data.get("summary", "") - svgs = svg_files(root) - resources.append( - make_resource( - resource_id=f"deck.{slug(name)}", - source_path=root, - source_root=source_root, - kind="deck_template", - ppt_master_role="finished deck style and page-sequence template", - svglide_target="deck-rhythm-pack/style-pack reference", - protocol_compatibility="needs_normalization", - conversion_strategy="Extract page sequence, rhythm, and style rules; do not copy finished deck SVG or logos.", - selection_tags=limited_tags("deck", name, summary), - granularity="template_directory", - unsupported_features=sorted(set(detect_svg_unsupported(svgs) + ["finished_deck_template"])), - normalized_primitives=RAW_SVG_NORMALIZED_PRIMITIVES + ["deck_rhythm", "style_tokens"], - non_migratable_reason="Finished deck templates contain brand assets and raw SVG pages that require re-authoring.", - risk_flags=["brand_trademark", "third_party_logo", "requires_svglide_normalization"], - metadata={ - "summary": summary, - "canvas_format": data.get("canvas_format", ""), - "page_count": data.get("page_count", len(svgs)), - "primary_color": data.get("primary_color", ""), - "svg_file_count": len(svgs), - "image_file_count": len(image_files(root)), - }, - summary=summary, - ) - ) - return resources - - -def build_chart_resources(source_root: Path) -> list[dict]: - index_path = source_root / "skills/ppt-master/templates/charts/charts_index.json" - index = read_json(index_path) - resources = [] - for name, data in sorted(index.get("charts", {}).items()): - path = index_path.parent / f"{name}.svg" - summary = data.get("summary", "") - resources.append( - make_resource( - resource_id=f"chart.{slug(name)}", - source_path=path, - source_root=source_root, - kind="chart_template", - ppt_master_role="visualization template", - svglide_target="svg-recipes.json/component-catalog", - protocol_compatibility="needs_normalization", - conversion_strategy="Extract geometry slots and chart role contract; do not copy final SVG as a page template.", - selection_tags=limited_tags("chart", name, summary), - granularity="file", - unsupported_features=detect_svg_unsupported([path]), - normalized_primitives=RAW_SVG_NORMALIZED_PRIMITIVES + ["chart_data_slots", "geometry_contract"], - non_migratable_reason="Raw chart SVG must be normalized into SVGlide primitives and checked before activation.", - risk_flags=["pptx_export_bias", "requires_svglide_normalization"], - metadata={"summary": summary, "view_box": index.get("meta", {}).get("defaultViewBox", "")}, - summary=summary, - ) - ) - return resources - - -def icon_style(library: str) -> str: - styles = { - "chunk-filled": "filled sharp-corner geometric icons", - "tabler-filled": "filled rounded icons", - "tabler-outline": "stroke outline icons", - "phosphor-duotone": "duotone soft-depth icons", - "simple-icons": "brand-logo silhouettes", - } - return styles.get(library, "SVG icon library") - - -def build_icon_resources(source_root: Path) -> list[dict]: - icons_root = source_root / "skills/ppt-master/templates/icons" - resources = [] - for root in direct_dirs(icons_root): - icon_paths = sorted(root.glob("*.svg")) - samples = [path.stem for path in icon_paths[:20]] - is_brand = root.name == "simple-icons" - resources.append( - make_resource( - resource_id=f"icon_library.{slug(root.name)}", - source_path=root, - source_root=source_root, - kind="icon_library", - ppt_master_role="icon library summary and searchable index", - svglide_target="icon-registry/style-binding", - protocol_compatibility="needs_normalization", - conversion_strategy="Expose a library-level summary and searchable index; select one style library per deck and never inject every SVG into the prompt.", - selection_tags=limited_tags("icon", root.name, icon_style(root.name), samples), - granularity="library_summary", - unsupported_features=["raw_svg_icon_files"] + (["brand_logo_assets"] if is_brand else []), - normalized_primitives=["data-icon_placeholder", "searchable_icon_index", "single_library_binding"], - non_migratable_reason="Individual icon SVGs are not copied into the main prompt; brand icons require license review.", - risk_flags=(["brand_trademark", "third_party_logo"] if is_brand else ["large_library_prompt_risk"]), - metadata={ - "style": icon_style(root.name), - "icon_count": len(icon_paths), - "sample_icons": samples, - "index_policy": "library_summary_only", - }, - summary=icon_style(root.name), - ) - ) - return resources - - -def build_reference_doc_resources( - source_root: Path, - *, - rel_dir: str, - kind: str, - id_prefix: str, - ppt_master_role: str, - svglide_target: str, - conversion_strategy: str, - normalized_primitives: list[str], - extra_tags: Iterable[str] = (), -) -> list[dict]: - root = source_root / rel_dir - resources = [] - for path in direct_md_files(root): - name = path.stem - summary = first_heading_or_line(path) - resources.append( - make_resource( - resource_id=f"{id_prefix}.{slug(name)}", - source_path=path, - source_root=source_root, - kind=kind, - ppt_master_role=ppt_master_role, - svglide_target=svglide_target, - protocol_compatibility="documentation_reference", - conversion_strategy=conversion_strategy, - selection_tags=limited_tags(id_prefix, name, summary, extra_tags), - copy_policy="derive_rules_only", - license_status="reference_only", - granularity="file", - unsupported_features=[], - normalized_primitives=normalized_primitives, - non_migratable_reason="Reference prose is a source for derived rules, not a runtime asset.", - risk_flags=["style_drift_if_copied_verbatim"], - metadata={"summary": summary, "bytes": path.stat().st_size}, - summary=summary, - ) - ) - return resources - - -def first_heading_or_line(path: Path) -> str: - try: - for line in path.read_text(encoding="utf-8", errors="ignore").splitlines(): - stripped = line.strip() - if stripped.startswith("#"): - return stripped.lstrip("#").strip() - if stripped: - return stripped[:160] - except OSError: - return "" - return "" - - -def build_mode_resources(source_root: Path) -> list[dict]: - return build_reference_doc_resources( - source_root, - rel_dir="skills/ppt-master/references/modes", - kind="narrative_mode", - id_prefix="mode", - ppt_master_role="deck-level narrative mode", - svglide_target="deck intent and page rhythm selector", - conversion_strategy="Derive narrative mode tags and page rhythm signals.", - normalized_primitives=["deck_intent", "page_rhythm", "story_arc"], - extra_tags=["narrative", "mode"], - ) - - -def build_workflow_reference_resources(source_root: Path) -> list[dict]: - rel_paths = [ - "skills/ppt-master/references/strategist.md", - "skills/ppt-master/references/executor-base.md", - "skills/ppt-master/references/visual-review.md", - "skills/ppt-master/references/shared-standards.md", - "skills/ppt-master/references/image-layout-patterns.md", - "skills/ppt-master/references/image-layout-spec.md", - ] - resources = [] - for rel_path in rel_paths: - path = source_root / rel_path - summary = first_heading_or_line(path) - resources.append( - make_resource( - resource_id=f"workflow.{slug(path.stem)}", - source_path=path, - source_root=source_root, - kind="workflow_reference", - ppt_master_role="pipeline and quality-gate reference", - svglide_target="runner receipts, quality gate, and asset planning docs", - protocol_compatibility="documentation_reference", - conversion_strategy="Derive process checks, receipt fields, and review heuristics without importing ppt-master as runtime dependency.", - selection_tags=limited_tags("workflow", path.stem, summary), - copy_policy="derive_process_contract_only", - license_status="reference_only", - granularity="file", - unsupported_features=[], - normalized_primitives=["receipt_contract", "quality_gate_signal", "stage_boundary"], - non_migratable_reason="Workflow references shape implementation rules and are not prompt-time visual assets.", - risk_flags=["second_protocol_risk"], - golden_example_required=False, - metadata={"summary": summary, "bytes": path.stat().st_size if path.exists() else 0}, - summary=summary, - ) - ) - return resources - - -def build_image_reference_collections(source_root: Path) -> list[dict]: - root = source_root / "skills/ppt-master/references/ai-image-comparison" - collections = [ - ("type", "image_type_visual_references"), - ("rendering", "image_rendering_visual_references"), - ("palette", "image_palette_visual_references"), - ] - resources = [] - for dirname, role in collections: - path = root / dirname - if not path.exists(): - continue - pngs = image_files(path) - resources.append( - make_resource( - resource_id=f"image_reference_collection.{slug(dirname)}", - source_path=path, - source_root=source_root, - kind="image_reference_collection", - ppt_master_role=role, - svglide_target="asset_strategy/reference-oracle", - protocol_compatibility="reference_only", - conversion_strategy="Summarize bitmap reference coverage; do not copy images into CLI runtime.", - selection_tags=limited_tags("image reference", dirname, [p.stem for p in pngs[:20]]), - copy_policy="derive_contract_only", - license_status="reference_only", - granularity="collection_summary", - unsupported_features=["bitmap_reference_images"], - normalized_primitives=["asset_strategy_tags", "visual_oracle_labels"], - non_migratable_reason="Reference PNGs require provenance and authorization before production use.", - risk_flags=["third_party_image", "runtime_dependency_risk"], - golden_example_required=False, - metadata={"image_count": len(pngs), "sample_images": [p.name for p in pngs[:12]]}, - summary=f"{dirname} bitmap reference collection", - ) - ) - return resources - - -def build_example_resources(source_root: Path) -> list[dict]: - examples_root = source_root / "examples" - resources = [] - for project in direct_dirs(examples_root): - final_svgs = svg_files(project / "svg_final") - output_svgs = svg_files(project / "svg_output") - pages = final_svgs or output_svgs - media = image_files(project / "images") - base64_count = count_base64_images(final_svgs + output_svgs) - notes_count = len(direct_md_files(project / "notes")) if (project / "notes").exists() else 0 - all_project_files = files_under(project) - unsupported = ["finished_deck_asset"] - if media: - unsupported.append("third_party_images") - if base64_count: - unsupported.append("embedded_base64_image") - if list((project / "exports").glob("*.pptx")): - unsupported.append("pptx_export") - summary = first_heading_or_line(project / "README.md") or project.name - resources.append( - make_resource( - resource_id=f"example.{slug(project.name)}", - source_path=project, - source_root=source_root, - kind="example_project", - ppt_master_role="finished example deck and mining corpus", - svglide_target="golden/negative corpus and style-mining source", - protocol_compatibility="reference_only", - conversion_strategy="Mine page rhythm, style signals, and failure cases; do not copy finished pages, images, base64, or PPTX exports.", - selection_tags=limited_tags("example", project.name, summary, [p.stem for p in pages[:8]]), - copy_policy="derive_contract_only", - license_status="reference_only", - granularity="project_page_media_summary", - unsupported_features=unsupported, - normalized_primitives=["page_rhythm", "layout_family_labels", "style_signals", "negative_corpus_signals"], - non_migratable_reason="Example projects are finished artifacts with generated images, possible third-party media, base64, and PPTX exports.", - risk_flags=["third_party_image", "base64_asset", "finished_deck_copy_risk"], - golden_example_required=False, - metadata={ - "summary": summary, - "page_count": len(pages), - "svg_final_count": len(final_svgs), - "svg_output_count": len(output_svgs), - "media_count": len(media), - "base64_count": base64_count, - "notes_count": notes_count, - "pptx_export_count": len(list((project / "exports").glob("*.pptx"))) if (project / "exports").exists() else 0, - "page_samples": [p.name for p in pages[:10]], - "media_samples": [p.name for p in media[:10]], - "project_digest": digest_paths(source_root, all_project_files), - }, - summary=summary, - ) - ) - return resources - - -def build_asset_map(source_root: Path) -> dict: - source_root = source_root.resolve() - if not source_root.exists(): - raise InventoryError(f"ppt-master source does not exist: {source_root}") - - resources: list[dict] = [] - resources.extend(build_brand_resources(source_root)) - resources.extend(build_layout_resources(source_root)) - resources.extend(build_deck_resources(source_root)) - resources.extend(build_chart_resources(source_root)) - resources.extend(build_icon_resources(source_root)) - resources.extend( - build_reference_doc_resources( - source_root, - rel_dir="skills/ppt-master/references/visual-styles", - kind="visual_style", - id_prefix="visual_style", - ppt_master_role="visual style guide", - svglide_target="style-presets.json/quality-oracle", - conversion_strategy="Derive style tags, visual anti-patterns, and quality-oracle signals.", - normalized_primitives=["style_tags", "forbidden_patterns", "quality_oracle"], - extra_tags=["style", "visual"], - ) - ) - resources.extend( - build_reference_doc_resources( - source_root, - rel_dir="skills/ppt-master/references/image-palettes", - kind="image_palette", - id_prefix="image_palette", - ppt_master_role="image palette reference", - svglide_target="asset_strategy/image-palette-binding", - conversion_strategy="Derive palette tags and image-planning hints; do not copy generated images.", - normalized_primitives=["palette_tags", "image_style_binding"], - extra_tags=["image", "palette"], - ) - ) - resources.extend( - build_reference_doc_resources( - source_root, - rel_dir="skills/ppt-master/references/image-renderings", - kind="image_rendering", - id_prefix="image_rendering", - ppt_master_role="image rendering preset", - svglide_target="asset_strategy/rendering-preset", - conversion_strategy="Derive rendering tags and fallback policy for image planning.", - normalized_primitives=["rendering_tags", "fallback_policy"], - extra_tags=["image", "rendering"], - ) - ) - resources.extend( - build_reference_doc_resources( - source_root, - rel_dir="skills/ppt-master/references/image-type-templates", - kind="image_type_template", - id_prefix="image_type", - ppt_master_role="image composition recipe", - svglide_target="asset_strategy/image-composition-recipe", - conversion_strategy="Derive image composition contracts and prompt planning tags.", - normalized_primitives=["composition_recipe", "asset_slots"], - extra_tags=["image", "composition"], - ) - ) - resources.extend(build_mode_resources(source_root)) - resources.extend(build_example_resources(source_root)) - resources.extend(build_workflow_reference_resources(source_root)) - resources.extend(build_image_reference_collections(source_root)) - resources.sort(key=lambda item: item["id"]) - - counts = Counter(resource["kind"] for resource in resources) - icon_svg_count = sum(resource.get("metadata", {}).get("icon_count", 0) for resource in resources) - example_page_count = sum(resource.get("metadata", {}).get("page_count", 0) for resource in resources) - example_media_count = sum(resource.get("metadata", {}).get("media_count", 0) for resource in resources) - all_source_files = files_under(source_root) - asset_map = { - "schema_version": SCHEMA_VERSION, - "generated_from": source_root.name, - "generation_policy": { - "runtime_dependency": "none", - "raw_svg_default": { - "protocol_compatibility": "needs_normalization", - "copy_policy": "derive_contract_only", - "license_status": "reference_only", - "activation_status": "candidate", - }, - "icon_policy": "library_summary_and_searchable_index_only", - "example_policy": "project_page_media_summary_only", - }, - "summary": { - "counts": { - **dict(sorted(counts.items())), - "total_resources": len(resources), - "icon_svg_files": icon_svg_count, - "example_pages": example_page_count, - "example_media_files": example_media_count, - }, - "digests": { - "all_source_files": digest_paths(source_root, all_source_files), - "brands": digest_paths(source_root, files_under(source_root / "skills/ppt-master/templates/brands")), - "layouts": digest_paths(source_root, files_under(source_root / "skills/ppt-master/templates/layouts")), - "decks": digest_paths(source_root, files_under(source_root / "skills/ppt-master/templates/decks")), - "charts": digest_paths(source_root, files_under(source_root / "skills/ppt-master/templates/charts")), - "icons": digest_paths(source_root, files_under(source_root / "skills/ppt-master/templates/icons")), - "references": digest_paths(source_root, files_under(source_root / "skills/ppt-master/references")), - "examples": digest_paths(source_root, files_under(source_root / "examples")), - }, - }, - "resources": resources, - } - validate_asset_map(asset_map) - return asset_map - - -def validate_asset_map(asset_map: dict) -> None: - if asset_map.get("schema_version") != SCHEMA_VERSION: - raise InventoryError("unexpected schema_version") - resources = asset_map.get("resources") - if not isinstance(resources, list) or not resources: - raise InventoryError("asset map has no resources") - - counts = Counter() - for index, resource in enumerate(resources): - missing = REQUIRED_FIELDS - set(resource) - if missing: - raise InventoryError(f"resource[{index}] missing fields: {sorted(missing)}") - counts[resource["kind"]] += 1 - if resource["activation_status"] == "rejected" and not resource["non_migratable_reason"]: - raise InventoryError(f"{resource['id']} is rejected without a reason") - if resource["activation_status"] == "active" and not resource["golden_example_required"]: - raise InventoryError(f"{resource['id']} active resources must require golden examples") - raw_like = ( - resource["source_path"].endswith(".svg") - or resource.get("metadata", {}).get("svg_file_count", 0) - or resource.get("metadata", {}).get("raw_svg_file_count", 0) - or "raw_svg_icon_files" in resource["unsupported_features"] - ) - if raw_like and resource["protocol_compatibility"] == "needs_normalization": - if resource["copy_policy"] != "derive_contract_only": - raise InventoryError(f"{resource['id']} raw SVG must use derive_contract_only") - if resource["license_status"] != "reference_only": - raise InventoryError(f"{resource['id']} raw SVG must be reference_only") - if resource["activation_status"] in {"validated", "active"}: - raise InventoryError(f"{resource['id']} raw SVG cannot start as validated/active") - for field in ("selection_tags", "unsupported_features", "normalized_primitives", "risk_flags"): - if not isinstance(resource[field], list): - raise InventoryError(f"{resource['id']} field {field} must be a list") - - missing_kinds = sorted(REQUIRED_KINDS - set(counts)) - if missing_kinds: - raise InventoryError(f"inventory missing required resource kinds: {missing_kinds}") - - -def render_markdown(asset_map: dict) -> str: - counts = asset_map["summary"]["counts"] - digests = asset_map["summary"]["digests"] - resources = asset_map["resources"] - lines = [ - "# ppt-master Asset Inventory", - "", - "This file is generated by `skills/lark-slides/scripts/ppt_master_asset_inventory.py`.", - "The JSON asset map is the source of truth; this Markdown is a human-readable summary.", - "", - "## Safety Defaults", - "", - "- Raw ppt-master SVG assets start as `needs_normalization`, `derive_contract_only`, `reference_only`, and `candidate`.", - "- Icon assets are summarized by library and searchable index metadata; individual icon SVG files are not injected into the main prompt.", - "- Examples are summarized by project/page/media counts and treated as reference corpus, not runtime assets.", - "- Production/golden selection must not use rejected, reference-only, or raw unnormalized assets.", - "", - "## Counts", - "", - "| Kind | Count |", - "| --- | ---: |", - ] - for key, value in sorted(counts.items()): - lines.append(f"| `{key}` | {value} |") - lines.extend(["", "## Digests", "", "| Scope | SHA-256 prefix |", "| --- | --- |"]) - for key, value in sorted(digests.items()): - lines.append(f"| `{key}` | `{value}` |") - - lines.extend(["", "## Resource Summary", "", "| Kind | Example IDs |", "| --- | --- |"]) - by_kind: dict[str, list[str]] = {} - for resource in resources: - by_kind.setdefault(resource["kind"], []).append(resource["id"]) - for kind, ids in sorted(by_kind.items()): - preview = ", ".join(f"`{item}`" for item in ids[:8]) - if len(ids) > 8: - preview += f", ... (+{len(ids) - 8})" - lines.append(f"| `{kind}` | {preview} |") - - lines.extend(["", "## Example Corpus", "", "| Example | Pages | Media | Base64 | Digest |", "| --- | ---: | ---: | ---: | --- |"]) - for resource in resources: - if resource["kind"] != "example_project": - continue - metadata = resource.get("metadata", {}) - lines.append( - f"| `{resource['id']}` | {metadata.get('page_count', 0)} | {metadata.get('media_count', 0)} | " - f"{metadata.get('base64_count', 0)} | `{metadata.get('project_digest', '')}` |" - ) - - lines.extend(["", "## Icon Libraries", "", "| Library | Icons | Policy |", "| --- | ---: | --- |"]) - for resource in resources: - if resource["kind"] != "icon_library": - continue - metadata = resource.get("metadata", {}) - lines.append(f"| `{resource['id']}` | {metadata.get('icon_count', 0)} | {metadata.get('index_policy', '')} |") - - return "\n".join(lines) + "\n" - - -def parse_args(argv: list[str] | None = None) -> argparse.Namespace: - parser = argparse.ArgumentParser(description="Build the SVGlide ppt-master asset inventory.") - parser.add_argument("--source", type=Path, default=default_source_root(), help="Path to the local ppt-master checkout.") - parser.add_argument("--out-json", type=Path, help="Output path for ppt-master-asset-map.json.") - parser.add_argument("--out-md", type=Path, help="Output path for ppt-master-asset-inventory.md.") - parser.add_argument("--check", action="store_true", help="Validate the source and generated map without writing files.") - return parser.parse_args(argv) - - -def main(argv: list[str] | None = None) -> int: - args = parse_args(argv) - asset_map = build_asset_map(args.source) - if args.check: - print(json.dumps(asset_map["summary"], ensure_ascii=False, indent=2)) - return 0 - if args.out_json: - args.out_json.parent.mkdir(parents=True, exist_ok=True) - args.out_json.write_text(json.dumps(asset_map, ensure_ascii=False, indent=2) + "\n", encoding="utf-8") - if args.out_md: - args.out_md.parent.mkdir(parents=True, exist_ok=True) - args.out_md.write_text(render_markdown(asset_map), encoding="utf-8") - if not args.out_json and not args.out_md: - print(json.dumps(asset_map, ensure_ascii=False, indent=2)) - return 0 - - -if __name__ == "__main__": - raise SystemExit(main()) diff --git a/skills/lark-slides/scripts/ppt_master_asset_inventory_test.py b/skills/lark-slides/scripts/ppt_master_asset_inventory_test.py deleted file mode 100644 index 9231f07ea..000000000 --- a/skills/lark-slides/scripts/ppt_master_asset_inventory_test.py +++ /dev/null @@ -1,96 +0,0 @@ -# Copyright (c) 2026 Lark Technologies Pte. Ltd. -# SPDX-License-Identifier: MIT -from __future__ import annotations - -import json -import sys -import unittest -from collections import Counter -from pathlib import Path - -SCRIPT_DIR = Path(__file__).resolve().parent -sys.path.insert(0, str(SCRIPT_DIR)) - -import ppt_master_asset_inventory as inventory - - -REFERENCE_DIR = SCRIPT_DIR.parent / "references" - - -def ppt_master_source() -> Path: - return inventory.default_source_root() - - -class PptMasterAssetInventoryTest(unittest.TestCase): - @classmethod - def setUpClass(cls) -> None: - cls.source = ppt_master_source() - cls.asset_map = inventory.build_asset_map(cls.source) - - def test_schema_and_counts_cover_required_categories(self) -> None: - inventory.validate_asset_map(self.asset_map) - resources = self.asset_map["resources"] - counts = Counter(resource["kind"] for resource in resources) - - self.assertEqual(self.asset_map["schema_version"], inventory.SCHEMA_VERSION) - self.assertEqual(counts["brand_preset"], 2) - self.assertEqual(counts["layout_template"], 7) - self.assertEqual(counts["deck_template"], 8) - self.assertEqual(counts["chart_template"], 71) - self.assertEqual(counts["icon_library"], 5) - self.assertEqual(counts["example_project"], 21) - self.assertEqual(counts["visual_style"], 18) - self.assertEqual(counts["image_palette"], 14) - self.assertEqual(counts["image_rendering"], 20) - self.assertEqual(counts["image_type_template"], 11) - self.assertEqual(counts["narrative_mode"], 5) - self.assertGreaterEqual(counts["workflow_reference"], 6) - - for resource in resources: - missing = inventory.REQUIRED_FIELDS - set(resource) - self.assertEqual(missing, set(), resource["id"]) - - def test_checked_in_asset_map_matches_source_counts_and_digests(self) -> None: - checked_in = json.loads((REFERENCE_DIR / "ppt-master-asset-map.json").read_text(encoding="utf-8")) - - self.assertEqual(checked_in["schema_version"], inventory.SCHEMA_VERSION) - self.assertEqual(checked_in["summary"]["counts"], self.asset_map["summary"]["counts"]) - self.assertEqual(checked_in["summary"]["digests"], self.asset_map["summary"]["digests"]) - - def test_raw_svg_resources_are_never_active(self) -> None: - raw_resources = [ - resource - for resource in self.asset_map["resources"] - if resource["protocol_compatibility"] == "needs_normalization" - ] - self.assertGreater(len(raw_resources), 0) - - for resource in raw_resources: - self.assertEqual(resource["copy_policy"], "derive_contract_only", resource["id"]) - self.assertEqual(resource["license_status"], "reference_only", resource["id"]) - self.assertNotIn(resource["activation_status"], {"validated", "active"}, resource["id"]) - - def test_icons_and_examples_are_summarized_not_expanded(self) -> None: - icon_resources = [resource for resource in self.asset_map["resources"] if resource["kind"] == "icon_library"] - icon_file_count = self.asset_map["summary"]["counts"]["icon_svg_files"] - - self.assertEqual(len(icon_resources), 5) - self.assertGreater(icon_file_count, 11000) - self.assertLess(len(self.asset_map["resources"]), icon_file_count) - for resource in icon_resources: - self.assertEqual(resource["granularity"], "library_summary") - self.assertEqual(resource["metadata"]["index_policy"], "library_summary_only") - self.assertIn("sample_icons", resource["metadata"]) - - examples = [resource for resource in self.asset_map["resources"] if resource["kind"] == "example_project"] - self.assertEqual(len(examples), 21) - for resource in examples: - self.assertEqual(resource["granularity"], "project_page_media_summary") - self.assertIn("page_count", resource["metadata"]) - self.assertIn("media_count", resource["metadata"]) - self.assertIn("base64_count", resource["metadata"]) - self.assertIn("project_digest", resource["metadata"]) - - -if __name__ == "__main__": - unittest.main() diff --git a/skills/lark-slides/scripts/svg_preflight.py b/skills/lark-slides/scripts/svg_preflight.py index bf12e7f06..fc0362e8b 100644 --- a/skills/lark-slides/scripts/svg_preflight.py +++ b/skills/lark-slides/scripts/svg_preflight.py @@ -1025,8 +1025,8 @@ def validate_chart_markers(root: ET.Element) -> list[dict[str, Any]]: def bbox_for_element(element: ET.Element) -> dict[str, float] | None: - name = local_name(element.tag) - if name in {"rect", "foreignObject", "image"}: + name = local_name(element.tag).lower() + if name in {"rect", "foreignobject", "image"}: x = parse_required_number(element, "x") y = parse_required_number(element, "y") width = parse_required_number(element, "width") @@ -1096,6 +1096,16 @@ def is_safe_area_exempt_backing(element: ET.Element, bbox: dict[str, float], can return False +def is_safe_area_exempt_page_chrome(element: ET.Element, bbox: dict[str, float], canvas_width: float, canvas_height: float) -> bool: + identifier = normalize_name(element_identifier_text(element)) + name = local_name(element.tag).lower() + if re.search(r"(^|_)(top|bottom|page)_(rule|line)$", identifier): + return bbox["height"] <= 8 and bbox["width"] >= canvas_width * 0.5 + if "footer" in identifier and name in {"foreignobject", "text"}: + return bbox["y"] >= SAFE_AREA["y"] + SAFE_AREA["height"] - 2 and bbox["x"] >= SAFE_AREA["x"] and bbox_right(bbox) <= SAFE_AREA["x"] + SAFE_AREA["width"] + return False + + def bbox_outside(bbox: dict[str, float], rect: dict[str, float]) -> bool: return ( bbox["x"] < rect["x"] @@ -1289,7 +1299,11 @@ def validate_geometry(elements: list[ET.Element], canvas_width: float, canvas_he "Non-background elements must fit inside the slide canvas.", ) ) - elif bbox_outside(bbox, SAFE_AREA) and not is_safe_area_exempt_backing(element, bbox, canvas_width, canvas_height): + elif ( + bbox_outside(bbox, SAFE_AREA) + and not is_safe_area_exempt_backing(element, bbox, canvas_width, canvas_height) + and not is_safe_area_exempt_page_chrome(element, bbox, canvas_width, canvas_height) + ): issues.append( issue( "warning", @@ -1938,6 +1952,7 @@ def summarize_visual_primitives(root: ET.Element, elements: list[ET.Element], te semi_transparent_rects: list[dict[str, float]] = [] primitive_areas: dict[str, float] = {} hidden_counts: dict[str, int] = {} + element_bboxes: list[dict[str, Any]] = [] for element in elements: name = local_name(element.tag) @@ -1952,6 +1967,15 @@ def summarize_visual_primitives(root: ET.Element, elements: list[ET.Element], te for primitive in ["path"] if name == "path" else []: hidden_counts[primitive] = hidden_counts.get(primitive, 0) + 1 continue + if not is_background_bbox(bbox, canvas_width, canvas_height): + element_bboxes.append( + { + "tag": name, + "role": svg_role(element) or "", + "element_id": get_attr(element, "id") or "", + "bbox": {key: round(value, 2) for key, value in bbox.items()}, + } + ) area = bbox["width"] * bbox["height"] signal_area = primitive_signal_area(element, bbox) hidden = element_is_hidden(element) @@ -2102,6 +2126,7 @@ def summarize_visual_primitives(root: ET.Element, elements: list[ET.Element], te "counts": counts, "primitive_areas": {key: round(value, 2) for key, value in sorted(primitive_areas.items())}, "hidden_counts": dict(sorted(hidden_counts.items())), + "bboxes": element_bboxes, "gradient_count": gradients, "gradient_ref_count": gradient_refs, "filter_count": filters + filter_refs, @@ -2397,10 +2422,23 @@ def validation_profile(plan: dict[str, Any]) -> dict[str, Any]: return profile if isinstance(profile, dict) else {} +def validation_profile_name(plan: dict[str, Any]) -> str: + profile = plan.get("validation_profile") + if isinstance(profile, str): + raw = normalize_name(profile) + elif isinstance(profile, dict): + raw = normalize_name(profile.get("profile") or profile.get("name") or profile.get("mode")) + else: + raw = "" + if raw in {"gold", "golden_regression"}: + return "golden" + return raw + + def seed_gate_level(plan: dict[str, Any]) -> str: profile = validation_profile(plan) drift_policy = normalize_name(profile.get("drift_policy") or profile.get("seed_policy") or profile.get("mode")) - if profile.get("strict") is True or drift_policy in {"error", "errors", "strict", "fail", "fail_closed"}: + if profile.get("strict") is True or validation_profile_name(plan) == "golden" or drift_policy in {"error", "errors", "strict", "fail", "fail_closed"}: return "error" return "warning" @@ -3005,12 +3043,12 @@ def slide_chart_type(slide: dict[str, Any]) -> str: for key in ["chart_type", "chartType", "primary_chart_type", "primaryChartType"]: if key in slide: return normalize_name(slide.get(key)) - reference = slide.get("reference_asset") or slide.get("ppt_master_reference_asset") + reference = slide.get("reference_asset") or slide.get("design_reference_asset") if isinstance(reference, dict): asset_id = textify(reference.get("asset_id") or reference.get("id")).strip() if asset_id.startswith("chart."): return normalize_name(asset_id.split(".", 1)[1]) - references = slide.get("ppt_master_reference_assets") + references = slide.get("design_reference_assets") if isinstance(references, list): for item in references: if not isinstance(item, dict): @@ -3021,6 +3059,146 @@ def slide_chart_type(slide: dict[str, Any]) -> str: return "" +def strategist_contract_level(plan: dict[str, Any]) -> str: + return "error" if seed_gate_level(plan) == "error" or validation_profile_name(plan) == "golden" else "warning" + + +def page_rhythm_contract(plan: dict[str, Any]) -> Any: + for key in ["page_rhythm", "deck_rhythm", "rhythm_plan", "narrative_rhythm"]: + value = plan.get(key) + if value is not None and textify(value).strip(): + return value + return None + + +def slide_page_type(slide: dict[str, Any]) -> str: + for key in ["page_type", "slide_type", "archetype", "layout_type"]: + value = normalize_name(slide.get(key)) + if value: + return value + return "" + + +def slide_main_visual_anchor(slide: dict[str, Any]) -> Any: + for key in ["main_visual_anchor", "visual_anchor", "visual_focal_anchor", "primary_visual_anchor"]: + if key in slide: + return slide.get(key) + return None + + +def slide_reference_assets(slide: dict[str, Any]) -> list[Any]: + out: list[Any] = [] + for key in ["reference_asset", "design_reference_asset"]: + if key in slide: + out.append(slide.get(key)) + references = slide.get("reference_assets") + if isinstance(references, list): + out.extend(references) + references = slide.get("design_reference_assets") + if isinstance(references, list): + out.extend(references) + return out + + +def reference_asset_is_structured(value: Any) -> bool: + if value is None: + return True + if isinstance(value, str): + return bool(NO_ASSET_RE.search(value)) + if isinstance(value, list): + return all(reference_asset_is_structured(item) for item in value) + if not isinstance(value, dict): + return False + if value.get("enabled") is False: + return True + identity = textify( + value.get("asset_id") + or value.get("id") + or value.get("source_id") + or value.get("local_path") + or value.get("local_path_or_href") + or value.get("href") + or value.get("path") + ).strip() + source = textify(value.get("source") or value.get("source_type") or value.get("kind")).strip() + return bool(identity and source) + + +def anchor_matches_layout_box(anchor: str, layout_boxes: list[dict[str, Any]]) -> bool: + normalized_anchor = normalize_name(anchor.removeprefix("#")) + if not normalized_anchor: + return False + for box in layout_boxes: + if not isinstance(box, dict): + continue + candidates = [ + box.get("role"), + box.get("id"), + box.get("name"), + box.get("layout_box_role"), + ] + if normalized_anchor in {normalize_name(candidate) for candidate in candidates if textify(candidate).strip()}: + return True + return anchor.strip().startswith("#") + + +def anchor_contract_is_checkable(anchor: Any, layout_boxes: list[dict[str, Any]]) -> bool: + if isinstance(anchor, dict): + if layout_box_has_positive_bbox(anchor): + return True + for key in ["layout_box_role", "role", "element_id", "svg_element_id", "component_id"]: + if textify(anchor.get(key)).strip(): + return True + return False + if isinstance(anchor, str): + return anchor_matches_layout_box(anchor, layout_boxes) + return False + + +def anchor_bbox(anchor: Any, layout_boxes: list[dict[str, Any]]) -> dict[str, float] | None: + if isinstance(anchor, dict) and layout_box_has_positive_bbox(anchor): + return bbox_from_mapping(anchor) + role = "" + if isinstance(anchor, dict): + role = textify(anchor.get("layout_box_role") or anchor.get("role")).strip() + elif isinstance(anchor, str): + role = anchor.removeprefix("#").strip() + normalized_role = normalize_name(role) + if not normalized_role: + return None + for box in layout_boxes: + if not isinstance(box, dict): + continue + candidates = [box.get("role"), box.get("id"), box.get("name"), box.get("layout_box_role")] + if normalized_role in {normalize_name(candidate) for candidate in candidates if textify(candidate).strip()} and layout_box_has_positive_bbox(box): + return bbox_from_mapping(box) + return None + + +def source_has_geometry_in_bbox(file: dict[str, Any], bbox: dict[str, float]) -> bool: + visual = file.get("visual_primitives", {}) + boxes = visual.get("bboxes") if isinstance(visual, dict) else None + if not isinstance(boxes, list): + return False + for item in boxes: + if not isinstance(item, dict): + continue + item_bbox = item.get("bbox") + if not isinstance(item_bbox, dict): + continue + try: + candidate = {key: float(item_bbox[key]) for key in ["x", "y", "width", "height"]} + except (KeyError, TypeError, ValueError): + continue + if candidate["width"] <= 0 or candidate["height"] <= 0: + continue + if is_background_bbox(candidate, CANVAS_WIDTH, CANVAS_HEIGHT): + continue + if overlap_area(candidate, bbox) >= min(candidate["width"] * candidate["height"], bbox["width"] * bbox["height"]) * 0.2: + return True + return False + + def required_plan_primitives(slide: dict[str, Any]) -> set[str]: return normalize_primitives(slide.get("required_primitives")) @@ -3165,6 +3343,7 @@ def lint_plan(plan: dict[str, Any], path: str = "", context: dict[str, Any is_create_svg_route = context.get("route_id") == CREATE_SVG_ROUTE_ID is_svg_plan = plan.get("output_mode") == "svglide-svg" or is_create_svg_route seed_level = seed_gate_level(plan) + strategist_level = strategist_contract_level(plan) deck_preset_id = deck_style_preset_id(plan) deck_style_system = style_system(plan) if is_svg_plan: @@ -3238,6 +3417,16 @@ def lint_plan(plan: dict[str, Any], path: str = "", context: dict[str, Any f"SVGlide style_system must include {field}", ) ) + if len(slides) > 1 and page_rhythm_contract(plan) is None: + issues.append( + plan_issue( + strategist_level, + "plan_missing_page_rhythm", + "multi-page SVGlide plans must declare page_rhythm so deck pacing can be checked", + None, + "Add page_rhythm/deck_rhythm with per-section rhythm, density changes, and repeated-page guardrails.", + ) + ) renderer_ids: list[str] = [] layout_families: list[str] = [] @@ -3255,6 +3444,7 @@ def lint_plan(plan: dict[str, Any], path: str = "", context: dict[str, Any continue visual_plan = slide_visual_plan(slide) + layout_boxes = slide_layout_boxes(visual_plan) renderer_id = slide_renderer_id(visual_plan) renderer_ids.append(renderer_id) @@ -3280,14 +3470,47 @@ def lint_plan(plan: dict[str, Any], path: str = "", context: dict[str, Any "SVGlide plan slides must include layout_family so deck-level layout diversity is enforceable", slide, "Use layout families such as hero, agenda_matrix, dashboard, table, timeline, flow, risk_grid, swimlane, or closing.", + ) ) - ) raw_visual_recipe = normalize_name(visual_plan.get("visual_recipe")) visual_recipe = raw_visual_recipe route_private_requested = raw_visual_recipe == ROUTE_PRIVATE_VISUAL_RECIPE seed_id = slide_seed_id(visual_plan) seed_data = None + page_type = slide_page_type(visual_plan) + if not page_type: + issues.append( + plan_issue( + strategist_level, + "plan_missing_page_type", + "SVGlide plan slides must declare page_type so Strategist intent is checkable", + slide, + "Use page_type values such as cover, section, process, comparison, data_story, image_story, dashboard, chart, or closing.", + ) + ) + anchor = slide_main_visual_anchor(visual_plan) + if not anchor_contract_is_checkable(anchor, layout_boxes): + issues.append( + plan_issue( + strategist_level, + "plan_missing_main_visual_anchor", + "SVGlide plan slides must declare a checkable main_visual_anchor", + slide, + "Point main_visual_anchor at a layout box role, #svg-element-id, component_id, or explicit x/y/width/height bbox.", + ) + ) + for reference in slide_reference_assets(visual_plan): + if not reference_asset_is_structured(reference): + issues.append( + plan_issue( + strategist_level, + "plan_reference_asset_unstructured", + "reference_asset must be structured source metadata, not prose", + slide, + "Use {source, asset_id/id} for design references or source/license/path metadata for concrete assets.", + ) + ) if not seed_id: issues.append( plan_issue( @@ -3332,7 +3555,6 @@ def lint_plan(plan: dict[str, Any], path: str = "", context: dict[str, Any "Seed and visual_recipe must describe the same structure; choose a different seed or update the recipe.", ) ) - layout_boxes = slide_layout_boxes(visual_plan) if not layout_boxes: issues.append( plan_issue( @@ -4103,6 +4325,7 @@ def lint_plan_svg_alignment(plan: dict[str, Any], files: list[dict[str, Any]], c chart_type_contracts = context.get("chart_type_contracts", CHART_TYPE_CONTRACTS) for slide_index, slide, file in alignments: visual_plan = slide_visual_plan(slide) + layout_boxes = slide_layout_boxes(visual_plan) recipe = resolved_visual_recipe_for_slide(visual_plan, slide_index, context) seed_id = slide_seed_id(visual_plan) seed_data = seed_catalog.get(seed_id) if seed_id in seed_catalog else None @@ -4209,7 +4432,19 @@ def lint_plan_svg_alignment(plan: dict[str, Any], files: list[dict[str, Any]], c "plan_chart_type_contract_not_met", f'chart_type "{chart_type}" geometry contract is not met: {", ".join(chart_failures)}', slide, - f"SVG file {file.get('path')} does not expose enough chart geometry for the declared ppt-master-style page type.", + f"SVG file {file.get('path')} does not expose enough chart geometry for the declared design-pattern page type.", + ) + ) + anchor = slide_main_visual_anchor(visual_plan) + visual_anchor_bbox = anchor_bbox(anchor, layout_boxes) + if visual_anchor_bbox is not None and not source_has_geometry_in_bbox(file, visual_anchor_bbox): + issues.append( + plan_issue( + "error", + "plan_main_visual_anchor_not_met", + "main_visual_anchor does not overlap visible SVG source geometry", + slide, + f"SVG file {file.get('path')} must place the declared main visual inside the anchored layout box or bbox.", ) ) if "image" in source_primitives: diff --git a/skills/lark-slides/scripts/svg_preflight_test.py b/skills/lark-slides/scripts/svg_preflight_test.py index b10ecbaef..6287e516b 100644 --- a/skills/lark-slides/scripts/svg_preflight_test.py +++ b/skills/lark-slides/scripts/svg_preflight_test.py @@ -581,6 +581,24 @@ class SvgPreflightTest(unittest.TestCase): self.assertNotIn("safe_area", codes) self.assertEqual(result["summary"]["error_count"], 0) + def test_lint_svg_does_not_warn_safe_area_for_page_chrome_rule_and_footer(self) -> None: + svg = """ + + + +
Demo footer · 01
+
+ +
+ """ + result = svg_preflight.lint_svg(with_contract(svg)) + safe_area_ids = [issue.get("element_id") for issue in result.get("issues", []) if issue.get("code") == "safe_area"] + + self.assertEqual(["badge"], safe_area_ids) + def test_lint_svg_reports_text_bbox_overlap(self) -> None: svg = """ None: + plan = single_slide_plan() + second_slide = dict(plan["slides"][0]) + second_slide["page"] = 2 + second_slide["title"] = "Route 2" + plan["page_count"] = 2 + plan["slides"] = [plan["slides"][0], second_slide] + + result = svg_preflight.lint_plan(plan) + + self.assertIn("plan_missing_page_rhythm", issue_codes(result)) + self.assertEqual(issue_levels(result, "plan_missing_page_rhythm"), ["warning"]) + + def test_lint_plan_golden_requires_strategist_contract_fields(self) -> None: + plan = single_slide_plan() + second_slide = dict(plan["slides"][0]) + second_slide["page"] = 2 + plan["page_count"] = 2 + plan["slides"] = [plan["slides"][0], second_slide] + plan["validation_profile"] = {"profile": "golden"} + + result = svg_preflight.lint_plan(plan) + + self.assertEqual(issue_levels(result, "plan_missing_page_rhythm"), ["error"]) + self.assertIn("plan_missing_page_type", issue_codes(result)) + self.assertIn("plan_missing_main_visual_anchor", issue_codes(result)) + + def test_lint_plan_reports_unstructured_reference_asset(self) -> None: + plan = single_slide_plan(reference_asset="use the chart reference") + + result = svg_preflight.lint_plan(plan) + + self.assertIn("plan_reference_asset_unstructured", issue_codes(result)) + def test_public_recipe_registry_is_runtime_catalog_source(self) -> None: registry = read_json(PUBLIC_RECIPE_REGISTRY_PATH) recipes = registry.get("recipes") @@ -1763,6 +1815,22 @@ class SvgPreflightTest(unittest.TestCase): result = svg_preflight.lint_files([str(svg_path)], str(plan_path)) self.assertIn("plan_chart_type_contract_not_met", plan_issue_codes(result)) + def test_lint_files_reports_main_visual_anchor_not_met_by_source(self) -> None: + with tempfile.TemporaryDirectory() as tmpdir: + tmp = Path(tmpdir) + svg_path = tmp / "page-001.svg" + plan_path = tmp / "slide_plan.json" + svg_path.write_text(VALID_SVG, encoding="utf-8") + plan = single_slide_plan( + main_visual_anchor={"x": 880, "y": 450, "width": 40, "height": 40}, + ) + plan["svg_files"] = [{"page": 1, "path": "page-001.svg"}] + plan_path.write_text(json.dumps(plan), encoding="utf-8") + + result = svg_preflight.lint_files([str(svg_path)], str(plan_path)) + + self.assertIn("plan_main_visual_anchor_not_met", plan_issue_codes(result)) + def test_lint_files_reports_svg_input_count_mismatch_with_plan_page_count(self) -> None: with tempfile.TemporaryDirectory() as tmpdir: tmp = Path(tmpdir) @@ -1814,13 +1882,13 @@ class SvgPreflightTest(unittest.TestCase): "geometric_composition", list(svg_preflight.VISUAL_RECIPE_CATALOG["geometric_composition"]["required_primitives"]), chart_type="bubble_chart", - ppt_master_reference_assets=[{"asset_id": "chart.bubble_chart", "source": "ppt-master"}], + design_reference_assets=[{"asset_id": "chart.bubble_chart", "source": "svglide_design_pattern"}], ) donut_plan = single_slide_plan( "infographic_scorecard", ["typography", "micro_chart"], chart_type="donut_chart", - ppt_master_reference_assets=[{"asset_id": "chart.donut_chart", "source": "ppt-master"}], + design_reference_assets=[{"asset_id": "chart.donut_chart", "source": "svglide_design_pattern"}], ) slide7 = bubble_plan["slides"][0] slide8 = donut_plan["slides"][0] @@ -2092,7 +2160,7 @@ class SvgPreflightTest(unittest.TestCase): codes = [issue["code"] for issue in result["plan"]["issues"]] self.assertIn("plan_asset_contract_missing_metadata", codes) self.assertEqual(result["summary"]["error_count"], 0) - self.assertEqual(result["summary"]["warning_count"], 1) + self.assertEqual(codes.count("plan_asset_contract_missing_metadata"), 1) def test_lint_files_accepts_preview_image_asset_metadata(self) -> None: with tempfile.TemporaryDirectory() as tmpdir: diff --git a/skills/lark-slides/scripts/svg_preview_lint.py b/skills/lark-slides/scripts/svg_preview_lint.py index ff40b4a1c..bfa67f27b 100644 --- a/skills/lark-slides/scripts/svg_preview_lint.py +++ b/skills/lark-slides/scripts/svg_preview_lint.py @@ -36,6 +36,10 @@ NUMBER_RE = re.compile(r"[-+]?(?:\d+\.?\d*|\.\d+)(?:[eE][-+]?\d+)?") STYLE_DECL_RE = re.compile(r"\s*([A-Za-z-]+)\s*:\s*([^;]+)") RGB_RE = re.compile(r"rgba?\(([^)]+)\)", re.IGNORECASE) TRANSFORM_RE = re.compile(r"(translate|scale)\s*\(([^)]*)\)", re.IGNORECASE) +PLACEHOLDER_COPY_RE = re.compile( + r"\b(contract renderer|placeholder|lorem ipsum|todo|draft only|smoke deck)\b|SVGlide\s+contract\s+renderer", + re.IGNORECASE, +) LOCAL_IMAGE_EXTENSIONS = (".png", ".jpg", ".jpeg", ".gif", ".webp", ".svg") RENDERABLE_TAGS = {"rect", "circle", "ellipse", "line", "path", "polygon", "polyline", "image", "text", "foreignobject"} PRESENTATION_ATTRS = { @@ -849,6 +853,27 @@ def is_label_text(element: RenderElement) -> bool: return any(token in ident for token in ["label", "name", "title", "badge", "pill"]) +def is_page_chrome(element: RenderElement, canvas_height: float) -> bool: + ident = element.element_id.lower() + if element.background: + return True + if ident in {"top-rule", "bottom-rule", "page-rule"}: + return True + if "footer" in ident: + return True + if element.tag in {"text", "foreignobject"} and element.box.y >= canvas_height - 52 and element.font_size <= 12: + return True + if element.tag in {"rect", "line"} and element.box.height <= 6 and element.box.width >= 0.5 * DEFAULT_CANVAS_WIDTH: + return True + return False + + +def semantic_label_token_count(elements: list[RenderElement]) -> int: + text = " ".join(element.text for element in elements if element.text.strip()) + parts = re.split(r"[/、,,;;|·\n]+", text) + return sum(1 for part in parts if len(part.strip()) >= 2) + + def element_path(project: Path, source: SvgSource) -> str: if source.path: return safe_rel(source.path, project) @@ -861,9 +886,17 @@ def lint_svg_source(project: Path, source: SvgSource) -> list[dict[str, Any]]: elements = collect_render_elements(source) body_elements = [element for element in elements if not element.background and element.box.area > 4] text_elements = [element for element in elements if element.tag in {"text", "foreignobject"} and element.text.strip()] + semantic_text_elements = [element for element in text_elements if not is_page_chrome(element, canvas_height)] dark_backings = [element for element in elements if element.tag in {"rect", "path", "polygon"} and is_dark_color(element.fill)] label_backings = [element for element in elements if is_label_backing(element)] image_elements = [element for element in elements if element.tag == "image" and element.box.area > 0] + meaningful_visual_elements = [ + element + for element in body_elements + if element.tag not in {"text", "foreignobject"} + and not is_page_chrome(element, canvas_height) + and element.box.area > 80 + ] path = element_path(project, source) if body_elements: @@ -894,6 +927,35 @@ def lint_svg_source(project: Path, source: SvgSource) -> list[dict[str, Any]]: source=source.label, ) ) + if any(PLACEHOLDER_COPY_RE.search(element.text) for element in text_elements): + checks.append( + check( + "placeholder_renderer_copy", + "warning", + "high", + "page exposes implementation, placeholder, or smoke-test copy", + page=source.page, + path=path, + source=source.label, + ) + ) + if ( + not image_elements + and len(meaningful_visual_elements) >= 5 + and len(semantic_text_elements) <= 2 + and semantic_label_token_count(semantic_text_elements) < 4 + ): + checks.append( + check( + "unlabeled_visual_system", + "warning", + "medium", + "page has many visual marks but too few semantic labels or annotations", + page=source.page, + path=path, + source=source.label, + ) + ) for element in elements: if element.tag in {"text", "foreignobject"} and element.box.area <= 0: diff --git a/skills/lark-slides/scripts/svg_preview_lint_test.py b/skills/lark-slides/scripts/svg_preview_lint_test.py index 3eab72788..5c266c133 100644 --- a/skills/lark-slides/scripts/svg_preview_lint_test.py +++ b/skills/lark-slides/scripts/svg_preview_lint_test.py @@ -228,6 +228,50 @@ class SvgPreviewLintTest(unittest.TestCase): self.assertIn("low_information_density", self.codes(result)) self.assertLess(result["visual_score"], 100) + def test_placeholder_renderer_copy_gets_warning(self) -> None: + project = self.make_project() + self.write_preview(project) + self.write_svg( + project, + """ + + + + Smoke deck + A generated page that still exposes implementation copy. + SVGlide contract renderer · 01 + """, + ) + + result = self.lint(project) + + self.assertEqual(result["status"], "passed") + self.assertIn("placeholder_renderer_copy", self.codes(result)) + self.assertLess(result["visual_score"], 100) + + def test_unlabeled_visual_system_gets_density_warning(self) -> None: + project = self.make_project() + self.write_preview(project) + self.write_svg( + project, + """ + + + + + + + Shape-only dashboard + The message is present but the visual system is not labeled. + """, + ) + + result = self.lint(project) + + self.assertEqual(result["status"], "passed") + self.assertIn("unlabeled_visual_system", self.codes(result)) + self.assertLess(result["visual_score"], 100) + def test_repeated_multi_page_layout_gets_variety_warning(self) -> None: project = self.make_project() refs = [] diff --git a/skills/lark-slides/scripts/svglide_asset_selector.py b/skills/lark-slides/scripts/svglide_asset_selector.py index e12fe0ea6..a17c48cbc 100644 --- a/skills/lark-slides/scripts/svglide_asset_selector.py +++ b/skills/lark-slides/scripts/svglide_asset_selector.py @@ -54,7 +54,7 @@ def script_path() -> Path: def default_asset_map_path() -> Path: - return script_path().parents[1] / "references/ppt-master-asset-map.json" + return script_path().parents[1] / "references/svglide-design-pattern-map.json" def load_asset_map(path: Path) -> dict: @@ -272,7 +272,7 @@ def estimate_prompt_tokens(selected_assets: list[dict]) -> int: def parse_args(argv: list[str] | None = None) -> argparse.Namespace: - parser = argparse.ArgumentParser(description="Select a small SVGlide active asset context from ppt-master-asset-map.json.") + parser = argparse.ArgumentParser(description="Select a small SVGlide active asset context from svglide-design-pattern-map.json.") parser.add_argument("--asset-map", type=Path, default=default_asset_map_path()) parser.add_argument("--brief", default="", help="User brief or deck topic.") parser.add_argument("--tags", default="", help="Comma-separated explicit selector tags.") diff --git a/skills/lark-slides/scripts/svglide_asset_selector_test.py b/skills/lark-slides/scripts/svglide_asset_selector_test.py index 26c1d3daa..588630f94 100644 --- a/skills/lark-slides/scripts/svglide_asset_selector_test.py +++ b/skills/lark-slides/scripts/svglide_asset_selector_test.py @@ -25,7 +25,7 @@ def resource( ) -> dict: return { "id": resource_id, - "source_path": f"ppt-master/{resource_id}.svg", + "source_path": f"svglide-design-patterns/{resource_id}.svg", "kind": kind, "summary": summary, "selection_tags": tags or ["business", "strategy", "roadmap", "chart"], @@ -40,13 +40,34 @@ def resource( def asset_map(resources: list[dict]) -> dict: return { - "schema_version": "svglide-ppt-master-asset-map/v1", + "schema_version": "svglide-design-pattern-map/v1", "summary": {"digests": {"all_source_files": "fixture"}}, "resources": resources, } class SVGlideAssetSelectorTest(unittest.TestCase): + def test_skill_sources_do_not_reference_external_reference_project_names(self) -> None: + skill_root = SCRIPT_DIR.parent + banned_tokens = [ + "ppt" + "-master", + "ppt" + "_master", + "ppt" + " master", + "hugo" + "he3", + "ppt" + "169", + "global" + "_ai" + "_capital", + ] + hits: list[str] = [] + for path in skill_root.rglob("*"): + if path.suffix not in {".py", ".md", ".json"} or not path.is_file(): + continue + text = path.read_text(encoding="utf-8").lower() + for token in banned_tokens: + if token in text: + hits.append(f"{path.relative_to(skill_root)} contains {token}") + + self.assertEqual([], hits) + def test_reference_only_is_excluded_from_production(self) -> None: data = asset_map( [ diff --git a/skills/lark-slides/scripts/svglide_gen_runtime.py b/skills/lark-slides/scripts/svglide_gen_runtime.py new file mode 100644 index 000000000..94f33ed39 --- /dev/null +++ b/skills/lark-slides/scripts/svglide_gen_runtime.py @@ -0,0 +1,2028 @@ +#!/usr/bin/env python3 +# Copyright (c) 2026 Lark Technologies Pte. Ltd. +# SPDX-License-Identifier: MIT + +from __future__ import annotations + +import argparse +import hashlib +import html +import json +import math +import re +import sys +from pathlib import Path +from typing import Any + + +W = 960 +H = 540 +COMPONENT_REPORT_SCHEMA = "svglide-component-report/v1" +DESIGN_PATTERN_USAGE_SCHEMA = "svglide-design-pattern-usage/v1" +RUNTIME_CACHE_SCHEMA = "svglide-gen-runtime-cache/v1" +SUPPORTED_PAGE_KINDS = ( + "cover", + "agenda", + "section", + "editor_note", + "kpi_cards", + "bar_chart", + "bubble_chart", + "donut_chart", + "sankey_chart", + "hub_spoke", + "closing", +) +DEFAULT_ACCENTS = ("#2563EB", "#0F9F8E", "#F59E0B", "#E11D48", "#7C3AED", "#0891B2", "#65A30D", "#334155") +PAGE_KIND_ALIASES = { + "cover_slide": "cover", + "contents": "agenda", + "table_of_contents": "agenda", + "toc": "agenda", + "agenda_numbered_path": "agenda", + "section_divider": "section", + "section_divider_index": "section", + "chapter": "section", + "editor-note": "editor_note", + "editorial_note": "editor_note", + "note": "editor_note", + "kpi": "kpi_cards", + "metric_cards": "kpi_cards", + "bubble": "bubble_chart", + "bubble_scatter": "bubble_chart", + "donut": "donut_chart", + "sankey": "sankey_chart", + "hub": "hub_spoke", + "hub-and-spoke": "hub_spoke", + "hub_and_spoke": "hub_spoke", + "ending": "closing", + "summary": "closing", +} +DEFAULT_ASSET_BY_KIND = { + "cover": "layout.page_type.cover", + "agenda": "chart.agenda_list", + "section": "chart.numbered_steps", + "editor_note": "layout.page_type.content", + "closing": "layout.page_type.ending", + "bar_chart": "chart.bar_chart", + "bubble_chart": "chart.bubble_chart", + "donut_chart": "chart.donut_chart", + "hub_spoke": "chart.hub_spoke", + "kpi_cards": "chart.kpi_cards", + "sankey_chart": "chart.sankey_chart", +} +BG = "#070A0F" +BG_2 = "#0D1118" +PANEL = "#111820" +PANEL_2 = "#17202B" +RULE = "#29323D" +INK = "#F4EFE7" +MUTED = "#9AA0A8" +RED = "#E63946" +GOLD = "#C8BFB4" +SERIF = "Cambria,'Times New Roman','Source Han Serif SC','Noto Serif CJK SC',serif" +SANS = "'Source Han Sans SC','Noto Sans CJK SC',Roboto,sans-serif" + + +def esc(value: object) -> str: + return html.escape(str(value), quote=True) + + +def attrs(values: dict[str, object]) -> str: + return " ".join(f'{key}="{esc(value)}"' for key, value in values.items() if value is not None) + + +def tag(name: str, values: dict[str, object], content: str = "") -> str: + if content: + return f"<{name} {attrs(values)}>{content}" + return f"<{name} {attrs(values)} />" + + +def text_box( + box_id: str, + x: int, + y: int, + width: int, + height: int, + text: str, + *, + size: int = 18, + weight: int = 600, + color: str = "#18212F", + align: str = "left", + family: str = SANS, + line_height: float = 1.24, + letter_spacing: float = 0, +) -> str: + style = ( + f"font-family:{family};" + f"font-size:{size}px;font-weight:{weight};color:{color};line-height:{line_height};" + f"text-align:{align};letter-spacing:{letter_spacing}px;" + ) + body = esc(text).replace("\n", "
") + return tag( + "foreignObject", + { + "id": box_id, + "slide:role": "shape", + "slide:shape-type": "text", + "x": x, + "y": y, + "width": width, + "height": height, + }, + f'
{body}
', + ) + + +def rect(rect_id: str, x: int, y: int, width: int, height: int, fill: str, **extra: object) -> str: + return tag( + "rect", + {"id": rect_id, "slide:role": "shape", "x": x, "y": y, "width": width, "height": height, "fill": fill, **extra}, + ) + + +def circle(circle_id: str, cx: int, cy: int, r: int, fill: str, **extra: object) -> str: + return tag("circle", {"id": circle_id, "slide:role": "shape", "cx": cx, "cy": cy, "r": r, "fill": fill, **extra}) + + +def assert_no_arc_path(d: str) -> None: + if re.search(r"[Aa](?=[\s,\d.+-])", d): + raise ValueError("SVGlide-safe SVG does not allow path A/a arc commands") + + +def path(path_id: str, d: str, fill: str = "none", stroke: str | None = None, **extra: object) -> str: + assert_no_arc_path(d) + values = {"id": path_id, "slide:role": "shape", "d": d, "fill": fill, **extra} + if stroke: + values["stroke"] = stroke + return tag("path", values) + + +def line(line_id: str, x1: int, y1: int, x2: int, y2: int, stroke: str, **extra: object) -> str: + return tag("line", {"id": line_id, "slide:role": "shape", "x1": x1, "y1": y1, "x2": x2, "y2": y2, "stroke": stroke, **extra}) + + +def asset_mark(asset_id: str, x: int, y: int, scale: float, color: str, *, opacity: float = 1.0) -> list[str]: + # SVG-safe, arc-free ornament paths distilled into SVGlide editorial marks. + marks = { + "quote_ticks": ["M0 0 L8 0 L6 20 L0 20 Z", "M14 0 L22 0 L20 20 L14 20 Z"], + "spark": ["M12 0 L16 9 L26 12 L16 15 L12 24 L8 15 L0 12 L8 9 Z"], + "chevron": ["M0 0 L20 0 L32 12 L20 24 L0 24 L12 12 Z"], + "slash": ["M0 24 L7 24 L25 0 L18 0 Z"], + } + paths = marks.get(asset_id) + if not paths: + raise ValueError(f"unknown asset mark {asset_id}") + return [ + path( + f"{asset_id}-{index+1}", + d, + fill=color, + transform=f"translate({x} {y}) scale({scale})", + opacity=opacity, + ) + for index, d in enumerate(paths) + ] + + +def radial_connector(cx: int, cy: int, tx: int, ty: int, start_radius: int, end_radius: int) -> tuple[int, int, int, int]: + dx = tx - cx + dy = ty - cy + distance = math.hypot(dx, dy) or 1 + ux = dx / distance + uy = dy / distance + return ( + int(round(cx + ux * start_radius)), + int(round(cy + uy * start_radius)), + int(round(tx - ux * end_radius)), + int(round(ty - uy * end_radius)), + ) + + +def bbox(x: int, y: int, width: int, height: int) -> dict[str, int]: + return {"x": x, "y": y, "width": width, "height": height} + + +class ComponentReport: + def __init__(self, *, generator: str = "svglide_gen_runtime") -> None: + self.generator = generator + self.pages: dict[int, list[dict[str, Any]]] = {} + + def add( + self, + page: int, + component_id: str, + renderer_id: str, + box: dict[str, int], + primitives: list[str], + *, + effects: list[str] | None = None, + source_trace: str = "", + asset_id: str = "", + ) -> None: + component = { + "id": component_id, + "renderer_id": renderer_id, + "bbox": box, + "primitives": primitives, + "effects": effects or [], + } + if source_trace: + component["source_trace"] = source_trace + if asset_id: + component["asset_id"] = asset_id + self.pages.setdefault(page, []).append(component) + + def to_dict(self) -> dict[str, Any]: + pages = [{"page": page, "components": self.pages[page]} for page in sorted(self.pages)] + return { + "schema_version": COMPONENT_REPORT_SCHEMA, + "status": "passed", + "generator": self.generator, + "pages": pages, + "summary": { + "page_count": len(pages), + "component_count": sum(len(page["components"]) for page in pages), + "error_count": 0, + "warning_count": 0, + }, + } + + +def report_page(report: dict[str, Any], page: int) -> dict[str, Any]: + for item in report.get("pages", []): + if isinstance(item, dict) and item.get("page") == page: + return item + return {"page": page, "components": []} + + +def design_pattern_usage_receipt(report: dict[str, Any]) -> dict[str, Any]: + usages: list[dict[str, Any]] = [] + for page in report.get("pages", []): + if not isinstance(page, dict): + continue + component_ids: dict[str, list[str]] = {} + traces: dict[str, str] = {} + for component in page.get("components", []): + if not isinstance(component, dict): + continue + asset_id = str(component.get("asset_id") or "").strip() + if not asset_id: + continue + component_ids.setdefault(asset_id, []).append(str(component.get("id") or "")) + traces[asset_id] = str(component.get("source_trace") or component.get("renderer_id") or "runtime") + for asset_id, ids in sorted(component_ids.items()): + usages.append( + { + "page": page.get("page"), + "asset_id": asset_id, + "component_ids": [item for item in ids if item], + "source_trace": traces.get(asset_id, "runtime"), + } + ) + return { + "schema_version": DESIGN_PATTERN_USAGE_SCHEMA, + "status": "passed", + "used_asset_ids": sorted({str(usage.get("asset_id") or "") for usage in usages if usage.get("asset_id")}), + "page_usages": usages, + "error_count": 0, + "warning_count": 0, + } + + +def svg_wrap(page: int, accent: str, body: list[str]) -> str: + defs = f""" + + + + + + + """ + return ( + f'\n' + + defs + + "\n " + + "\n ".join(body) + + "\n\n" + ) + + +def editorial_shell(page: int, accent: str, title: str, summary: str) -> list[str]: + return [ + *editorial_background(page, accent), + rect("title-surface", 78, 54, 700, 60, "#090D13", rx=0, opacity=0.82), + rect("title-accent", 70, 52, 8, 36, accent, rx=3), + text_box("kicker", 86, 40, 430, 18, f"PART {page:02d} / SVGLIDE EDITORIAL RENDERER", size=8, weight=800, color="#6D737D", family=SERIF, letter_spacing=2), + text_box("title", 86, 62, 680, 44, title, size=25, weight=900, color=INK, family=SERIF), + text_box("subtitle", 86, 120, 610, 34, summary[:130], size=10, weight=500, color=MUTED), + ] + + +def editorial_background(page: int, accent: str) -> list[str]: + return [ + rect("background", 0, 0, W, H, f"url(#bg-{page})"), + rect("edge-shadow", 40, 0, 1, H, "#202632", opacity=0.65), + rect("right-note-band", 790, 0, 1, H, "#1B232E", opacity=0.78), + ] + + +def editorial_footer(page: int) -> str: + return text_box( + "footer", + 70, + 480, + 820, + 18, + f"SVGlide local preview · design renderer v1 · {page:02d}", + size=8, + weight=600, + color="#5F6670", + align="center", + ) + + +def compact_text(value: Any) -> str: + if value is None: + return "" + if isinstance(value, str): + return value.strip() + if isinstance(value, dict): + return " ".join(compact_text(item) for item in value.values() if compact_text(item)) + if isinstance(value, list): + return " ".join(compact_text(item) for item in value if compact_text(item)) + return str(value).strip() + + +def contract_required_evidence(spec: dict[str, Any]) -> list[str]: + contract = spec.get("visual_design_contract") + if not isinstance(contract, dict): + return [] + raw = contract.get("required_visual_evidence") + if isinstance(raw, str): + return [raw.strip()] if raw.strip() else [] + if isinstance(raw, list): + return [str(item).strip() for item in raw if str(item).strip()] + return [] + + +def evidence_effects(spec: dict[str, Any], defaults: list[str]) -> list[str]: + out: list[str] = [] + for item in defaults: + if item not in out: + out.append(item) + return out + + +def is_strategist_contract(plan: dict[str, Any]) -> bool: + return text_from_any(plan.get("schema_version")).startswith("svglide-strategist-contract/") + + +def layout_boxes_by_role(spec: dict[str, Any]) -> dict[str, dict[str, int]]: + out: dict[str, dict[str, int]] = {} + boxes = spec.get("layout_boxes") + if not isinstance(boxes, list): + return out + for box in boxes: + if not isinstance(box, dict): + continue + try: + box_data = { + "x": int(float(box.get("x", 0))), + "y": int(float(box.get("y", 0))), + "width": int(float(box.get("width", 0))), + "height": int(float(box.get("height", 0))), + } + except (TypeError, ValueError): + continue + aliases = [ + text_from_any(box.get("id")), + text_from_any(box.get("role")), + text_from_any(box.get("name")), + text_from_any(box.get("layout_box_role")), + ] + for alias in aliases: + key = slug_id(alias).replace("-", "_") + if key and key not in out: + out[key] = box_data + return out + + +def contract_box(boxes: dict[str, dict[str, int]], *roles: str, fallback: tuple[int, int, int, int]) -> dict[str, int]: + for role in roles: + normalized = slug_id(role).replace("-", "_") + if normalized in boxes: + return boxes[normalized] + x, y, width, height = fallback + return {"x": x, "y": y, "width": width, "height": height} + + +def role_text_limit(spec: dict[str, Any], role: str, fallback: int) -> int: + budgets = spec.get("text_budget_by_role") + if not isinstance(budgets, dict): + return fallback + raw = budgets.get(role) + if isinstance(raw, dict): + value = raw.get("max_chars") or raw.get("chars") or raw.get("max_visible_chars") + else: + value = raw + try: + parsed = int(value) + except (TypeError, ValueError): + return fallback + return max(4, parsed) + + +def shorten(text: str, limit: int) -> str: + text = re.sub(r"\s+", " ", text.strip()) + if len(text) <= limit: + return text + if limit <= 1: + return text[:limit] + return text[: max(1, limit - 1)].rstrip() + "…" + + +PROMPT_PREFIX_RE = re.compile( + r"^\s*(cover|kpi\s+dashboard|kpi|roadmap|process|comparison|capability|chart|closing|封面|仪表盘|路线图|流程|对比|能力|图表|收束)\s*[::]\s*", + re.IGNORECASE, +) + + +def strip_prompt_prefix(text: str) -> str: + return PROMPT_PREFIX_RE.sub("", text).strip() + + +def topic_context(spec: dict[str, Any], deck_title: str = "") -> str: + return " ".join( + part + for part in [ + deck_title, + compact_text(spec.get("title")), + compact_text(spec.get("key_message")), + compact_text(spec.get("summary")), + compact_text(spec.get("description")), + compact_text(spec.get("body")), + ] + if part + ).lower() + + +def topic_node_labels(spec: dict[str, Any], deck_title: str = "", *, count: int = 5) -> list[str]: + context = topic_context(spec, deck_title) + if any(token in context for token in ["阿克苏", "绿洲", "胡杨", "四季", "水系", "oasis"]): + labels = ["水系入口", "春配套", "夏活力", "胡杨秋境", "暖廊冬居", "地域纹样"] + elif any(token in context for token in ["低空", "物流", "无人机", "空域", "配送", "drone"]): + labels = ["订单入口", "空域调度", "无人机执行", "末端交付", "安全冗余", "运营回收"] + elif any(token in context for token in ["ai", "capital", "compute", "gpu", "stargate", "openai"]): + labels = ["资本入口", "算力承诺", "模型平台", "收入兑现", "生态回流", "风险折扣"] + else: + labels = ["核心节点", "资源配置", "执行路径", "价值回收", "风险控制", "下一步"] + return labels[:count] + + +def semantic_caption_for_topic(spec: dict[str, Any], deck_title: str = "", *, count: int = 4) -> str: + return " / ".join(topic_node_labels(spec, deck_title, count=count)) + + +def add_semantic_caption(text: str, spec: dict[str, Any], deck_title: str = "", *, max_chars: int = 96) -> str: + caption = semantic_caption_for_topic(spec, deck_title) + if not caption: + return text + if all(label in text for label in caption.split(" / ")): + return text + combined = f"{text}\n{caption}" if text else caption + return shorten(combined, max_chars) + + +def agenda_labels_for_topic(spec: dict[str, Any], deck_title: str = "", *, count: int = 6) -> list[str]: + raw_items = spec.get("items") or spec.get("sections") or spec.get("agenda") + labels: list[str] = [] + if isinstance(raw_items, list): + for item in raw_items: + if isinstance(item, dict): + label_text = text_from_any(item.get("title") or item.get("label") or item.get("name")) + else: + label_text = text_from_any(item) + if label_text: + labels.append(label_text) + if len(labels) >= count: + return labels + context = topic_context(spec, deck_title) + if any(token in context for token in ["阿克苏", "绿洲", "胡杨", "四季", "水系", "oasis"]): + labels = ["核心定位", "春之地块", "夏之地块", "秋之地块", "冬之地块", "价值展望"] + elif any(token in context for token in ["ai", "capital", "compute", "gpu", "stargate", "openai"]): + labels = ["资本入口", "算力承诺", "平台竞合", "财务兑现", "风险折扣", "2026 判断"] + else: + labels = ["问题定义", "核心逻辑", "能力结构", "推进路径", "风险控制", "结论行动"] + return labels[:count] + + +def section_index_for_spec(spec: dict[str, Any], page: int) -> str: + for key in ("section_index", "chapter_index", "index_label", "kicker"): + value = text_from_any(spec.get(key)) + match = re.search(r"\d{1,2}", value) + if match: + return match.group(0).zfill(2) + title = text_from_any(spec.get("title") or spec.get("headline")) + match = re.search(r"\d{1,2}", title) + if match: + return match.group(0).zfill(2) + return str(page).zfill(2) + + +def comparison_rows_for_topic(spec: dict[str, Any], deck_title: str = "") -> list[tuple[str, str, str]]: + context = topic_context(spec, deck_title) + if any(token in context for token in ["阿克苏", "绿洲", "胡杨", "四季", "水系", "oasis"]): + return [ + ("空间", "单点景观割裂", "水系串联四境"), + ("生活", "配套平均铺开", "春夏秋冬差异体验"), + ("文化", "弱地域识别", "胡杨与纹样成为记忆点"), + ] + if any(token in context for token in ["低空", "物流", "无人机", "空域", "配送", "drone"]): + return [ + ("效率", "地面高峰拥堵", "低空走廊直达"), + ("成本", "线性增车扩张", "平台调度摊薄成本"), + ("韧性", "单一路由失效", "空地协同冗余"), + ] + if any(token in context for token in ["ai", "capital", "compute", "gpu", "stargate", "openai"]): + return [ + ("资本", "财务投资分散", "算力绑定资本闭环"), + ("供给", "GPU 采购排队", "云厂商承诺前置"), + ("回报", "估值叙事驱动", "收入兑现重新定价"), + ] + return [ + ("效率", "资源分散推进", "主路径集中突破"), + ("体验", "信息散落难记", "视觉系统承载重点"), + ("风险", "后置发现问题", "前置门禁与回归"), + ] + + +def dashboard_metrics_for_topic(spec: dict[str, Any], deck_title: str = "") -> list[tuple[str, str]]: + context = topic_context(spec, deck_title) + if any(token in context for token in ["阿克苏", "绿洲", "胡杨", "四季", "水系", "oasis"]): + return [("4境", "四季地块"), ("1环", "水系闭环"), ("3核", "价值引擎"), ("全年龄", "人群覆盖")] + if any(token in context for token in ["低空", "物流", "无人机", "空域", "配送", "drone"]): + return [("92%", "准点率"), ("18m", "时效"), ("4.2k", "日单量"), ("-23%", "单均成本")] + if any(token in context for token in ["ai", "capital", "compute", "gpu", "stargate", "openai"]): + return [("$122B", "融资规模"), ("10GW", "算力承诺"), ("4家", "核心投资人"), ("2026", "资本窗口")] + return [("4", "关键抓手"), ("1", "主路径"), ("3", "价值层"), ("90d", "推进节奏")] + + +def contract_text( + spec: dict[str, Any], + box_id: str, + box: dict[str, int], + text: str, + *, + role: str, + size: int, + weight: int = 700, + color: str = "#111827", + align: str = "left", +) -> str: + limit = role_text_limit(spec, role, 34 if role == "title" else 120) + return text_box( + box_id, + box["x"], + box["y"], + box["width"], + box["height"], + shorten(text, limit), + size=size, + weight=weight, + color=color, + align=align, + family=SANS, + ) + + +def slide_anchor_role(spec: dict[str, Any]) -> str: + anchor = spec.get("main_visual_anchor") + if isinstance(anchor, dict): + role = text_from_any(anchor.get("layout_box_role") or anchor.get("role")) + if role: + return role + return "visual" + + +def contract_summary(spec: dict[str, Any], fallback: str) -> str: + return strip_prompt_prefix(first_text(spec, ["key_message", "summary", "description", "subtitle", "body"], fallback)) + + +def contract_title(spec: dict[str, Any], kind: str, deck_title: str) -> str: + title = first_text(spec, ["title", "headline", "name"], "") + if title: + return strip_prompt_prefix(title) + title_by_type = { + "cover": deck_title, + "kpi_overview": "运营指标总览", + "roadmap": "阶段推进路线", + "process_flow": "端到端运行链路", + "comparison": "方案价值对照", + "capability_map": "平台能力图谱", + "chart_takeaway": "关键场景变化", + "closing": "下一步行动建议", + } + page_type = text_from_any(spec.get("page_type")) + if page_type in title_by_type: + return title_by_type[page_type] + page_type = text_from_any(spec.get("page_type")).replace("_", " ").title() + return strip_prompt_prefix(page_type or kind.replace("_", " ").title() or deck_title) + + +def contract_background(page: int, accent: str) -> list[str]: + return [ + rect("background", 0, 0, W, H, "#F8FAFC"), + rect("accent-page-wash", 720, 0, 240, H, accent, rx=0, opacity=0.085), + rect("accent-wash-left", 48, 44, 10, 436, accent, rx=0, opacity=0.26), + rect("accent-wash-bottom", 48, 476, 864, 4, accent, rx=0, opacity=0.18), + rect("top-rule", 48, 32, 864, 4, accent, rx=0), + ] + + +def contract_footer(page: int, spec: dict[str, Any], boxes: dict[str, dict[str, int]]) -> str: + footer = contract_box(boxes, "footer", fallback=(64, 500, 832, 18)) + footer_label = first_text(spec, ["footer", "source_note", "visible_source_note", "_deck_title"], "Page") + return contract_text(spec, "footer", footer, f"{footer_label} · {page:02d}", role="footer", size=9, weight=600, color="#64748B", align="center") + + +def visual_box_for_contract(spec: dict[str, Any], boxes: dict[str, dict[str, int]], fallback: tuple[int, int, int, int]) -> dict[str, int]: + anchor = slide_anchor_role(spec) + return contract_box(boxes, anchor, "visual", "chart", "flow", "table", "grid", "body", fallback=fallback) + + +def render_contract_cover(page: int, accent: str, spec: dict[str, Any], boxes: dict[str, dict[str, int]], title: str, summary: str, report: ComponentReport, source_trace: str, asset_id: str, deck_title: str) -> list[str]: + title_box = contract_box(boxes, "title", fallback=(72, 150, 560, 120)) + body_box = contract_box(boxes, "body", fallback=(76, 284, 520, 72)) + visual = visual_box_for_contract(spec, boxes, (600, 84, 288, 360)) + body_copy = add_semantic_caption(summary, spec, deck_title, max_chars=role_text_limit(spec, "body", 96)) + body = [ + *contract_background(page, accent), + rect("cover-title-field", 64, 132, 584, 160, accent, rx=0, opacity=0.055), + rect("cover-map-field", 592, 72, 308, 386, "#DBEAFE", rx=0, opacity=0.88), + rect("cover-coordinate-stack-1", 616, 112, 236, 1, accent, rx=0, opacity=0.16), + rect("cover-coordinate-stack-2", 616, 184, 236, 1, accent, rx=0, opacity=0.16), + rect("cover-coordinate-stack-3", 616, 256, 236, 1, accent, rx=0, opacity=0.16), + rect("cover-coordinate-stack-4", 616, 328, 236, 1, accent, rx=0, opacity=0.16), + path("cover-route-ribbon", "M606 414 C658 298 698 250 750 192 C796 142 836 116 886 96 L886 134 C826 162 800 192 766 236 C714 304 674 358 638 430 Z", fill=accent, opacity=0.18), + rect("visual-anchor-panel", visual["x"], visual["y"], visual["width"], visual["height"], "#E0F2FE", rx=0, opacity=0.96), + rect("visual-anchor-side-band", visual["x"] + visual["width"] - 42, visual["y"], 42, visual["height"], accent, rx=0, opacity=0.24), + rect("visual-grid-a", visual["x"] + 24, visual["y"] + 48, visual["width"] - 72, 1, accent, rx=0, opacity=0.14), + rect("visual-grid-b", visual["x"] + 24, visual["y"] + 148, visual["width"] - 72, 1, accent, rx=0, opacity=0.14), + rect("visual-grid-c", visual["x"] + 24, visual["y"] + 248, visual["width"] - 72, 1, accent, rx=0, opacity=0.14), + path("visual-route-shadow", f"M{visual['x'] + 10} {visual['y'] + visual['height'] - 28} C{visual['x'] + 86} {visual['y'] + 48} {visual['x'] + 158} {visual['y'] + visual['height'] - 106} {visual['x'] + visual['width'] - 6} {visual['y'] + 34}", stroke="#94A3B8", **{"stroke-width": 24, "stroke-linecap": "round", "opacity": 0.20}), + path("visual-route-path", f"M{visual['x'] + 24} {visual['y'] + visual['height'] - 42} C{visual['x'] + 96} {visual['y'] + 30} {visual['x'] + 172} {visual['y'] + visual['height'] - 80} {visual['x'] + visual['width'] - 24} {visual['y'] + 52}", stroke=accent, **{"stroke-width": 10, "stroke-linecap": "round", "opacity": 0.92}), + rect("visual-node-a", visual["x"] + 38, visual["y"] + visual["height"] - 66, 28, 28, "#FFFFFF", rx=0, stroke=accent, **{"stroke-width": 4}), + rect("visual-node-b", visual["x"] + visual["width"] - 58, visual["y"] + 40, 28, 28, "#FFFFFF", rx=0, stroke=accent, **{"stroke-width": 4}), + contract_text(spec, "title", title_box, title, role="title", size=38, weight=900, color="#0F172A"), + contract_text(spec, "body", body_box, body_copy, role="body", size=18, weight=650, color="#334155"), + contract_footer(page, spec, boxes), + ] + report.add(page, "contract-cover", "contract.cover", bbox(visual["x"], visual["y"], visual["width"], visual["height"]), ["path", "geometric_shape", "typography"], effects=evidence_effects(spec, ["path", "typography", "full_page_archetype", "hero_route", "title_field"]), source_trace=source_trace, asset_id=asset_id) + return body + + +def render_contract_agenda(page: int, accent: str, spec: dict[str, Any], boxes: dict[str, dict[str, int]], title: str, report: ComponentReport, source_trace: str, asset_id: str) -> list[str]: + title_box = contract_box(boxes, "title", fallback=(64, 58, 624, 58)) + timeline = contract_box(boxes, "timeline", fallback=(110, 148, 690, 250)) + body_box = contract_box(boxes, "body", fallback=(110, 414, 690, 48)) + visual = contract_box(boxes, "visual", fallback=(732, 92, 128, 348)) + body_text_y = max(body_box["y"] + 14, title_box["y"] + title_box["height"] + 30) + body_text_box = { + "x": body_box["x"], + "y": body_text_y, + "width": body_box["width"], + "height": max(40, body_box["y"] + body_box["height"] - body_text_y), + } + labels = agenda_labels_for_topic(spec, text_from_any(spec.get("_deck_title")), count=6) + backplane_x = min(timeline["x"], body_box["x"]) - 18 + backplane_y = max(min(timeline["y"], body_box["y"]) - 18, title_box["y"] + title_box["height"] + 26) + backplane_right = max(timeline["x"] + timeline["width"], body_box["x"] + body_box["width"]) + 18 + backplane_bottom = max(timeline["y"] + timeline["height"], body_box["y"] + body_box["height"]) + 18 + body = [ + *contract_background(page, accent), + rect("agenda-route-backplane", backplane_x, backplane_y, backplane_right - backplane_x, backplane_bottom - backplane_y, "#FFFFFF", rx=0, stroke="#D8E2EE", **{"stroke-width": 1, "opacity": 0.96}), + rect("agenda-side-index-field", visual["x"], visual["y"], visual["width"], visual["height"], accent, rx=0, opacity=0.10), + contract_text(spec, "title", title_box, title, role="title", size=24, weight=900, color="#0F172A"), + ] + y_step = max(34, timeline["height"] // max(1, len(labels))) + points: list[tuple[int, int]] = [] + for index, _label_text in enumerate(labels, 1): + x = timeline["x"] + timeline["width"] // 2 + (8 if index % 2 else -8) + y = timeline["y"] + 20 + (index - 1) * y_step + points.append((x, y)) + body.append(rect(f"agenda-number-{index}", x - 13, y - 13, 26, 26, "#FFFFFF", rx=0, stroke=accent, **{"stroke-width": 2})) + if points: + route_d = "M" + " L".join(f"{x} {y}" for x, y in points) + body.append(path("agenda-route-shadow", route_d, stroke="#94A3B8", **{"stroke-width": 12, "stroke-linecap": "round", "opacity": 0.12})) + body.append(path("agenda-route-path", route_d, stroke=accent, **{"stroke-width": 4, "stroke-linecap": "round", "opacity": 0.72})) + body.extend( + [ + rect("agenda-visual-mark", visual["x"], visual["y"], visual["width"], visual["height"], accent, rx=0, opacity=0.36), + contract_text(spec, "body", body_text_box, " / ".join(f"{index:02d} {label}" for index, label in enumerate(labels, 1)), role="body", size=17, weight=750, color="#0F172A"), + contract_footer(page, spec, boxes), + ] + ) + report.add(page, "contract-agenda", "contract.agenda", bbox(backplane_x, backplane_y, backplane_right - backplane_x, backplane_bottom - backplane_y), ["path", "geometric_shape", "typography"], effects=evidence_effects(spec, ["numbered_path", "section_index", "semantic_labels", "connector_flow"]), source_trace=source_trace, asset_id=asset_id) + return body + + +def render_contract_section(page: int, accent: str, spec: dict[str, Any], boxes: dict[str, dict[str, int]], title: str, summary: str, report: ComponentReport, source_trace: str, asset_id: str, deck_title: str) -> list[str]: + title_box = contract_box(boxes, "title", fallback=(82, 164, 620, 104)) + body_box = contract_box(boxes, "body", fallback=(86, 300, 560, 58)) + visual = visual_box_for_contract(spec, boxes, (652, 80, 214, 356)) + section_index = section_index_for_spec(spec, page) + hero_width = min(220, max(120, visual["width"] // 3)) + hero_x = max(48, min(visual["x"] + visual["width"] - hero_width - 48, 912 - hero_width)) + hero_y = max(visual["y"] + 18, min(visual["y"] + 34, 414)) + body_copy = add_semantic_caption(summary, spec, deck_title, max_chars=role_text_limit(spec, "body", 60)) + body = [ + *contract_background(page, accent), + rect("section-signal-field", 48, 66, 864, 386, "#FFFFFF", rx=0, stroke="#D8E2EE", **{"stroke-width": 1, "opacity": 0.95}), + rect("section-index-rail", visual["x"], visual["y"], visual["width"], visual["height"], accent, rx=0, opacity=0.13), + rect("section-motif-block", visual["x"] + 64, visual["y"] + 42, max(80, visual["width"] - 128), 16, accent, rx=0, opacity=0.26), + rect("section-hero-number", hero_x, hero_y, hero_width, 92, accent, rx=0, opacity=0.18), + rect("section-hero-number-mark", hero_x + 18, hero_y + 22, hero_width - 36, 12, accent, rx=0, opacity=0.62), + contract_text(spec, "title", title_box, title, role="title", size=32, weight=900, color="#0F172A"), + contract_text(spec, "body", body_box, body_copy, role="body", size=15, weight=650, color="#334155"), + rect("section-bottom-rule", 82, 394, 520, 5, accent, rx=0, opacity=0.58), + contract_footer(page, spec, boxes), + ] + report.add(page, "contract-section", "contract.section", bbox(visual["x"], visual["y"], visual["width"], visual["height"]), ["path", "geometric_shape", "typography"], effects=evidence_effects(spec, ["section_index", "hero_signal", "full_page_archetype"]), source_trace=source_trace, asset_id=asset_id) + return body + + +def render_contract_dashboard(page: int, accent: str, spec: dict[str, Any], boxes: dict[str, dict[str, int]], title: str, summary: str, report: ComponentReport, source_trace: str, asset_id: str) -> list[str]: + title_box = contract_box(boxes, "title", fallback=(48, 34, 864, 48)) + metric = contract_box(boxes, "metric", fallback=(64, 106, 260, 128)) + grid = contract_box(boxes, "grid", fallback=(348, 106, 548, 128)) + chart = contract_box(boxes, "chart", fallback=(64, 258, 832, 150)) + body_box = contract_box(boxes, "body", fallback=(64, 426, 832, 56)) + body = [*contract_background(page, accent), contract_text(spec, "title", title_box, title, role="title", size=22, weight=900)] + metrics = dashboard_metrics_for_topic(spec, text_from_any(spec.get("_deck_title"))) + body.append(rect("metric-hero-card", metric["x"], metric["y"], metric["width"], metric["height"], "#E0F2FE", rx=0, stroke=accent, **{"stroke-width": 2})) + body.append(contract_text(spec, "metric", {"x": metric["x"] + 18, "y": metric["y"] + 20, "width": metric["width"] - 36, "height": 78}, f"{metrics[0][0]}\n{metrics[0][1]}", role="metric", size=27, weight=900, color=accent)) + for index, (value, label_text) in enumerate(metrics[1:], 1): + x = grid["x"] + (index - 1) * max(1, grid["width"] // 3) + w = max(92, grid["width"] // 3 - 14) + body.append(rect(f"dashboard-card-{index}", x, grid["y"] + 10, w, 96, "#FFFFFF", rx=0, stroke="#CBD5E1", **{"stroke-width": 1})) + body.append(rect(f"dashboard-card-cap-{index}", x, grid["y"] + 10, w, 8, accent, rx=0, opacity=0.42)) + body.append(text_box(f"metric-card-value-{index}", x + 16, grid["y"] + 25, w - 32, 28, value, size=22, weight=900, color="#0F172A")) + body.append(text_box(f"metric-card-name-{index}", x + 16, grid["y"] + 53, w - 32, 18, label_text, size=9, weight=800, color="#475569")) + body.append(rect(f"kpi-mini-bar-{index}", x + 16, grid["y"] + 78, w - 32, 14 + index * 5, accent, rx=0, opacity=0.72)) + body.append(line("chart-axis", chart["x"] + 70, chart["y"] + chart["height"] - 18, chart["x"] + chart["width"] - 70, chart["y"] + chart["height"] - 18, "#CBD5E1", **{"stroke-width": 1})) + for grid_index in range(3): + grid_y = chart["y"] + 22 + grid_index * 38 + body.append(line(f"chart-grid-{grid_index}", chart["x"] + 70, grid_y, chart["x"] + chart["width"] - 70, grid_y, "#E2E8F0", **{"stroke-width": 1, "opacity": 0.7})) + for index, height in enumerate((48, 78, 104, 64), 1): + x = chart["x"] + 80 + index * 120 + body.append(rect(f"chart-bar-{index}", x, chart["y"] + chart["height"] - height - 18, 56, height, accent, rx=0, opacity=0.8)) + body_text = {"x": body_box["x"], "y": body_box["y"], "width": body_box["width"], "height": min(42, body_box["height"])} + metric_line = " / ".join(f"{label}{value}" for value, label in metrics) + dashboard_copy = f"{summary} / {metric_line}" if summary else metric_line + body.extend( + [ + contract_text(spec, "body", body_text, dashboard_copy, role="body", size=13, weight=600, color="#334155"), + contract_footer(page, spec, boxes), + ] + ) + report.add(page, "contract-dashboard", "contract.dashboard", bbox(chart["x"], chart["y"], chart["width"], chart["height"]), ["geometric_shape", "typography", "micro_chart", "dashboard"], effects=evidence_effects(spec, ["chart_geometry", "typography", "metric_hierarchy", "dashboard_grid"]), source_trace=source_trace, asset_id=asset_id) + return body + + +def render_contract_flow(page: int, accent: str, spec: dict[str, Any], boxes: dict[str, dict[str, int]], title: str, summary: str, report: ComponentReport, source_trace: str, asset_id: str) -> list[str]: + title_box = contract_box(boxes, "title", fallback=(64, 46, 680, 52)) + flow = visual_box_for_contract(spec, boxes, (96, 160, 768, 210)) + body_box = contract_box(boxes, "body", fallback=(96, 380, 768, 48)) + callout_box = contract_box(boxes, "callout", "note", fallback=(flow["x"] + flow["width"] - 260, flow["y"] + flow["height"] + 22, 250, 48)) + body = [ + *contract_background(page, accent), + contract_text(spec, "title", title_box, title, role="title", size=22, weight=900), + rect("flow-backplane", flow["x"], flow["y"] - 18, flow["width"], flow["height"] + 84, "#FFFFFF", rx=0, stroke="#D8E2EE", **{"stroke-width": 1, "opacity": 0.96}), + rect("flow-lane-upper", flow["x"] + 24, flow["y"] + 22, flow["width"] - 48, 44, accent, rx=0, opacity=0.11), + rect("flow-lane-lower", flow["x"] + 24, flow["y"] + flow["height"] - 78, flow["width"] - 48, 44, accent, rx=0, opacity=0.08), + ] + y = flow["y"] + flow["height"] // 2 + start_x = flow["x"] + 36 + step_gap = max(90, (flow["width"] - 72) // 4) + points = [] + for index in range(5): + x = start_x + index * step_gap + points.append((x, y + (index % 2) * 26 - 13)) + path_d = "M" + " L".join(f"{x} {node_y}" for x, node_y in points) + body.append(rect("flow-zone-band", flow["x"], flow["y"] + flow["height"] // 2 - 34, flow["width"], 68, accent, rx=0, opacity=0.13)) + body.append(path("flow-route-shadow", path_d, stroke="#94A3B8", **{"stroke-width": 20, "stroke-linecap": "round", "opacity": 0.18})) + body.append(path("flow-route-path", path_d, stroke=accent, **{"stroke-width": 9, "stroke-linecap": "round", "opacity": 0.88})) + labels = topic_node_labels(spec, text_from_any(spec.get("_deck_title")), count=5) + for index, (x, node_y) in enumerate(points, 1): + body.append(rect(f"flow-stage-panel-{index}", x - 30, node_y - 22, 60, 44, "#FFFFFF", rx=0, stroke=accent, **{"stroke-width": 1, "opacity": 0.82})) + body.append(rect(f"flow-tick-{index}", x - 5, node_y - 5, 10, 10, accent, rx=0, opacity=0.88)) + body_text = {"x": body_box["x"], "y": body_box["y"], "width": body_box["width"], "height": min(46, body_box["height"])} + callout_text = {"x": callout_box["x"], "y": callout_box["y"], "width": callout_box["width"], "height": min(46, callout_box["height"])} + body_copy = f"输入:{labels[0]} / {labels[1]} / {labels[2]}" if body_box["width"] < 220 else summary + body.extend( + [ + contract_text(spec, "body", body_text, body_copy, role="body", size=13, weight=600, color="#334155"), + contract_text(spec, "callout", callout_text, " / ".join(labels), role="callout", size=12, weight=800, color=accent), + contract_footer(page, spec, boxes), + ] + ) + report.add(page, "contract-flow", "contract.flow", bbox(flow["x"], flow["y"], flow["width"], flow["height"]), ["path", "annotation", "typography"], effects=evidence_effects(spec, ["path", "connector_flow", "typography", "flow_lanes", "phase_spine", "full_page_archetype"]), source_trace=source_trace, asset_id=asset_id) + return body + + +def render_contract_comparison(page: int, accent: str, spec: dict[str, Any], boxes: dict[str, dict[str, int]], title: str, report: ComponentReport, source_trace: str, asset_id: str) -> list[str]: + title_box = contract_box(boxes, "title", fallback=(64, 48, 720, 54)) + table = contract_box(boxes, "table", fallback=(84, 204, 792, 190)) + left_panel = contract_box(boxes, "left_panel", "left-panel", fallback=(72, 136, 384, 300)) + right_panel = contract_box(boxes, "right_panel", "right-panel", fallback=(504, 136, 384, 300)) + rows_data = comparison_rows_for_topic(spec, text_from_any(spec.get("_deck_title"))) + body = [ + *contract_background(page, accent), + contract_text(spec, "title", title_box, title, role="title", size=22, weight=900), + rect("comparison-left-panel", left_panel["x"], left_panel["y"], left_panel["width"], left_panel["height"], "#EEF2FF", rx=0, opacity=0.72), + rect("comparison-right-panel", right_panel["x"], right_panel["y"], right_panel["width"], right_panel["height"], "#ECFDF5", rx=0, opacity=0.72), + rect("comparison-table-frame", table["x"], table["y"], table["width"], table["height"], "#FFFFFF", rx=0, stroke=accent, **{"stroke-width": 2}), + ] + rows = 3 + cols = 2 + for row in range(1, rows): + y = table["y"] + row * table["height"] // rows + body.append(rect(f"comparison-row-band-{row}", table["x"], y - 2, table["width"], 4, accent, rx=0, opacity=0.12)) + body.append(line(f"comparison-row-{row}", table["x"], y, table["x"] + table["width"], y, "#CBD5E1", **{"stroke-width": 1})) + body.append(line("comparison-mid-line", table["x"] + table["width"] // 2, table["y"], table["x"] + table["width"] // 2, table["y"] + table["height"], "#CBD5E1", **{"stroke-width": 1})) + for index, (row_label, left_copy, right_copy) in enumerate(rows_data, 1): + row_y = table["y"] + (index - 1) * table["height"] // rows + body.append(text_box(f"comparison-dimension-{index}", table["x"] + 14, row_y + 12, 62, 22, row_label, size=11, weight=900, color=accent)) + body.append(text_box(f"comparison-left-value-{index}", table["x"] + 88, row_y + 14, table["width"] // 2 - 112, 30, left_copy, size=12, weight=700, color="#334155")) + body.append(text_box(f"comparison-right-value-{index}", table["x"] + table["width"] // 2 + 28, row_y + 14, table["width"] // 2 - 60, 30, right_copy, size=12, weight=800, color="#0F172A")) + accent_path = f"M{table['x']} {table['y'] - 24} L{table['x'] + 132} {table['y'] - 24} L{table['x'] + 148} {table['y'] - 16}" + body.extend([path("comparison-accent-path", accent_path, stroke=accent, **{"stroke-width": 3, "opacity": 0.54}), contract_footer(page, spec, boxes)]) + report.add(page, "contract-comparison", "contract.comparison", bbox(table["x"], table["y"], table["width"], table["height"]), ["path", "geometric_shape", "typography"], effects=evidence_effects(spec, ["path", "typography", "decision_matrix", "contrast_panels", "semantic_labels"]), source_trace=source_trace, asset_id=asset_id) + return body + + +def render_contract_hub(page: int, accent: str, spec: dict[str, Any], boxes: dict[str, dict[str, int]], title: str, summary: str, report: ComponentReport, source_trace: str, asset_id: str) -> list[str]: + title_box = contract_box(boxes, "title", fallback=(64, 46, 660, 52)) + visual = visual_box_for_contract(spec, boxes, (92, 132, 776, 300)) + legend = contract_box(boxes, "legend", "body", fallback=(126, 454, 708, 26)) + cx = visual["x"] + visual["width"] // 2 + cy = visual["y"] + visual["height"] // 2 + radius = min(118, max(70, min(visual["width"], visual["height"]) // 3)) + labels = topic_node_labels(spec, text_from_any(spec.get("_deck_title")), count=5) + body = [ + *contract_background(page, accent), + contract_text(spec, "title", title_box, title, role="title", size=22, weight=900), + rect("hub-backplane", visual["x"], visual["y"], visual["width"], visual["height"], "#F8FAFC", rx=0, stroke="#D8E2EE", **{"stroke-width": 1}), + path("hub-sector-1", f"M{cx} {cy} L{cx + radius + 58} {cy - 82} L{cx + radius + 80} {cy + 28} Z", fill=accent, opacity=0.12), + path("hub-sector-2", f"M{cx} {cy} L{cx - radius - 74} {cy - 52} L{cx - radius - 38} {cy + 92} Z", fill=accent, opacity=0.09), + rect("hub-satellite-panel-1", visual["x"] + 36, visual["y"] + 34, 138, 46, "#FFFFFF", rx=0, stroke="#D8E2EE", **{"stroke-width": 1, "opacity": 0.92}), + rect("hub-satellite-panel-2", visual["x"] + visual["width"] - 174, visual["y"] + visual["height"] - 80, 138, 46, "#FFFFFF", rx=0, stroke="#D8E2EE", **{"stroke-width": 1, "opacity": 0.92}), + rect("capability-orbit-outer", cx - radius - 42, cy - radius - 42, (radius + 42) * 2, (radius + 42) * 2, "none", rx=0, stroke=accent, **{"stroke-width": 2, "opacity": 0.18}), + rect("capability-orbit-inner", cx - radius + 8, cy - radius + 8, (radius - 8) * 2, (radius - 8) * 2, "none", rx=0, stroke=accent, **{"stroke-width": 2, "opacity": 0.16}), + rect("capability-center", cx - 42, cy - 42, 84, 84, "#FFFFFF", rx=0, stroke=accent, **{"stroke-width": 4}), + rect("capability-center-glow", cx - 66, cy - 66, 132, 132, accent, rx=0, opacity=0.08), + ] + for index, angle in enumerate((0, 72, 144, 216, 288), 1): + radians = math.radians(angle) + x = int(cx + math.cos(radians) * radius) + y = int(cy + math.sin(radians) * radius) + body.append(line(f"capability-spoke-{index}", cx, cy, x, y, accent, **{"stroke-width": 2, "opacity": 0.62})) + body.append(circle(f"capability-icon-node-{index}", x, y, 20, "#E0F2FE")) + body.append(circle(f"capability-icon-dot-{index}", x, y, 6, accent, opacity=0.72)) + legend_copy = " / ".join(labels) + body.extend([contract_text(spec, "legend", legend, legend_copy, role="body", size=12, weight=600, color="#334155", align="center"), contract_footer(page, spec, boxes)]) + hub_box_x = min(visual["x"], legend["x"]) + hub_box_y = min(visual["y"], legend["y"]) + hub_box_right = max(visual["x"] + visual["width"], legend["x"] + legend["width"]) + hub_box_bottom = max(visual["y"] + visual["height"], legend["y"] + legend["height"]) + report.add(page, "contract-hub", "contract.hub", bbox(hub_box_x, hub_box_y, hub_box_right - hub_box_x, hub_box_bottom - hub_box_y), ["geometric_shape", "connector", "typography", "icon"], effects=evidence_effects(spec, ["connector_flow", "typography", "hub_spoke", "sector_field", "semantic_labels", "full_page_archetype"]), source_trace=source_trace, asset_id=asset_id) + return body + + +def render_contract_bar(page: int, accent: str, spec: dict[str, Any], boxes: dict[str, dict[str, int]], title: str, summary: str, report: ComponentReport, source_trace: str, asset_id: str) -> list[str]: + title_box = contract_box(boxes, "title", fallback=(64, 42, 680, 48)) + takeaway = contract_box(boxes, "takeaway", "body", fallback=(66, 94, 660, 34)) + chart = contract_box(boxes, "chart", fallback=(86, 154, 650, 296)) + callout = contract_box(boxes, "callout", "annotation", fallback=(748, 168, 150, 168)) + labels = topic_node_labels(spec, text_from_any(spec.get("_deck_title")), count=4) + body = [ + *contract_background(page, accent), + contract_text(spec, "title", title_box, title, role="title", size=22, weight=900), + contract_text(spec, "body", takeaway, summary, role="body", size=12, weight=600, color="#334155"), + rect("bar-plot-backplane", chart["x"] - 18, chart["y"] - 20, chart["width"] + 38, chart["height"] + 34, "#FFFFFF", rx=0, stroke="#D8E2EE", **{"stroke-width": 1}), + rect("bar-insight-strip", callout["x"] - 18, callout["y"] - 20, callout["width"] + 36, callout["height"] + 40, accent, rx=0, opacity=0.11), + path("bar-variance-path", f"M{chart['x'] + 76} {chart['y'] + 210} C{chart['x'] + 210} {chart['y'] + 148} {chart['x'] + 350} {chart['y'] + 180} {chart['x'] + 528} {chart['y'] + 86}", stroke=accent, **{"stroke-width": 5, "stroke-linecap": "round", "opacity": 0.28}), + ] + for grid_index in range(4): + grid_y = chart["y"] + 34 + grid_index * 46 + body.append(line(f"bar-grid-{grid_index}", chart["x"], grid_y, chart["x"] + chart["width"], grid_y, "#E2E8F0", **{"stroke-width": 1, "opacity": 0.72})) + body.append(line("bar-axis-x", chart["x"], chart["y"] + chart["height"] - 24, chart["x"] + chart["width"], chart["y"] + chart["height"] - 24, "#94A3B8", **{"stroke-width": 1})) + for index, value in enumerate((0.42, 0.62, 0.78, 0.55), 1): + bar_h = int((chart["height"] - 70) * value) + x = chart["x"] + 70 + index * 90 + y = chart["y"] + chart["height"] - 24 - bar_h + body.append(rect(f"chart-bar-shadow-{index}", x + 8, y + 8, 52, bar_h, "#94A3B8", rx=0, opacity=0.12)) + body.append(rect(f"chart-bar-{index}", x, y, 52, bar_h, accent, rx=0, opacity=0.82)) + body.extend([contract_text(spec, "callout", callout, " / ".join(labels[:3]), role="callout", size=12, weight=800, color=accent, align="center"), contract_footer(page, spec, boxes)]) + report.add(page, "contract-bar", "contract.bar", bbox(chart["x"], chart["y"], chart["width"], chart["height"]), ["geometric_shape", "typography", "micro_chart"], effects=evidence_effects(spec, ["chart_geometry", "typography", "insight_strip", "full_page_archetype"]), source_trace=source_trace, asset_id=asset_id) + return body + + +def render_contract_closing(page: int, accent: str, spec: dict[str, Any], boxes: dict[str, dict[str, int]], title: str, summary: str, report: ComponentReport, source_trace: str, asset_id: str) -> list[str]: + title_box = contract_box(boxes, "title", fallback=(72, 96, 620, 104)) + body_box = contract_box(boxes, "body", fallback=(76, 240, 520, 96)) + cta = contract_box(boxes, "callout", "cta", fallback=(628, 248, 236, 88)) + steps = ["双走廊", "三场景", "一中台"] + body = [ + *contract_background(page, accent), + rect("closing-backplane", 64, 74, 832, 374, "#FFFFFF", rx=0, stroke="#D8E2EE", **{"stroke-width": 1, "opacity": 0.94}), + path("closing-route-ribbon", "M84 392 C220 356 348 414 496 382 C622 354 730 386 862 338 L862 372 C724 412 624 382 504 410 C348 444 218 386 84 422 Z", fill=accent, opacity=0.10), + path("closing-horizon-path", f"M76 390 C210 360 360 420 500 386 C640 348 760 408 864 360", stroke=accent, **{"stroke-width": 11, "stroke-linecap": "round", "opacity": 0.24}), + rect("spotlight-callout-panel", cta["x"], cta["y"], cta["width"], cta["height"], "#E0F2FE", rx=0, stroke=accent, **{"stroke-width": 3}), + rect("spotlight-callout-accent", cta["x"], cta["y"], 10, cta["height"], accent, rx=0, opacity=0.66), + contract_text(spec, "title", title_box, title, role="title", size=28, weight=900), + contract_text(spec, "body", body_box, summary, role="body", size=15, weight=650, color="#334155"), + contract_text(spec, "callout", cta, "双走廊 / 三场景 / 一中台", role="callout", size=14, weight=900, color=accent, align="center"), + contract_footer(page, spec, boxes), + ] + for index, step in enumerate(steps, 1): + x = 76 + (index - 1) * 166 + body.append(rect(f"closing-step-card-{index}", x, 356, 138, 52, "#FFFFFF", rx=0, stroke="#CBD5E1", **{"stroke-width": 1})) + body.append(rect(f"closing-step-index-{index}", x, 356, 28, 52, accent, rx=0, opacity=0.18)) + report.add(page, "contract-closing", "contract.closing", bbox(64, 74, 832, 374), ["path", "geometric_shape", "typography", "annotation"], effects=evidence_effects(spec, ["spotlight", "typography", "closing_ribbon", "action_cards", "full_page_archetype"]), source_trace=source_trace, asset_id=asset_id) + return body + + +def render_contract_slide(page: int, kind: str, title: str, summary: str, asset_id: str, accent: str, spec: dict[str, Any], report: ComponentReport, deck_title: str) -> str: + spec = dict(spec) + spec["_deck_title"] = deck_title + boxes = layout_boxes_by_role(spec) + page_type = text_from_any(spec.get("page_type")) + source_trace = asset_id or f"svglide-contract:{page_type or kind}" + title = contract_title(spec, kind, deck_title) + summary = contract_summary(spec, summary) + if page_type == "cover" or kind == "cover": + body = render_contract_cover(page, accent, spec, boxes, title, summary, report, source_trace, asset_id, deck_title) + elif page_type == "agenda" or kind == "agenda": + body = render_contract_agenda(page, accent, spec, boxes, title, report, source_trace, asset_id) + elif page_type == "section_divider" or kind == "section": + body = render_contract_section(page, accent, spec, boxes, title, summary, report, source_trace, asset_id, deck_title) + elif page_type == "kpi_overview" or kind == "kpi_cards": + body = render_contract_dashboard(page, accent, spec, boxes, title, summary, report, source_trace, asset_id) + elif page_type in {"roadmap", "process_flow"} or kind in {"timeline", "process_flow"}: + body = render_contract_flow(page, accent, spec, boxes, title, summary, report, source_trace, asset_id) + elif page_type == "comparison" or kind == "comparison_table": + body = render_contract_comparison(page, accent, spec, boxes, title, report, source_trace, asset_id) + elif page_type == "capability_map" or kind == "hub_spoke": + body = render_contract_hub(page, accent, spec, boxes, title, summary, report, source_trace, asset_id) + elif page_type == "chart_takeaway" or kind == "bar_chart": + body = render_contract_bar(page, accent, spec, boxes, title, summary, report, source_trace, asset_id) + elif page_type == "closing" or kind == "closing": + body = render_contract_closing(page, accent, spec, boxes, title, summary, report, source_trace, asset_id) + else: + body = render_contract_flow(page, accent, spec, boxes, title, summary, report, source_trace, asset_id) + return svg_wrap(page, accent, body) + + +def label(prefix: str, x: int, y: int, width: int, text: str, *, color: str = MUTED) -> str: + return text_box(prefix, x, y, width, 18, text, size=9, weight=700, color=color) + + +def stat(prefix: str, x: int, y: int, value: str, caption: str, accent: str) -> list[str]: + return [ + text_box(f"{prefix}-value", x, y, 150, 40, value, size=28, weight=900, color=accent), + text_box(f"{prefix}-caption", x, y + 42, 150, 18, caption, size=8, weight=700, color=MUTED), + ] + + +def merged_spec(spec: dict[str, Any] | None) -> dict[str, Any]: + if not isinstance(spec, dict): + return {} + out: dict[str, Any] = {} + visual_plan = spec.get("visual_plan") + if isinstance(visual_plan, dict): + out.update(visual_plan) + out.update(spec) + return out + + +def spec_text(spec: dict[str, Any], keys: list[str], fallback: str = "") -> str: + for key in keys: + value = text_from_any(spec.get(key)) + if value: + return value + return fallback + + +def spec_dict(spec: dict[str, Any], keys: list[str]) -> dict[str, Any]: + for key in keys: + value = spec.get(key) + if isinstance(value, dict): + return value + return {} + + +def spec_list(spec: dict[str, Any], keys: list[str]) -> list[Any]: + for key in keys: + value = spec.get(key) + if isinstance(value, list): + return value + return [] + + +def clean_items(items: list[Any], limit: int) -> list[dict[str, Any]]: + out: list[dict[str, Any]] = [] + for item in items: + if isinstance(item, dict): + out.append(item) + elif isinstance(item, str) and item.strip(): + out.append({"label": item.strip()}) + if len(out) >= limit: + break + return out + + +def item_text(item: dict[str, Any], keys: list[str], fallback: str = "") -> str: + for key in keys: + value = text_from_any(item.get(key)) + if value: + return value + return fallback + + +def item_color(item: dict[str, Any], fallback: str) -> str: + color = text_from_any(item.get("color") or item.get("accent") or item.get("fill")) + if re.fullmatch(r"#[0-9A-Fa-f]{6}", color): + return color.upper() + return fallback + + +def slug_id(value: str) -> str: + slug = re.sub(r"[^0-9A-Za-z]+", "-", value.strip().lower()).strip("-") + return slug or "item" + + +def split_hero_title(title: str) -> str: + title = title.strip() or "Untitled" + if "\n" in title: + return title + if "·" in title: + return title.replace("·", "·\n", 1) + words = title.split() + if len(words) >= 4: + midpoint = len(words) // 2 + return " ".join(words[:midpoint]) + "\n" + " ".join(words[midpoint:]) + if len(title) > 14: + midpoint = len(title) // 2 + return title[:midpoint] + "\n" + title[midpoint:] + return title + + +def metric_items(spec: dict[str, Any], defaults: list[tuple[str, str, str, str]]) -> list[tuple[str, str, str, str]]: + raw_items = clean_items(spec_list(spec, ["metrics", "kpis", "cards", "items"]), len(defaults)) + if not raw_items: + return defaults + out: list[tuple[str, str, str, str]] = [] + for index, item in enumerate(raw_items): + default = defaults[min(index, len(defaults) - 1)] + out.append( + ( + item_text(item, ["value", "number", "metric"], default[0]), + item_text(item, ["label", "name", "title"], default[1]), + item_text(item, ["note", "caption", "description"], default[2]), + item_color(item, default[3]), + ) + ) + while len(out) < len(defaults): + out.append(defaults[len(out)]) + return out + + +def numeric_weight(value: str, fallback: float) -> float: + match = re.search(r"[-+]?\d+(?:\.\d+)?", value.replace(",", "")) + if not match: + return fallback + try: + return max(1.0, float(match.group(0))) + except ValueError: + return fallback + + +def renderer_family(kind: str) -> str: + if kind in {"cover", "editor_note", "closing"}: + return f"layout.{kind}" + if kind == "kpi_cards": + return "chart.kpi" + if kind == "bubble_chart": + return "chart.bubble" + if kind == "donut_chart": + return "chart.donut" + if kind == "sankey_chart": + return "chart.sankey" + if kind in {"toc", "chapter", "content", "ending", "reference_style"}: + return f"layout.{kind}" + chart_groups = { + "line": {"line_chart", "area_chart", "stacked_area_chart", "dual_axis_line_chart"}, + "bar": {"bar_chart", "horizontal_bar_chart", "grouped_bar_chart", "stacked_bar_chart", "waterfall_chart", "pareto_chart", "butterfly_chart"}, + "proportion": {"pie_chart", "donut_chart", "treemap_chart", "funnel_chart", "sankey_chart"}, + "matrix": {"matrix_2x2", "quadrant_text_bullets", "quadrant_bubble_scatter", "heatmap_chart", "comparison_table", "feature_matrix_table", "harvey_balls_table"}, + "flow": {"process_flow", "pipeline_with_stages", "timeline", "roadmap_vertical", "gantt_chart", "journey_map", "snake_flow", "numbered_steps"}, + "hub": {"hub_spoke", "hub_inward_arrows", "mind_map", "top_down_tree", "client_server_flow", "module_composition", "layered_architecture"}, + "radial": {"circular_stages", "segmented_wheel", "concentric_circles", "radar_chart", "venn_diagram", "gauge_chart"}, + "table": {"basic_table", "consulting_table", "project_schedule_table", "financial_statement_table"}, + } + for family, names in chart_groups.items(): + if kind in names: + return f"chart.{family}" + return "chart.framework" + + +def render_demo_slide( + *, + page: int, + kind: str, + title: str, + summary: str, + asset_id: str = "", + accent: str = "#4A90E2", + spec: dict[str, Any] | None = None, + report: ComponentReport | None = None, +) -> str: + renderer_id = renderer_family(kind) + report = report or ComponentReport() + payload = merged_spec(spec) + source_trace = asset_id or f"svglide-pattern:{kind}" + if kind == "cover": + body = editorial_background(page, accent) + else: + body = editorial_shell(page, accent, title, summary) + report.add(page, "title-block", renderer_id, bbox(70, 40, 700, 114), ["typography", "geometric_shape"], effects=["editorial_hierarchy"], source_trace=source_trace, asset_id=asset_id) + + if kind == "cover": + body.extend(render_cover_visual(page, accent, title, summary, payload, report, renderer_id, source_trace, asset_id)) + elif kind == "editor_note": + body.extend(render_editor_note_visual(page, accent, title, summary, payload, report, renderer_id, source_trace, asset_id)) + elif kind == "closing": + body.extend(render_closing_visual(page, accent, payload, report, renderer_id, source_trace, asset_id)) + elif kind == "kpi_cards": + body.extend(render_kpi_cards_visual(page, accent, payload, report, renderer_id, source_trace, asset_id)) + elif kind == "bubble_chart": + body.extend(render_bubble_visual(page, accent, payload, report, renderer_id, source_trace, asset_id)) + elif kind == "donut_chart": + body.extend(render_donut_visual(page, accent, payload, report, renderer_id, source_trace, asset_id)) + elif kind == "sankey_chart": + body.extend(render_sankey_visual(page, accent, payload, report, renderer_id, source_trace, asset_id)) + elif renderer_id.startswith("layout."): + body.extend(render_layout_visual(page, kind, accent, report, source_trace, asset_id)) + elif renderer_id == "chart.line": + body.extend(render_line_visual(page, accent, report, renderer_id, source_trace, asset_id)) + elif renderer_id == "chart.bar": + body.extend(render_bar_visual(page, accent, payload, report, renderer_id, source_trace, asset_id)) + elif renderer_id == "chart.proportion": + body.extend(render_proportion_visual(page, accent, report, renderer_id, source_trace, asset_id)) + elif renderer_id == "chart.matrix": + body.extend(render_matrix_visual(page, accent, report, renderer_id, source_trace, asset_id)) + elif renderer_id == "chart.flow": + body.extend(render_flow_visual(page, accent, report, renderer_id, source_trace, asset_id)) + elif renderer_id == "chart.hub": + body.extend(render_hub_visual(page, accent, payload, report, renderer_id, source_trace, asset_id)) + elif renderer_id == "chart.radial": + body.extend(render_radial_visual(page, accent, report, renderer_id, source_trace, asset_id)) + elif renderer_id == "chart.table": + body.extend(render_table_visual(page, accent, report, renderer_id, source_trace, asset_id)) + else: + body.extend(render_framework_visual(page, accent, report, renderer_id, source_trace, asset_id)) + body.append(editorial_footer(page)) + report.add(page, "footer", renderer_id, bbox(64, 480, 832, 18), ["typography"], source_trace=source_trace) + return svg_wrap(page, accent, body) + + +def render_cover_visual(page: int, accent: str, title: str, summary: str, spec: dict[str, Any], report: ComponentReport, renderer_id: str, source_trace: str, asset_id: str) -> list[str]: + kicker = spec_text(spec, ["kicker", "eyebrow", "section_label"], "GLOBAL AI CAPITAL / EDITORIAL BRIEFING") + hero_title = split_hero_title(spec_text(spec, ["cover_title", "title"], title)) + subtitle = spec_text(spec, ["cover_subtitle", "body", "subtitle", "summary"], summary or "Capital, compute, and the closed-loop race for strategic control") + year = spec_text(spec, ["year", "date", "period"], "2026") + meta_1 = spec_text(spec, ["meta_1", "meta_primary", "right_label"], "INDUSTRY BRIEFING") + meta_2 = spec_text(spec, ["meta_2", "meta_secondary", "right_note"], "AI INFRA\nCAPITAL MAP") + body = [ + rect("cover-atmosphere", 0, 0, W, H, "#05070B", opacity=0.38), + rect("cover-left-rail", 68, 74, 7, 348, RED, rx=0), + rect("cover-master-rule", 94, 388, 520, 5, accent, rx=0), + rect("cover-master-rule-muted", 632, 424, 142, 5, "#6D737D", rx=0), + text_box("cover-kicker", 96, 76, 430, 18, kicker, size=9, weight=900, color=accent, family=SERIF, letter_spacing=2), + text_box("title", 96, 126, 700, 160, hero_title, size=64, weight=900, color=INK, family=SERIF, line_height=1.08), + text_box("cover-master-title", 98, 304, 560, 48, subtitle, size=19, weight=800, color=GOLD, family=SERIF, line_height=1.18), + text_box("cover-year", 684, 72, 180, 54, year, size=40, weight=900, color=INK, align="right", family=SERIF), + rect("cover-meta-rule", 746, 306, 92, 3, RED, rx=0), + text_box("cover-meta-1", 704, 322, 136, 30, meta_1, size=8, weight=900, color=accent, align="right", family=SERIF, letter_spacing=1.5), + text_box("cover-meta-2", 704, 360, 136, 44, meta_2, size=10, weight=900, color=MUTED, align="right"), + ] + for index in range(20): + x = 880 + (index % 2) * 18 + y = 82 + (index // 2) * 32 + r = 2 if index % 3 else 3 + body.append(circle(f"cover-particle-{index+1}", x, y, r, accent if index % 2 else RED, opacity=0.28)) + body.extend(asset_mark("slash", 820, 418, 0.9, RED, opacity=0.48)) + for index, y in enumerate((438, 452, 466), 1): + body.append(line(f"cover-data-line-{index}", 96, y, 846 - index * 78, y, "#28313D", **{"stroke-width": 1})) + report.add(page, "cover-hero", renderer_id, bbox(68, 72, 796, 396), ["geometric_shape", "typography", "annotation"], effects=["editorial_cover", "large_hero_type"], source_trace=source_trace, asset_id=asset_id) + return body + + +def render_editor_note_visual(page: int, accent: str, title: str, summary: str, spec: dict[str, Any], report: ComponentReport, renderer_id: str, source_trace: str, asset_id: str) -> list[str]: + thesis = spec_text(spec, ["thesis", "key_message", "claim", "title"], "三组数字,足以定义 2026 年的 AI 资本格局。") + copy = spec_text(spec, ["copy", "body", "description", "summary"], summary or "Capital is no longer a financing footnote; it is the operating system for compute, model access, and strategic control.") + metrics = metric_items( + spec, + [ + ("$297B", "Q1 global venture capital", "", RED), + ("$188B", "AI captured the majority", "", "#F4A261"), + ], + ) + side = spec_dict(spec, ["side_note", "sidebar", "question"]) + side_label = item_text(side, ["label", "title"], spec_text(spec, ["side_label"], "EDITORIAL\nQUESTION")) + side_copy = item_text(side, ["copy", "note", "body"], spec_text(spec, ["side_copy"], "Who controls the capital loop when compute becomes scarce?")) + body = [ + rect("note-panel", 82, 176, 666, 246, PANEL, rx=0, stroke=RULE, **{"stroke-width": 1}), + *asset_mark("quote_ticks", 102, 188, 1.0, RED, opacity=0.95), + rect("note-quote", 124, 238, 5, 92, accent, rx=0), + text_box("note-thesis", 154, 206, 520, 54, thesis, size=24, weight=900, color=INK, family=SERIF), + text_box("note-copy", 154, 274, 512, 44, copy, size=12, weight=600, color=MUTED), + text_box("note-stat-a-value", 154, 328, 210, 66, metrics[0][0], size=50, weight=900, color=metrics[0][3], family=SERIF), + text_box("note-stat-a-caption", 158, 400, 178, 18, metrics[0][1] or metrics[0][2], size=8, weight=800, color=MUTED), + text_box("note-stat-b-value", 402, 328, 210, 66, metrics[1][0], size=50, weight=900, color=metrics[1][3], family=SERIF), + text_box("note-stat-b-caption", 406, 400, 178, 18, metrics[1][1] or metrics[1][2], size=8, weight=800, color=MUTED), + rect("note-side", 770, 184, 108, 226, "#0A0E14", rx=0, stroke="#222B36", **{"stroke-width": 1}), + rect("note-side-accent", 770, 184, 108, 5, RED, rx=0), + text_box("note-side-label", 786, 216, 76, 34, side_label, size=8, weight=900, color=accent, align="center"), + text_box("note-side-copy", 786, 286, 76, 74, side_copy, size=9, weight=800, color=GOLD, align="center"), + ] + report.add(page, "editor-note-panel", renderer_id, bbox(82, 176, 796, 246), ["geometric_shape", "typography", "annotation"], effects=["editorial_note", "hero_metrics"], source_trace=source_trace, asset_id=asset_id) + return body + + +def render_closing_visual(page: int, accent: str, spec: dict[str, Any], report: ComponentReport, renderer_id: str, source_trace: str, asset_id: str) -> list[str]: + body = [ + rect("closing-list", 86, 174, 650, 260, PANEL, rx=0, stroke=RULE, **{"stroke-width": 1}), + text_box("closing-section-label", 108, 188, 210, 18, spec_text(spec, ["section_label", "label"], "SIX TAKEAWAYS"), size=10, weight=900, color=RED), + ] + defaults = ["资本约束将先于模型约束", "算力采购成为战略资产负债表", "闭环投资提高生态锁定", "主权资本进入 AI 基建底层", "赢家从模型公司扩展到电力与网络", "现金流证明会替代叙事融资"] + raw_items = clean_items(spec_list(spec, ["takeaways", "items", "bullets"]), 6) + items = [(f"{index+1:02d}", item_text(item, ["copy", "text", "label", "title"], defaults[index])) for index, item in enumerate(raw_items)] + while len(items) < 6: + items.append((f"{len(items)+1:02d}", defaults[len(items)])) + for index, (num, copy) in enumerate(items): + col = index % 2 + row = index // 2 + x = 108 + col * 308 + y = 222 + row * 58 + body.append(rect(f"closing-red-index-{index+1}", x, y, 42, 34, RED if index == 0 else "#24171C", rx=0, stroke=RED, **{"stroke-width": 1})) + body.append(text_box(f"closing-num-{index+1}", x + 5, y + 8, 32, 14, num, size=10, weight=900, color=INK if index == 0 else RED, align="center")) + body.append(text_box(f"closing-copy-{index+1}", x + 58, y + 1, 210, 32, copy, size=13, weight=900, color=INK)) + body.append(line(f"closing-rule-{index+1}", x + 58, y + 42, x + 268, y + 42, RULE, **{"stroke-width": 1})) + body.extend([ + rect("closing-critical", 86, 430, 650, 34, "#151D27", rx=0), + rect("closing-critical-bar", 86, 430, 6, 34, accent), + text_box("closing-critical-copy", 112, 439, 520, 16, spec_text(spec, ["critical_copy", "closing_statement", "callout"], "This is not yet the bubble bursting. It is the bubble compounding."), size=10, weight=900, color=GOLD), + rect("closing-sidebar", 762, 190, 112, 220, "#0A0E14", rx=0, stroke="#222B36", **{"stroke-width": 1}), + text_box("closing-sidebar-text", 782, 226, 72, 116, spec_text(spec, ["sidebar", "sidebar_text"], "CAPITAL\nDISCIPLINE\nREPLACES\nNARRATIVE\nFINANCE"), size=10, weight=900, color=MUTED, align="center"), + rect("closing-sidebar-rule", 786, 366, 64, 4, RED, rx=0), + ]) + report.add(page, "closing-system", renderer_id, bbox(86, 174, 788, 308), ["geometric_shape", "typography", "annotation"], effects=["closing_takeaways", "numbered_hierarchy"], source_trace=source_trace, asset_id=asset_id) + return body + + +def render_kpi_cards_visual(page: int, accent: str, spec: dict[str, Any], report: ComponentReport, renderer_id: str, source_trace: str, asset_id: str) -> list[str]: + body = [] + defaults = [ + ("$297B", "单季 VC 总额 / Q1 VC", "single-quarter record", RED), + ("81%", "AI 占全季 VC / AI share", "of disclosed capital", "#F4A261"), + ("$188B", "Top 4 笔大单 / Top 4 deals", "OpenAI · Anthropic · xAI · Waymo", "#B8B2A9"), + ("65%", "集中度 / Concentration", "Top 4 share of global VC", RED), + ] + values = metric_items(spec, defaults) + for index, (value, label_text, note, color) in enumerate(values): + col = index % 2 + row = index // 2 + x = 78 + col * 260 + y = 184 + row * 126 + body.append(rect(f"kpi-card-{index+1}", x, y, 230, 116, PANEL, rx=0, stroke=RULE, **{"stroke-width": 1})) + body.append(rect(f"kpi-card-accent-{index+1}", x, y, 4, 52, color, rx=0)) + body.append(text_box(f"kpi-label-{index+1}", x + 22, y + 18, 170, 16, label_text, size=8, weight=800, color=MUTED)) + body.append(text_box(f"kpi-value-{index+1}", x + 22, y + 36, 168, 56, value, size=42, weight=900, color=INK, family=SERIF, line_height=1.0)) + body.append(text_box(f"kpi-note-{index+1}", x + 22, y + 94, 178, 14, note, size=8, weight=700, color=GOLD)) + insight = spec_dict(spec, ["insight", "observation", "callout"]) + insight_label = item_text(insight, ["label"], "EDITORIAL NOTE") + insight_title = item_text(insight, ["title", "headline"], "钱没变多,\n是更集中。") + insight_copy = item_text(insight, ["copy", "body", "note"], "Capital concentrated into a select few companies and a single industry.") + insight_number = item_text(insight, ["number", "value", "metric"], "$172B") + body.extend([ + line("kpi-note-divider", 610, 184, 610, 438, RULE, **{"stroke-width": 1}), + text_box("kpi-observation-label", 634, 202, 160, 16, insight_label, size=8, weight=900, color=RED), + line("kpi-observation-rule", 634, 224, 736, 224, RED, **{"stroke-width": 1}), + rect("kpi-observation-title-back", 626, 246, 208, 82, "#090D13", rx=0, opacity=0.58), + text_box("kpi-observation-title", 634, 258, 180, 60, insight_title, size=22, weight=900, color=INK, family=SERIF), + text_box("kpi-observation-copy", 634, 336, 190, 58, insight_copy, size=9, weight=700, color=GOLD), + text_box("kpi-observation-number", 646, 404, 152, 36, insight_number, size=28, weight=900, color=RED, align="right", family=SERIF), + ]) + report.add(page, "kpi-card-set", renderer_id, bbox(78, 184, 746, 254), ["geometric_shape", "typography", "metric", "annotation"], effects=["metric_grid", "editorial_sidebar"], source_trace=source_trace, asset_id=asset_id) + return body + + +def render_bubble_visual(page: int, accent: str, spec: dict[str, Any], report: ComponentReport, renderer_id: str, source_trace: str, asset_id: str) -> list[str]: + defaults = [ + {"name": "xAI", "arr": 5, "valuation": 230, "investors": 8, "note": "$5B / $230B · 8+ investors", "color": "#F4A261"}, + {"name": "OpenAI", "arr": 24, "valuation": 852, "investors": 7, "note": "$24B / $852B · 7+ investors", "color": RED}, + {"name": "Anthropic", "arr": 30, "valuation": 380, "investors": 5, "note": "$30B / $380B · 5 investors", "color": RED}, + ] + raw_bubbles = clean_items(spec_list(spec, ["bubbles", "bubble_points", "points", "items"]), 5) or defaults + plot_x = 132 + plot_y = 190 + plot_w = 690 + plot_h = 224 + max_arr = numeric_weight(spec_text(spec, ["x_max", "arr_max"], "40"), 40) + max_valuation = numeric_weight(spec_text(spec, ["y_max", "valuation_max"], "1000"), 1000) + body = [ + line("bubble-axis-y", plot_x, plot_y, plot_x, plot_y + plot_h, "#5C6470", **{"stroke-width": 1}), + line("bubble-axis-x", plot_x, plot_y + plot_h, plot_x + plot_w, plot_y + plot_h, "#5C6470", **{"stroke-width": 1}), + ] + for index, ratio in enumerate((0.2, 0.4, 0.6, 0.8), 1): + y = int(round(plot_y + plot_h * (1 - ratio))) + x = int(round(plot_x + plot_w * ratio)) + body.append(rect(f"bubble-grid-y-{index}", plot_x, y, plot_w, 1, RULE, rx=0, opacity=0.42)) + body.append(text_box(f"bubble-y-label-{index}", 86, y - 7, 36, 14, f"${int(max_valuation * ratio)}B", size=8, weight=700, color="#5C6470", align="right", family=SERIF)) + body.append(text_box(f"bubble-x-label-{index}", x - 20, plot_y + plot_h + 10, 40, 14, f"${int(max_arr * ratio)}B", size=8, weight=700, color="#5C6470", align="center", family=SERIF)) + body.extend([ + text_box("bubble-y-axis-title", plot_x, 170, 120, 14, "VALUATION ($B)", size=8, weight=800, color=MUTED, align="left", family=SERIF, letter_spacing=1), + text_box("bubble-x-axis-title", 410, 438, 178, 12, "ANNUALIZED REVENUE (ARR, $B)", size=8, weight=800, color=MUTED, align="center", family=SERIF, letter_spacing=1), + path("bubble-fair-line", f"M{plot_x + 44} {plot_y + plot_h - 4} L{plot_x + plot_w - 18} {plot_y + 22}", stroke="#8A857E", **{"stroke-width": 1, "stroke-dasharray": "6 4", "opacity": 0.54}), + text_box("bubble-fair-line-label", 650, 182, 172, 14, spec_text(spec, ["reference_label"], "implied fair valuation trend"), size=8, weight=700, color=MUTED, align="right", family=SERIF), + ]) + for index, item in enumerate(raw_bubbles): + default = defaults[min(index, len(defaults) - 1)] + name = item_text(item, ["name", "label", "title"], str(default["name"])) + arr = numeric_weight(item_text(item, ["arr", "revenue", "x", "x_value"], str(default["arr"])), float(default["arr"])) + valuation = numeric_weight(item_text(item, ["valuation", "value", "y", "y_value"], str(default["valuation"])), float(default["valuation"])) + investors = numeric_weight(item_text(item, ["investors", "size", "weight"], str(default["investors"])), float(default["investors"])) + color = item_color(item, str(default["color"])) + note = item_text(item, ["note", "caption", "description"], str(default["note"])) + x = int(round(plot_x + min(arr, max_arr) / max_arr * plot_w)) + y = int(round(plot_y + plot_h - min(valuation, max_valuation) / max_valuation * plot_h)) + radius = int(round(18 + investors * 3.8)) + item_id = f"bubble-{slug_id(name)}" + label_x = x - 68 + note_x = x - 96 + label_y = y + radius + 18 + note_y = label_y + 18 + if index == 0: + label_x = x - 20 + note_x = x - 20 + label_y = y - radius - 36 + note_y = label_y + 24 + elif index == 1: + label_x = x - 236 + note_x = x - 256 + label_y = 160 + note_y = 184 + elif index == 2: + label_x = x + 42 + note_x = x + 42 + label_y = y - 14 + note_y = label_y + 24 + elif y > plot_y + plot_h * 0.58: + label_y = y - radius - 32 + note_y = label_y + 24 + body.append(circle(item_id, x, y, radius, color, stroke=color, **{"stroke-width": 2, "fill-opacity": 0.2})) + body.append(circle(f"{item_id}-center", x, y, 3, color)) + body.append(rect(f"{item_id}-name-plate", label_x - 4, label_y - 1, 144, 18, "#070A0F", rx=0, opacity=0.78)) + body.append(text_box(f"{item_id}-label", label_x, label_y, 136, 18, name, size=11, weight=900, color=INK, align="center", family=SERIF)) + body.append(text_box(f"{item_id}-note", note_x, note_y, 192, 14, note, size=8, weight=700, color=MUTED, align="center", family=SERIF)) + insight = spec_text( + spec, + ["insight", "observation", "callout"], + "所有气泡都站在公允线之上:估值跑得比收入快,xAI 偏离最远,Anthropic 兑现度最高。", + ) + body.extend([ + rect("bubble-insight-band", 82, 452, 782, 16, PANEL, rx=0), + rect("bubble-insight-rule", 82, 452, 4, 16, RED, rx=0), + text_box("bubble-insight-copy", 102, 455, 720, 10, insight, size=8, weight=900, color=GOLD), + ]) + report.add(page, "bubble-chart", renderer_id, bbox(82, 190, 782, 300), ["geometric_shape", "typography", "annotation", "metric"], effects=["bubble_scatter", "axis_grid", "valuation_revenue_encoding"], source_trace=source_trace, asset_id=asset_id) + return body + + +def render_donut_visual(page: int, accent: str, spec: dict[str, Any], report: ComponentReport, renderer_id: str, source_trace: str, asset_id: str) -> list[str]: + default_segments = [ + {"name": "Amazon", "value": "$50B", "note": "41% · AWS commitment", "share": 41, "color": accent}, + {"name": "Nvidia", "value": "$30B", "note": "25% · 10GW deployment", "share": 25, "color": "#F4A261"}, + {"name": "SoftBank", "value": "$30B", "note": "25% · Stargate vehicle", "share": 25, "color": "#B8B2A9"}, + {"name": "MSFT + a16z", "value": "$12B", "note": "9% · other investors", "share": 9, "color": RED}, + ] + segments_raw = clean_items(spec_list(spec, ["segments", "donut_segments", "investors", "items"]), 4) or default_segments + colors = [item_color(item, default_segments[min(index, 3)]["color"]) for index, item in enumerate(segments_raw)] + shares = [numeric_weight(item_text(item, ["share", "percent", "weight"], ""), 100.0 / max(1, len(segments_raw))) for item in segments_raw] + share_total = sum(shares) or 1.0 + body = [ + circle("donut-track", 300, 318, 116, "none", stroke="#2C333D", **{"stroke-width": 46}), + ] + circumference = 729.0 + offset = 0.0 + for index, (share, color) in enumerate(zip(shares, colors), 1): + length = circumference * share / share_total + body.append( + circle( + f"donut-segment-{index}", + 300, + 318, + 116, + "none", + stroke=color, + **{ + "stroke-width": 46, + "stroke-dasharray": f"{round(length, 1)} 729", + "stroke-dashoffset": round(-offset, 1), + "stroke-linecap": "butt", + "transform": "rotate(-90 300 318)", + }, + ) + ) + offset += length + center = spec_dict(spec, ["center", "total", "summary_metric"]) + center_value = item_text(center, ["value", "number"], spec_text(spec, ["center_value", "total_value"], "$122B")) + center_label = item_text(center, ["label", "title"], spec_text(spec, ["center_label"], "TOTAL ROUND")) + center_note = item_text(center, ["note", "caption"], spec_text(spec, ["center_note"], "@ $852B post-money")) + body.extend([ + circle("donut-hole", 300, 318, 68, BG, stroke="#3A424E", **{"stroke-width": 1}), + rect("donut-center-value-back", 236, 288, 128, 44, "#090D13", rx=0, opacity=0.62), + text_box("donut-center-value", 242, 292, 116, 38, center_value, size=31, weight=900, color=INK, align="center", family=SERIF), + text_box("donut-center-label", 250, 340, 100, 18, center_label, size=8, weight=800, color=MUTED, align="center"), + text_box("donut-center-note", 248, 362, 104, 16, center_note, size=8, weight=700, color="#5C6470", align="center"), + line("donut-investor-divider", 560, 196, 560, 436, RULE, **{"stroke-width": 1}), + text_box("donut-investor-label", 590, 204, 166, 16, spec_text(spec, ["legend_label"], "INVESTOR BREAKDOWN"), size=8, weight=900, color=RED), + ]) + for index, item in enumerate(segments_raw): + name = item_text(item, ["name", "label", "title"], default_segments[min(index, 3)]["name"]) + value = item_text(item, ["value", "number"], default_segments[min(index, 3)]["value"]) + note = item_text(item, ["note", "caption"], default_segments[min(index, 3)]["note"]) + y = 242 + index * 48 + body.append(rect(f"donut-legend-row-back-{index+1}", 604, y - 2, 218, 42, "#090D13", rx=0, opacity=0.58)) + body.append(rect(f"donut-legend-key-{index+1}", 590, y, 5, 28, colors[index], rx=0)) + body.append(text_box(f"donut-legend-name-{index+1}", 612, y + 2, 104, 18, name, size=10, weight=900, color=INK)) + body.append(text_box(f"donut-legend-value-{index+1}", 750, y, 66, 20, value, size=16, weight=900, color=colors[index], align="right", family=SERIF)) + body.append(text_box(f"donut-legend-note-{index+1}", 612, y + 23, 202, 14, note, size=8, weight=700, color=MUTED)) + body.append(line(f"donut-legend-rule-{index+1}", 590, y + 44, 816, y + 44, RULE, **{"stroke-width": 1})) + body.extend([ + rect("donut-micro-chart-1", 390, 424, 40, 10, RED, rx=0), + rect("donut-micro-chart-2", 434, 424, 28, 10, "#F4A261", rx=0), + rect("donut-micro-chart-3", 466, 424, 22, 10, "#B8B2A9", rx=0), + rect("donut-micro-chart-4", 492, 424, 16, 10, "#8A857E", rx=0), + text_box("donut-observation", 590, 440, 292, 24, spec_text(spec, ["observation", "insight"], "Three largest investors account for 91%."), size=8, weight=900, color=GOLD), + ]) + report.add(page, "donut-chart", renderer_id, bbox(132, 196, 684, 240), ["geometric_shape", "annotation", "typography"], effects=["donut_proportion", "investor_breakdown"], source_trace=source_trace, asset_id=asset_id) + return body + + +def render_sankey_visual(page: int, accent: str, spec: dict[str, Any], report: ComponentReport, renderer_id: str, source_trace: str, asset_id: str) -> list[str]: + origin = spec_dict(spec, ["origin", "source"]) + origin_name = item_text(origin, ["name", "label", "title"], "Nvidia") + origin_value = item_text(origin, ["value", "number"], "$40B+") + origin_label = item_text(origin, ["note", "caption", "subtitle"], "equity invested") + target_defaults = [ + {"name": "OpenAI", "value": "$30B", "color": "#F4A261"}, + {"name": "xAI", "value": "~$3B", "color": "#D98E4F"}, + {"name": "Anthropic", "value": "~$2B", "color": "#B97949"}, + {"name": "CoreWeave + other", "value": "~$4B", "color": "#9B6745"}, + ] + target_items = clean_items(spec_list(spec, ["targets", "nodes", "recipients"]), 4) or target_defaults + return_flow = spec_dict(spec, ["return_flow", "return", "outcome"]) + return_title = item_text(return_flow, ["title", "name", "label"], "芯片采购") + return_value = item_text(return_flow, ["value", "number"], "$100B+") + return_note = item_text(return_flow, ["note", "caption"], "implied return") + insight = spec_text(spec, ["insight", "caption"], "Equity checks recycle into accelerator demand and multi-year compute commitments.") + body = [ + text_box("sankey-origin-top-label", 104, 182, 90, 14, "ORIGIN", size=8, weight=900, color="#5C6470", align="center"), + text_box("sankey-recipient-label", 454, 182, 120, 14, "RECIPIENT", size=8, weight=900, color="#5C6470", align="center"), + text_box("sankey-return-label", 760, 182, 112, 14, "RETURN FLOW", size=8, weight=900, color="#5C6470", align="right"), + line("sankey-top-rule", 70, 204, 878, 204, RULE, **{"stroke-width": 1}), + ] + right_nodes = [] + for index, item in enumerate(target_items): + default = target_defaults[min(index, 3)] + right_nodes.append( + ( + 436, + 224 + index * 60, + item_color(item, default["color"]), + item_text(item, ["name", "label", "title"], default["name"]), + item_text(item, ["value", "number"], default["value"]), + ) + ) + flows = [ + ("M210 300 C300 264 338 248 436 248", RED, 30), + ("M210 324 C300 318 348 308 436 308", "#B43A3F", 16), + ("M210 342 C300 354 348 368 436 368", "#8E343B", 12), + ("M210 360 C300 392 348 428 436 428", "#6E2B34", 10), + ("M610 248 C692 248 718 300 782 300", "#F4A261", 30), + ("M610 308 C696 308 720 328 782 328", "#F4A261", 14), + ("M610 368 C696 368 720 352 782 352", "#F4A261", 11), + ("M610 428 C696 428 720 380 782 380", "#F4A261", 9), + ] + for index, (d, color, width) in enumerate(flows, 1): + body.append(path(f"sankey-flow-{index}", d, stroke=color, **{"stroke-width": width, "stroke-linecap": "round", "opacity": 0.62 if index <= 4 else 0.46})) + body.extend([ + rect("sankey-origin-1", 88, 252, 122, 154, RED, rx=0, opacity=0.96), + text_box("sankey-origin-name-1", 110, 284, 78, 18, origin_name, size=13, weight=900, color=INK, align="center"), + text_box("sankey-origin-value-1", 100, 316, 98, 42, origin_value, size=32, weight=900, color=INK, align="center", family=SERIF), + text_box("sankey-origin-label-1", 108, 362, 86, 18, origin_label, size=8, weight=800, color="#F7C5C8", align="center"), + ]) + for index, (x, y, color, name, value) in enumerate(right_nodes, 1): + body.append(rect(f"sankey-target-{index}", x, y, 174, 38, color, rx=0, opacity=0.92)) + body.append(text_box(f"sankey-target-name-{index}", x + 12, y + 11, 92, 14, name, size=9, weight=900, color="#111820")) + body.append(text_box(f"sankey-target-value-{index}", x + 126, y + 8, 36, 18, value, size=12, weight=900, color="#111820", align="right", family=SERIF)) + body.extend([ + rect("sankey-return-block", 782, 236, 90, 186, RED, rx=0, opacity=0.82), + text_box("sankey-return-title", 810, 266, 50, 32, return_title, size=10, weight=900, color=INK, align="center"), + text_box("sankey-return-value", 806, 320, 58, 52, return_value, size=17, weight=900, color=INK, align="center", family=SERIF), + text_box("sankey-return-note", 804, 392, 58, 24, return_note, size=8, weight=800, color="#F7C5C8", align="center"), + text_box("sankey-insight", 88, 448, 650, 18, insight, size=8, weight=800, color=GOLD), + ]) + report.add(page, "sankey-flow-system", renderer_id, bbox(70, 182, 808, 266), ["path", "geometric_shape", "connector", "typography"], effects=["flow_width_encoding", "return_flow"], source_trace=source_trace, asset_id=asset_id) + return body + + +def render_layout_visual(page: int, kind: str, accent: str, report: ComponentReport, source_trace: str, asset_id: str) -> list[str]: + body = [ + path("layout-hero-arc", "M86 380 C240 276 360 444 526 334 C676 234 780 332 878 260", stroke=accent, **{"stroke-width": 10, "stroke-linecap": "round", "opacity": 0.72}), + rect("layout-panel-a", 96, 190, 228, 170, "#FFFFFF", rx=10, stroke="#CBD5E1", **{"stroke-width": 2}), + rect("layout-panel-b", 366, 184, 210, 212, "#FFFFFF", rx=10, stroke="#CBD5E1", **{"stroke-width": 2}), + rect("layout-panel-c", 618, 202, 238, 150, "#FFFFFF", rx=10, stroke="#CBD5E1", **{"stroke-width": 2}), + text_box("layout-kind", 370, 418, 220, 28, kind.replace("_", " ").upper(), size=17, weight=800, color=accent, align="center"), + ] + report.add(page, "layout-structure", f"layout.{kind}", bbox(86, 184, 792, 262), ["path", "geometric_shape", "typography"], effects=["page_rhythm"], source_trace=source_trace, asset_id=asset_id) + return body + + +def render_line_visual(page: int, accent: str, report: ComponentReport, renderer_id: str, source_trace: str, asset_id: str) -> list[str]: + body = [line("axis-x", 120, 420, 850, 420, "#CBD5E1", **{"stroke-width": 2}), line("axis-y", 120, 185, 120, 420, "#CBD5E1", **{"stroke-width": 2})] + points = [(150, 380), (265, 330), (380, 350), (495, 266), (610, 292), (725, 216), (830, 238)] + d = "M" + " L".join(f"{x} {y}" for x, y in points) + body.append(path("line-series", d, stroke=accent, **{"stroke-width": 8, "stroke-linecap": "round", "stroke-linejoin": "round"})) + for index, (x, y) in enumerate(points, 1): + body.append(circle(f"line-point-{index}", x, y, 8, "#FFFFFF", stroke=accent, **{"stroke-width": 4})) + report.add(page, "line-chart", renderer_id, bbox(120, 185, 730, 235), ["path", "connector", "geometric_shape"], effects=["chart_geometry"], source_trace=source_trace, asset_id=asset_id) + return body + + +def render_bar_visual(page: int, accent: str, spec: dict[str, Any], report: ComponentReport, renderer_id: str, source_trace: str, asset_id: str) -> list[str]: + body = [ + line("bar-axis-x", 132, 374, 650, 374, "#5C6470", **{"stroke-width": 1}), + line("bar-axis-y", 132, 198, 132, 374, "#5C6470", **{"stroke-width": 1}), + ] + for index, y in enumerate((250, 280, 310, 340, 374), 1): + body.append(line(f"bar-grid-{index}", 132, y, 650, y, "#222B36", **{"stroke-width": 1, "stroke-dasharray": "2 4", "opacity": 0.48})) + defaults = [ + {"name": "Meta", "value": "$200B", "amount": 200, "color": RED}, + {"name": "MSFT", "value": "$190B", "amount": 190, "color": "#B8B2A9"}, + {"name": "Google", "value": "$190B", "amount": 190, "color": "#B8B2A9"}, + {"name": "Oracle", "value": "$145B+", "amount": 145, "color": "#8A857E"}, + ] + raw_bars = clean_items(spec_list(spec, ["bars", "bar_series", "categories", "items"]), 4) or defaults + amounts = [numeric_weight(item_text(item, ["amount", "raw_value", "value"], str(defaults[min(index, 3)]["amount"])), float(defaults[min(index, 3)]["amount"])) for index, item in enumerate(raw_bars)] + max_amount = max(amounts) if amounts else 1.0 + bars = [] + for index, item in enumerate(raw_bars): + default = defaults[min(index, 3)] + value = item_text(item, ["value", "number"], str(default["value"])) + height = int(round(84 + (amounts[index] / max_amount) * 92)) + bars.append((item_text(item, ["name", "label", "title"], str(default["name"])), value, height, item_color(item, str(default["color"])))) + for index, (name, value, height, color) in enumerate(bars): + x = 188 + index * 116 + y = 374 - height + body.append(rect(f"bar-{index+1}", x, y, 74, height, color, rx=0, opacity=0.96)) + body.append(rect(f"bar-value-back-{index+1}", x - 12, y - 30, 98, 24, "#090D13", rx=0, opacity=0.74)) + body.append(text_box(f"bar-value-{index+1}", x - 10, y - 30, 94, 24, value, size=18, weight=900, color=INK, align="center", family=SERIF)) + body.append(text_box(f"bar-label-{index+1}", x - 8, 382, 90, 16, name, size=8, weight=800, color=MUTED, align="center")) + body.extend([ + line("bar-insight-divider", 704, 190, 704, 432, RULE, **{"stroke-width": 1}), + text_box("bar-insight-label", 724, 210, 148, 16, spec_text(spec, ["insight_label"], "KEY INSIGHT"), size=8, weight=900, color=RED), + text_box("bar-insight-value", 724, 244, 154, 60, spec_text(spec, ["insight_value", "total_value"], "$725B"), size=42, weight=900, color=RED, family=SERIF), + text_box("bar-insight-copy", 724, 320, 150, 74, spec_text(spec, ["insight", "insight_copy", "observation"], "Power, cooling, and land now shape the AI infrastructure bottleneck."), size=9, weight=800, color=GOLD), + ]) + report.add(page, "bar-chart", renderer_id, bbox(132, 190, 742, 252), ["geometric_shape", "typography", "annotation"], effects=["chart_geometry", "editorial_sidebar"], source_trace=source_trace, asset_id=asset_id) + return body + + +def render_proportion_visual(page: int, accent: str, report: ComponentReport, renderer_id: str, source_trace: str, asset_id: str) -> list[str]: + colors = [accent, "#8BC34A", "#FFC107", "#E91E63"] + body = [] + for index, radius in enumerate([118, 92, 66, 40]): + body.append(circle(f"ring-{index+1}", 330, 316, radius, colors[index], opacity=0.14 + index * 0.13)) + for index, color in enumerate(colors): + body.append(rect(f"legend-{index+1}", 560, 228 + index * 48, 220, 26, color, rx=13, opacity=0.78)) + report.add(page, "proportion-system", renderer_id, bbox(212, 198, 568, 246), ["geometric_shape", "annotation"], effects=["proportion_focus"], source_trace=source_trace, asset_id=asset_id) + return body + + +def render_matrix_visual(page: int, accent: str, report: ComponentReport, renderer_id: str, source_trace: str, asset_id: str) -> list[str]: + body = [] + for row in range(2): + for col in range(2): + x = 142 + col * 320 + y = 186 + row * 126 + body.append(rect(f"matrix-cell-{row}-{col}", x, y, 286, 96, "#FFFFFF", rx=10, stroke=accent, **{"stroke-width": 2, "opacity": 0.9})) + body.append(text_box(f"matrix-label-{row}-{col}", x + 18, y + 24, 236, 30, f"Quadrant {row * 2 + col + 1}", size=17, weight=800, color="#1E293B")) + report.add(page, "matrix-grid", renderer_id, bbox(142, 186, 606, 222), ["geometric_shape", "typography"], effects=["comparison_grid"], source_trace=source_trace, asset_id=asset_id) + return body + + +def render_flow_visual(page: int, accent: str, report: ComponentReport, renderer_id: str, source_trace: str, asset_id: str) -> list[str]: + body = [] + for index in range(5): + x = 112 + index * 154 + body.append(rect(f"flow-step-{index+1}", x, 246, 104, 76, accent, rx=18, opacity=0.82)) + body.append(text_box(f"flow-label-{index+1}", x + 8, 270, 88, 20, f"{index+1:02d}", size=19, weight=900, color="#0F172A", align="center")) + if index < 4: + body.append(line(f"flow-link-{index+1}", x + 110, 284, x + 146, 284, accent, **{"stroke-width": 5, "stroke-linecap": "round"})) + report.add(page, "flow-chain", renderer_id, bbox(112, 246, 720, 76), ["geometric_shape", "connector", "typography"], effects=["process_flow"], source_trace=source_trace, asset_id=asset_id) + return body + + +def render_hub_visual(page: int, accent: str, spec: dict[str, Any], report: ComponentReport, renderer_id: str, source_trace: str, asset_id: str) -> list[str]: + hub = spec_dict(spec, ["hub", "center", "core"]) + hub_value = item_text(hub, ["value", "number"], "$500B") + hub_label = item_text(hub, ["label", "name", "title"], "STARGATE") + body = [ + rect("hub-panel", 86, 168, 690, 276, PANEL, rx=0, stroke=RULE, **{"stroke-width": 1}), + circle("hub-orbit-outer", 442, 306, 126, "none", stroke="#26303B", **{"stroke-width": 1, "opacity": 0.82}), + circle("hub-orbit-inner", 442, 306, 82, "none", stroke="#26303B", **{"stroke-width": 1, "opacity": 0.62}), + circle("hub-core-glow", 442, 306, 64, accent, opacity=0.12), + circle("hub-core", 442, 306, 58, BG, stroke=accent, **{"stroke-width": 3}), + circle("hub-core-fill", 442, 306, 43, "#101720", stroke="#2A3440", **{"stroke-width": 1}), + text_box("hub-core-value", 398, 286, 88, 22, hub_value, size=17, weight=900, color=INK, align="center"), + text_box("hub-core-label", 400, 314, 84, 16, hub_label, size=8, weight=900, color=accent, align="center"), + ] + default_nodes = [ + {"name": "OpenAI", "note": "model demand", "color": RED}, + {"name": "Oracle", "note": "cloud sites", "color": "#F4A261"}, + {"name": "SoftBank", "note": "capital stack", "color": GOLD}, + {"name": "MGX", "note": "sovereign capital", "color": accent}, + {"name": "10GW", "note": "power target", "color": "#B8B2A9"}, + {"name": "7 sites", "note": "US footprint", "color": "#6D737D"}, + ] + raw_nodes = clean_items(spec_list(spec, ["nodes", "spokes", "items"]), 6) or default_nodes + positions = [(224, 226), (658, 226), (224, 382), (658, 382), (442, 190), (442, 422)] + nodes = [] + for index, item in enumerate(raw_nodes): + default = default_nodes[min(index, 5)] + x, y = positions[index] + nodes.append((x, y, item_text(item, ["name", "label", "title"], default["name"]), item_text(item, ["note", "caption", "description"], default["note"]), item_color(item, default["color"]))) + for index, (x, y, name, note, node_color) in enumerate(nodes, 1): + sx, sy, ex, ey = radial_connector(442, 306, x, y, 62, 58) + body.append(line(f"hub-spoke-{index}", sx, sy, ex, ey, node_color, **{"stroke-width": 1, "opacity": 0.65})) + body.append(circle(f"hub-round-node-{index}", x, y, 18, "#0A0E14", stroke=node_color, **{"stroke-width": 2})) + label_x = x - 132 if x < 442 else x + 26 + if x == 442: + label_x = x + 44 + label_y = y - 24 + else: + label_y = y - 54 if y < 306 else y + 30 + body.append(rect(f"hub-node-{index}", label_x, label_y, 116, 48, "#0A0E14", rx=0, stroke="#2A3440", **{"stroke-width": 1, "opacity": 0.9})) + body.append(rect(f"hub-node-accent-{index}", label_x, label_y, 5, 48, node_color)) + body.append(text_box(f"hub-node-name-{index}", label_x + 16, label_y + 8, 84, 14, name, size=9, weight=900, color=INK, align="center")) + body.append(text_box(f"hub-node-note-{index}", label_x + 16, label_y + 27, 84, 12, note, size=8, weight=700, color=MUTED, align="center")) + body.extend([ + rect("hub-side", 792, 190, 78, 208, "#0A0E14", rx=0, stroke="#222B36", **{"stroke-width": 1}), + rect("hub-side-rule", 792, 190, 78, 5, RED, rx=0), + text_box("hub-side-copy", 806, 230, 50, 104, spec_text(spec, ["side_note", "side_copy"], "Project finance meets compute scarcity."), size=9, weight=900, color=GOLD, align="center"), + text_box("hub-side-index", 800, 360, 62, 16, spec_text(spec, ["side_index", "index_label"], "CAPEX LOOP"), size=8, weight=900, color=MUTED, align="center"), + ]) + report.add(page, "hub-network", renderer_id, bbox(86, 168, 784, 276), ["geometric_shape", "connector", "typography", "icon"], effects=["radial_network", "orbit_system"], source_trace=source_trace, asset_id=asset_id) + return body + + +def render_radial_visual(page: int, accent: str, report: ComponentReport, renderer_id: str, source_trace: str, asset_id: str) -> list[str]: + body = [] + wedges = [(480, 310, 610, 230, 626, 330), (480, 310, 564, 430, 456, 448), (480, 310, 350, 390, 334, 290), (480, 310, 396, 190, 504, 172)] + for index, (x1, y1, x2, y2, x3, y3) in enumerate(wedges, 1): + body.append(path(f"radial-wedge-{index}", f"M{x1} {y1} L{x2} {y2} L{x3} {y3} Z", fill=accent, opacity=0.12 + index * 0.035)) + body.append(circle("radial-center", 480, 310, 54, "#FFFFFF", stroke=accent, **{"stroke-width": 5})) + body.append(text_box("radial-center-label", 430, 296, 100, 28, "SYSTEM", size=15, weight=900, color=accent, align="center")) + report.add(page, "radial-system", renderer_id, bbox(326, 178, 308, 264), ["geometric_shape", "annotation"], effects=["radial_balance"], source_trace=source_trace, asset_id=asset_id) + return body + + +def render_table_visual(page: int, accent: str, report: ComponentReport, renderer_id: str, source_trace: str, asset_id: str) -> list[str]: + body = [rect("table-shell", 110, 184, 740, 250, "#FFFFFF", rx=12, stroke="#CBD5E1", **{"stroke-width": 2})] + for row in range(1, 5): + body.append(line(f"table-row-{row}", 110, 184 + row * 50, 850, 184 + row * 50, "#E2E8F0", **{"stroke-width": 2})) + for col in range(1, 4): + body.append(line(f"table-col-{col}", 110 + col * 185, 184, 110 + col * 185, 434, "#E2E8F0", **{"stroke-width": 2})) + body.append(rect("table-header", 110, 184, 740, 50, accent, rx=12, opacity=0.82)) + report.add(page, "table-system", renderer_id, bbox(110, 184, 740, 250), ["geometric_shape", "typography"], effects=["dense_table"], source_trace=source_trace, asset_id=asset_id) + return body + + +def render_framework_visual(page: int, accent: str, report: ComponentReport, renderer_id: str, source_trace: str, asset_id: str) -> list[str]: + body = [] + for index in range(4): + x = 132 + index * 178 + body.append(rect(f"framework-card-{index+1}", x, 218, 138, 148, "#FFFFFF", rx=16, stroke=accent, **{"stroke-width": 3, "opacity": 0.95})) + body.append(circle(f"framework-dot-{index+1}", x + 69, 210, 18, accent, opacity=0.9)) + body.append(text_box(f"framework-label-{index+1}", x + 14, 282, 110, 28, f"Pillar {index+1}", size=15, weight=800, color="#1E293B", align="center")) + report.add(page, "framework-cards", renderer_id, bbox(132, 192, 672, 174), ["geometric_shape", "typography"], effects=["parallel_structure"], source_trace=source_trace, asset_id=asset_id) + return body + + +def write_json(path: Path, data: Any) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(json.dumps(data, ensure_ascii=False, indent=2, sort_keys=True) + "\n", encoding="utf-8") + + +def read_json(path: Path) -> Any: + return json.loads(path.read_text(encoding="utf-8")) + + +def file_digest(path: Path) -> str: + return hashlib.sha256(path.read_bytes()).hexdigest() + + +def rel_path(root: Path, path: Path) -> str: + try: + return path.relative_to(root).as_posix() + except ValueError: + return path.as_posix() + + +def plan_path(project: Path, value: str) -> Path: + path_value = Path(value).expanduser() + if path_value.is_absolute(): + return path_value + return project / path_value + + +def text_from_any(value: Any) -> str: + if isinstance(value, str): + return value.strip() + if isinstance(value, (int, float)) and not isinstance(value, bool): + return str(value) + return "" + + +def normalize_kind(value: Any, fallback: str) -> str: + raw = text_from_any(value).replace(".", "_").replace("-", "_").lower() + if raw.startswith("chart_"): + raw = raw.removeprefix("chart_") + if raw.startswith("layout_"): + raw = raw.removeprefix("layout_") + return PAGE_KIND_ALIASES.get(raw, raw or fallback) + + +def asset_id_from(value: Any) -> str: + if isinstance(value, str): + return value.strip() + if isinstance(value, dict): + for key in ["asset_id", "id", "design_pattern_id"]: + found = asset_id_from(value.get(key)) + if found: + return found + if isinstance(value, list): + for item in value: + found = asset_id_from(item) + if found: + return found + return "" + + +def selected_asset_ids(plan: dict[str, Any]) -> set[str]: + selection = plan.get("design_pattern_selection") + if not isinstance(selection, dict): + return set() + assets = selection.get("selected_assets") + out: set[str] = set() + if isinstance(assets, list): + for asset in assets: + asset_id = asset_id_from(asset) + if asset_id: + out.add(asset_id) + else: + asset_id = asset_id_from(assets) + if asset_id: + out.add(asset_id) + return out + + +def slide_items(plan: dict[str, Any]) -> list[dict[str, Any]]: + for key in ["slides", "pages", "page_plan"]: + value = plan.get(key) + if isinstance(value, list) and value: + return [item for item in value if isinstance(item, dict)] + page_count = plan.get("page_count") or plan.get("slide_count") or len(SUPPORTED_PAGE_KINDS) + try: + count = max(1, int(page_count)) + except (TypeError, ValueError): + count = len(SUPPORTED_PAGE_KINDS) + return [{"page_kind": SUPPORTED_PAGE_KINDS[index % len(SUPPORTED_PAGE_KINDS)]} for index in range(count)] + + +def first_text(item: dict[str, Any], keys: list[str], fallback: str) -> str: + for key in keys: + value = text_from_any(item.get(key)) + if value: + return value + visual_plan = item.get("visual_plan") + if isinstance(visual_plan, dict): + value = text_from_any(visual_plan.get(key)) + if value: + return value + return fallback + + +def slide_summary_text(item: dict[str, Any], kind: str) -> str: + primary = first_text( + item, + ["summary", "subtitle", "key_message", "note", "speaker_note"], + f"Runtime-generated {kind.replace('_', ' ')} page.", + ) + body = first_text(item, ["body", "description", "supporting_copy"], "") + if body and body not in primary: + return f"{primary} {body}" + return primary + + +def slide_kind(item: dict[str, Any], index: int) -> str: + fallback = SUPPORTED_PAGE_KINDS[index % len(SUPPORTED_PAGE_KINDS)] + generic_kinds = {"chart", "charts", "content", "page", "slide"} + generic_match = "" + visual_plan = item.get("visual_plan") + for key in ["page_kind", "chart_type", "page_type", "archetype", "kind", "type", "renderer_id", "visual_recipe"]: + for source in [item, visual_plan if isinstance(visual_plan, dict) else {}]: + value = source.get(key) + if not value: + continue + kind = normalize_kind(value, fallback) + if kind in generic_kinds: + generic_match = generic_match or kind + continue + return kind + if generic_match: + return generic_match + return fallback + + +def slide_asset_id(item: dict[str, Any], kind: str, selected_assets: set[str]) -> str: + visual_plan = item.get("visual_plan") + nested_reference = visual_plan.get("reference_asset") if isinstance(visual_plan, dict) else None + nested_asset = visual_plan.get("asset_id") if isinstance(visual_plan, dict) else None + explicit = ( + asset_id_from(item.get("asset_id")) + or asset_id_from(item.get("design_pattern_id")) + or asset_id_from(item.get("reference_asset")) + or asset_id_from(item.get("design_reference_assets")) + or asset_id_from(nested_reference) + or asset_id_from(nested_asset) + ) + if explicit: + return explicit + default = DEFAULT_ASSET_BY_KIND.get(kind, "") + if default and (not selected_assets or default in selected_assets): + return default + return "" + + +def valid_hex_color(value: str) -> str: + return value.upper() if re.fullmatch(r"#[0-9A-Fa-f]{6}", value) else "" + + +def plan_palette_accent(plan: dict[str, Any], index: int) -> str: + style_system = plan.get("style_system") + palette = style_system.get("palette") if isinstance(style_system, dict) else None + if not isinstance(palette, dict): + return "" + accent = valid_hex_color(text_from_any(palette.get("accent"))) + if accent: + return accent + support = palette.get("support") + if isinstance(support, list) and support: + for offset in range(len(support)): + candidate = valid_hex_color(text_from_any(support[(index + offset) % len(support)])) + if candidate: + return candidate + return "" + + +def slide_accent(item: dict[str, Any], index: int, plan: dict[str, Any] | None = None) -> str: + accent = text_from_any(item.get("accent") or item.get("primary_color")) + explicit = valid_hex_color(accent) + if explicit: + return explicit + if isinstance(plan, dict): + planned = plan_palette_accent(plan, index) + if planned: + return planned + return DEFAULT_ACCENTS[index % len(DEFAULT_ACCENTS)] + + +def compose_project(project: Path, plan: Path) -> dict[str, Any]: + project = project.expanduser().resolve() + plan = plan.expanduser().resolve() + project.mkdir(parents=True, exist_ok=True) + data = read_json(plan) + if not isinstance(data, dict): + raise ValueError("compose plan must contain a JSON object") + + pages_dir = project / "pages" + receipts_dir = project / "receipts" + pages_dir.mkdir(parents=True, exist_ok=True) + receipts_dir.mkdir(parents=True, exist_ok=True) + + report = ComponentReport() + selected_assets = selected_asset_ids(data) + deck_title = first_text(data, ["title", "deck_title", "name"], "SVGlide Runtime Deck") + contract_mode = is_strategist_contract(data) + page_outputs: list[dict[str, Any]] = [] + + for index, item in enumerate(slide_items(data), 1): + kind = slide_kind(item, index - 1) + title = first_text(item, ["title", "headline", "name"], f"{deck_title}: {kind.replace('_', ' ').title()}") + summary = slide_summary_text(item, kind) + asset_id = slide_asset_id(item, kind, selected_assets) + accent = slide_accent(item, index - 1, data) + if contract_mode: + svg = render_contract_slide( + page=index, + kind=kind, + title=title, + summary=summary, + asset_id=asset_id, + accent=accent, + spec=item, + report=report, + deck_title=deck_title, + ) + else: + svg = render_demo_slide(page=index, kind=kind, title=title, summary=summary, asset_id=asset_id, accent=accent, spec=item, report=report) + output = pages_dir / f"page-{index:03d}.svg" + output.write_text(svg, encoding="utf-8") + page_outputs.append( + { + "page": index, + "page_kind": kind, + "renderer_id": renderer_family(kind), + "source_svg": rel_path(project, output), + "asset_id": asset_id, + } + ) + + component_report = report.to_dict() + usage_receipt = design_pattern_usage_receipt(component_report) + runtime_cache = { + "schema_version": RUNTIME_CACHE_SCHEMA, + "status": "passed", + "generator": "svglide_gen_runtime", + "project": project.as_posix(), + "plan": rel_path(project, plan), + "plan_digest": file_digest(plan), + "supported_page_kinds": list(SUPPORTED_PAGE_KINDS), + "page_count": len(page_outputs), + "pages": page_outputs, + "outputs": { + "pages_dir": rel_path(project, pages_dir), + "component_report": "receipts/emitted_components.json", + "design_pattern_usage": "receipts/design-pattern-usage.json", + "runtime_cache": "receipts/runtime-cache.json", + }, + } + write_json(receipts_dir / "emitted_components.json", component_report) + write_json(receipts_dir / "design-pattern-usage.json", usage_receipt) + write_json(receipts_dir / "runtime-cache.json", runtime_cache) + return runtime_cache + + +def build_parser() -> argparse.ArgumentParser: + parser = argparse.ArgumentParser(description="SVGlide runtime composition helpers") + subparsers = parser.add_subparsers(dest="command", required=True) + compose = subparsers.add_parser("compose", help="compose SVGlide-safe SVG pages from a JSON plan") + compose.add_argument("--project", required=True, help="project output directory") + compose.add_argument("--plan", required=True, help="plan JSON path, absolute or relative to --project") + return parser + + +def main(argv: list[str] | None = None) -> int: + parser = build_parser() + args = parser.parse_args(argv) + if args.command == "compose": + project = Path(args.project) + plan = plan_path(project.expanduser().resolve(), args.plan) + result = compose_project(project, plan) + print(json.dumps(result, ensure_ascii=False, indent=2, sort_keys=True)) + return 0 + parser.error(f"unsupported command: {args.command}") + return 2 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/skills/lark-slides/scripts/svglide_gen_runtime_test.py b/skills/lark-slides/scripts/svglide_gen_runtime_test.py new file mode 100644 index 000000000..073783090 --- /dev/null +++ b/skills/lark-slides/scripts/svglide_gen_runtime_test.py @@ -0,0 +1,776 @@ +# Copyright (c) 2026 Lark Technologies Pte. Ltd. +# SPDX-License-Identifier: MIT +from __future__ import annotations + +import contextlib +import io +import json +import sys +import tempfile +import unittest +import xml.etree.ElementTree as ET +from pathlib import Path + +SCRIPT_DIR = Path(__file__).resolve().parent +sys.path.insert(0, str(SCRIPT_DIR)) + +import svglide_gen_runtime as runtime +import svg_preview_lint as preview_lint + + +class SVGlideGenRuntimeTest(unittest.TestCase): + def test_render_demo_slide_emits_protocol_svg_and_component_report(self) -> None: + report = runtime.ComponentReport() + + svg = runtime.render_demo_slide( + page=1, + kind="timeline", + title="Timeline", + summary="Pick for milestone events.", + asset_id="chart.timeline", + report=report, + ) + data = report.to_dict() + + self.assertIn('slide:role="slide"', svg) + self.assertIn('width="960"', svg) + self.assertEqual(data["schema_version"], "svglide-component-report/v1") + self.assertEqual(data["pages"][0]["page"], 1) + self.assertGreaterEqual(len(data["pages"][0]["components"]), 3) + component = data["pages"][0]["components"][0] + self.assertIn("bbox", component) + self.assertTrue(component["primitives"]) + + def test_design_pattern_usage_receipt_requires_page_trace(self) -> None: + report = runtime.ComponentReport() + runtime.render_demo_slide( + page=2, + kind="bar_chart", + title="Bar Chart", + summary="Pick for category comparison.", + asset_id="chart.bar_chart", + report=report, + ) + + receipt = runtime.design_pattern_usage_receipt(report.to_dict()) + + self.assertEqual(receipt["schema_version"], "svglide-design-pattern-usage/v1") + self.assertEqual(receipt["status"], "passed") + self.assertEqual(receipt["page_usages"][0]["asset_id"], "chart.bar_chart") + self.assertEqual(receipt["page_usages"][0]["page"], 2) + self.assertTrue(receipt["page_usages"][0]["component_ids"]) + self.assertTrue(receipt["page_usages"][0]["source_trace"]) + + def test_design_pattern_visual_contracts_for_hero_pages(self) -> None: + cover_report = runtime.ComponentReport() + cover_svg = runtime.render_demo_slide( + page=1, + kind="cover", + title="Global AI Capital 2026", + summary="Capital, compute, and control.", + asset_id="layout.page_type.cover", + report=cover_report, + ) + self.assertIn('id="cover-master-title"', cover_svg) + self.assertIn('font-size:64px', cover_svg) + self.assertIn("Cambria", cover_svg) + self.assertIn('id="slash-1"', cover_svg) + self.assertNotIn('id="title-surface"', cover_svg) + self.assertIn("large_hero_type", json.dumps(cover_report.to_dict())) + + note_report = runtime.ComponentReport() + note_svg = runtime.render_demo_slide( + page=2, + kind="editor_note", + title="Why AI Capital Now", + summary="Editorial note.", + asset_id="layout.page_type.content", + report=note_report, + ) + self.assertIn('id="quote_ticks-1"', note_svg) + self.assertIn('font-size:50px', note_svg) + self.assertIn("Cambria", note_svg) + self.assertIn("hero_metrics", json.dumps(note_report.to_dict())) + + closing_report = runtime.ComponentReport() + closing_svg = runtime.render_demo_slide( + page=8, + kind="closing", + title="Closing Thesis", + summary="Takeaways.", + asset_id="layout.page_type.ending", + report=closing_report, + ) + self.assertIn('id="closing-red-index-1"', closing_svg) + self.assertIn("numbered_hierarchy", json.dumps(closing_report.to_dict())) + + def test_hub_renderer_uses_orbit_system_not_plain_cards(self) -> None: + report = runtime.ComponentReport() + svg = runtime.render_demo_slide( + page=7, + kind="hub_spoke", + title="Stargate Hub", + summary="Project finance meets compute scarcity.", + asset_id="chart.hub_spoke", + report=report, + ) + + self.assertIn('id="hub-orbit-outer"', svg) + self.assertIn('id="hub-core-glow"', svg) + self.assertIn("CAPEX LOOP", svg) + self.assertIn("orbit_system", json.dumps(report.to_dict())) + + def test_chart_renderers_use_design_pattern_asset_level_structures(self) -> None: + cases = [ + ("kpi_cards", "kpi-observation-label", "editorial_sidebar"), + ("bar_chart", "bar-insight-value", "editorial_sidebar"), + ("donut_chart", "donut-investor-label", "investor_breakdown"), + ("sankey_chart", "sankey-return-block", "return_flow"), + ] + + for kind, required_id, required_effect in cases: + with self.subTest(kind=kind): + report = runtime.ComponentReport() + svg = runtime.render_demo_slide( + page=3, + kind=kind, + title=kind.replace("_", " "), + summary="SVGlide pattern-level renderer contract", + asset_id=f"chart.{kind}", + report=report, + ) + self.assertIn(f'id="{required_id}"', svg) + self.assertIn(required_effect, json.dumps(report.to_dict())) + self.assertIn("Cambria", svg) + + def test_global_ai_pages_7_8_keep_design_pattern_bubble_then_donut_archetypes(self) -> None: + with tempfile.TemporaryDirectory() as raw: + project = Path(raw) / "runtime-project" + project.mkdir() + plan = project / "slide_plan.json" + plan.write_text( + json.dumps( + { + "title": "Global AI Capital 2026", + "slides": [ + {"page_kind": "cover", "title": "Cover"}, + {"page_kind": "editor_note", "title": "Editor's Note"}, + {"page_kind": "kpi_cards", "title": "Q1 VC Landscape"}, + {"page_kind": "bar_chart", "title": "Hyperscaler Capex"}, + {"page_kind": "donut_chart", "title": "OpenAI Investors"}, + {"page_kind": "sankey_chart", "title": "Nvidia Loop"}, + { + "page_kind": "bubble_chart", + "asset_id": "chart.bubble_chart", + "title": "估值兑现度:气泡大小 = 投资人数", + "summary": "Valuation × ARR × investor count", + "bubbles": [ + {"name": "xAI", "arr": 5, "valuation": 230, "investors": 8, "note": "$5B / $230B · 8+ investors"}, + {"name": "OpenAI", "arr": 24, "valuation": 852, "investors": 7, "note": "$24B / $852B · 7+ investors"}, + {"name": "Anthropic", "arr": 30, "valuation": 380, "investors": 5, "note": "$30B / $380B · 5 investors"}, + ], + "insight": "所有气泡都站在公允线之上,xAI 偏离最远。", + }, + { + "page_kind": "donut_chart", + "asset_id": "chart.donut_chart", + "title": "OpenAI $122B 这笔钱,从哪来", + "center": {"value": "$122B", "label": "TOTAL ROUND", "note": "@ $852B post-money"}, + "segments": [ + {"name": "Amazon", "value": "$50B", "share": 41, "note": "41% · AWS 算力承诺"}, + {"name": "Nvidia", "value": "$30B", "share": 25, "note": "25% · 10GW 系统部署"}, + {"name": "SoftBank", "value": "$30B", "share": 25, "note": "25% · Stargate 联合主体"}, + {"name": "MSFT + 其他", "value": "$12B", "share": 9, "note": "9% · 其他投资人"}, + ], + }, + ], + } + ), + encoding="utf-8", + ) + + runtime.main(["compose", "--project", str(project), "--plan", "slide_plan.json"]) + + page7 = (project / "pages" / "page-007.svg").read_text(encoding="utf-8") + page8 = (project / "pages" / "page-008.svg").read_text(encoding="utf-8") + cache = json.loads((project / "receipts" / "runtime-cache.json").read_text(encoding="utf-8")) + usage = json.loads((project / "receipts" / "design-pattern-usage.json").read_text(encoding="utf-8")) + self.assertEqual(cache["pages"][6]["page_kind"], "bubble_chart") + self.assertEqual(cache["pages"][7]["page_kind"], "donut_chart") + self.assertIn('id="bubble-openai"', page7) + self.assertIn('id="bubble-anthropic"', page7) + self.assertIn('id="bubble-xai"', page7) + self.assertIn('id="bubble-insight-band"', page7) + self.assertIn('id="bubble-openai-name-plate"', page7) + self.assertNotIn('id="bubble-openai-label-back"', page7) + self.assertIn('id="donut-track"', page8) + self.assertIn("$122B", page8) + self.assertNotIn('id="hub-orbit-outer"', page7) + self.assertNotIn('id="closing-list"', page8) + self.assertEqual(usage["page_usages"][6]["asset_id"], "chart.bubble_chart") + self.assertEqual(usage["page_usages"][7]["asset_id"], "chart.donut_chart") + root = ET.fromstring(page7) + elements = {element.get("id"): element for element in root.iter() if element.get("id")} + for name in ("bubble-xai", "bubble-openai", "bubble-anthropic"): + label = elements[f"{name}-label"] + note = elements[f"{name}-note"] + plate = elements[f"{name}-name-plate"] + self.assertGreaterEqual(float(note.get("y", "0")) - float(label.get("y", "0")), 24) + self.assertLessEqual(float(plate.get("y", "0")) + float(plate.get("height", "0")), float(note.get("y", "0"))) + + def test_strategist_contract_renderer_uses_real_labels_not_placeholder_footer(self) -> None: + spec = { + "schema_version": "svglide-strategist-contract/v1", + "page_type": "kpi_overview", + "key_message": "低空物流网络的关键运营指标,包括时效、成本、覆盖、可靠性", + "text_budget_by_role": { + "title": {"max_chars": 18, "max_boxes": 1}, + "metric": {"max_chars": 12, "max_boxes": 1}, + "body": {"max_chars": 72, "max_boxes": 1}, + "footer": {"max_chars": 32, "max_boxes": 1}, + }, + "layout_boxes": [ + {"id": "title", "role": "title", "x": 48, "y": 34, "width": 864, "height": 48}, + {"id": "primary-kpi", "role": "metric", "x": 64, "y": 106, "width": 260, "height": 128}, + {"id": "secondary-grid", "role": "grid", "x": 348, "y": 106, "width": 548, "height": 128}, + {"id": "chart-row", "role": "chart", "x": 64, "y": 258, "width": 832, "height": 150}, + {"id": "body", "role": "body", "x": 64, "y": 426, "width": 832, "height": 56}, + {"id": "footer", "role": "footer", "x": 64, "y": 500, "width": 832, "height": 24}, + ], + } + report = runtime.ComponentReport() + + svg = runtime.render_contract_slide( + page=2, + kind="kpi_cards", + title="", + summary="", + asset_id="chart.kpi_cards", + accent="#2563EB", + spec=spec, + report=report, + deck_title="城市级低空物流网络策划案", + ) + + self.assertNotIn("SVGlide contract renderer", svg) + self.assertIn("准点率", svg) + self.assertIn("时效", svg) + self.assertIn("城市级低空物流网络策划案", svg) + + def test_strategist_contract_dense_pages_emit_semantic_visual_labels(self) -> None: + base_spec = { + "schema_version": "svglide-strategist-contract/v1", + "key_message": "低空物流网络需要把订单入口、空域调度、无人机执行和末端交付串成闭环", + "text_budget_by_role": { + "title": {"max_chars": 24, "max_boxes": 1}, + "body": {"max_chars": 96, "max_boxes": 1}, + "callout": {"max_chars": 60, "max_boxes": 1}, + "footer": {"max_chars": 32, "max_boxes": 1}, + }, + "layout_boxes": [ + {"id": "title", "role": "title", "x": 64, "y": 46, "width": 680, "height": 52}, + {"id": "visual", "role": "visual", "x": 92, "y": 132, "width": 776, "height": 300}, + {"id": "body", "role": "body", "x": 96, "y": 388, "width": 760, "height": 46}, + {"id": "footer", "role": "footer", "x": 64, "y": 500, "width": 832, "height": 24}, + ], + } + + cases = [ + ("process_flow", "process_flow", 'id="callout"', "订单入口"), + ("capability_map", "hub_spoke", 'id="legend"', "空域调度"), + ("comparison", "comparison_table", 'id="comparison-dimension-1"', "拥堵"), + ("chart_takeaway", "bar_chart", 'id="callout"', "订单入口"), + ("closing", "closing", 'id="closing-step-card-1"', "一中台"), + ] + + for page_type, kind, required_id, required_copy in cases: + with self.subTest(page_type=page_type): + spec = dict(base_spec, page_type=page_type, title=page_type) + report = runtime.ComponentReport() + svg = runtime.render_contract_slide( + page=3, + kind=kind, + title="", + summary="", + asset_id=f"chart.{kind}", + accent="#2563EB", + spec=spec, + report=report, + deck_title="城市级低空物流网络策划案", + ) + + self.assertIn(required_id, svg) + self.assertIn(required_copy, svg) + + def test_strategist_contract_uses_full_page_archetype_geometry(self) -> None: + base_spec = { + "schema_version": "svglide-strategist-contract/v1", + "key_message": "低空物流网络需要形成可运营、可调度、可复盘的城市级基础设施", + "text_budget_by_role": { + "title": {"max_chars": 24, "max_boxes": 1}, + "body": {"max_chars": 96, "max_boxes": 1}, + "callout": {"max_chars": 60, "max_boxes": 1}, + "footer": {"max_chars": 32, "max_boxes": 1}, + }, + "layout_boxes": [ + {"id": "title", "role": "title", "x": 64, "y": 46, "width": 680, "height": 52}, + {"id": "visual", "role": "visual", "x": 92, "y": 132, "width": 776, "height": 300}, + {"id": "body", "role": "body", "x": 96, "y": 388, "width": 760, "height": 46}, + {"id": "callout", "role": "callout", "x": 612, "y": 380, "width": 250, "height": 60}, + {"id": "footer", "role": "footer", "x": 64, "y": 500, "width": 832, "height": 24}, + ], + } + + cases = [ + ("cover", "cover", ["cover-map-field", "cover-route-ribbon", "cover-coordinate-stack-1"], ["full_page_archetype", "hero_route", "title_field"]), + ("agenda", "agenda", ["agenda-route-backplane", "agenda-number-1", "agenda-route-path"], ["numbered_path", "section_index", "semantic_labels"]), + ("section_divider", "section", ["section-signal-field", "section-index-rail", "section-hero-number"], ["section_index", "hero_signal", "full_page_archetype"]), + ("process_flow", "process_flow", ["flow-backplane", "flow-lane-upper", "flow-lane-lower"], ["connector_flow", "flow_lanes", "full_page_archetype"]), + ("capability_map", "hub_spoke", ["hub-backplane", "hub-sector-1", "hub-satellite-panel-1"], ["hub_spoke", "sector_field", "semantic_labels"]), + ("chart_takeaway", "bar_chart", ["bar-plot-backplane", "bar-insight-strip", "bar-variance-path"], ["chart_geometry", "insight_strip", "full_page_archetype"]), + ("closing", "closing", ["closing-backplane", "closing-step-card-1", "closing-route-ribbon"], ["closing_ribbon", "action_cards", "full_page_archetype"]), + ] + + for page_type, kind, required_ids, required_evidence in cases: + with self.subTest(page_type=page_type): + spec = dict(base_spec, page_type=page_type, title=page_type) + spec["visual_design_contract"] = { + "required_visual_evidence": required_evidence, + } + if page_type == "cover": + spec["layout_boxes"] = [ + {"id": "title", "role": "title", "x": 72, "y": 150, "width": 560, "height": 120}, + {"id": "body", "role": "body", "x": 76, "y": 284, "width": 520, "height": 72}, + {"id": "visual", "role": "visual", "x": 600, "y": 84, "width": 288, "height": 360}, + {"id": "footer", "role": "footer", "x": 64, "y": 500, "width": 832, "height": 24}, + ] + report = runtime.ComponentReport() + + svg = runtime.render_contract_slide( + page=4, + kind=kind, + title="", + summary="", + asset_id=f"chart.{kind}", + accent="#2563EB", + spec=spec, + report=report, + deck_title="城市级低空物流网络策划案", + ) + + for required_id in required_ids: + self.assertIn(f'id="{required_id}"', svg) + encoded_report = json.dumps(report.to_dict()) + for evidence in spec["visual_design_contract"]["required_visual_evidence"]: + self.assertIn(evidence, encoded_report) + + def test_evidence_effects_does_not_echo_contract_required_evidence_as_proof(self) -> None: + effects = runtime.evidence_effects( + {"visual_design_contract": {"required_visual_evidence": ["fake_contract_evidence"]}}, + ["chart_geometry"], + ) + + self.assertEqual(["chart_geometry"], effects) + self.assertNotIn("fake_contract_evidence", effects) + + def test_style_system_palette_changes_rendered_svg_fingerprint(self) -> None: + def compose_with_accent(accent: str) -> str: + with tempfile.TemporaryDirectory() as raw: + project = Path(raw) / "runtime-project" + project.mkdir() + plan = project / "slide_plan.json" + plan.write_text( + json.dumps( + { + "schema_version": "svglide-strategist-contract/v1", + "title": "Theme Accent Test", + "style_system": {"palette": {"accent": accent}}, + "slides": [ + { + "page": 1, + "page_type": "agenda", + "title": "Contents", + "key_message": "Agenda route", + "visual_design_contract": { + "required_visual_evidence": ["numbered_path", "section_index", "semantic_labels"], + }, + } + ], + } + ), + encoding="utf-8", + ) + runtime.compose_project(project, plan) + return (project / "pages" / "page-001.svg").read_text(encoding="utf-8") + + blue = compose_with_accent("#4A90E2") + red = compose_with_accent("#E91E63") + + self.assertIn("#4A90E2", blue) + self.assertIn("#E91E63", red) + self.assertNotEqual(blue, red) + + def test_contract_navigation_pages_emit_enough_semantic_labels_for_preview_lint(self) -> None: + deck_title = "新疆阿克苏城区居住区策划案" + cases = [ + ( + "cover", + "cover", + { + "page_type": "cover", + "title": "以水为脉·四时为序", + "key_message": "四境共生·四季归心的绿洲栖居范本", + "layout_boxes": [ + {"id": "title", "role": "title", "x": 72, "y": 150, "width": 560, "height": 120}, + {"id": "body", "role": "body", "x": 76, "y": 284, "width": 520, "height": 72}, + {"id": "visual", "role": "visual", "x": 600, "y": 84, "width": 288, "height": 360}, + {"id": "footer", "role": "footer", "x": 64, "y": 500, "width": 832, "height": 24}, + ], + }, + ), + ( + "agenda", + "agenda", + { + "page_type": "agenda", + "title": "目录", + "layout_boxes": [ + {"id": "title", "role": "title", "x": 64, "y": 54, "width": 600, "height": 54}, + {"id": "rail", "role": "timeline", "x": 96, "y": 136, "width": 48, "height": 312}, + {"id": "body", "role": "body", "x": 154, "y": 126, "width": 650, "height": 330}, + {"id": "visual", "role": "visual", "x": 820, "y": 156, "width": 64, "height": 64}, + {"id": "footer", "role": "footer", "x": 64, "y": 500, "width": 832, "height": 24}, + ], + }, + ), + ( + "section", + "section", + { + "page_type": "section_divider", + "title": "01 项目核心定位与愿景", + "key_message": "以水串联四季,打造全季态生态居住区", + "layout_boxes": [ + {"id": "section-number", "role": "kicker", "x": 72, "y": 92, "width": 160, "height": 90}, + {"id": "title", "role": "title", "x": 180, "y": 188, "width": 600, "height": 88}, + {"id": "body", "role": "body", "x": 184, "y": 292, "width": 560, "height": 38}, + {"id": "visual", "role": "visual", "x": 0, "y": 320, "width": 960, "height": 180}, + {"id": "footer", "role": "footer", "x": 64, "y": 500, "width": 832, "height": 24}, + ], + }, + ), + ] + + with tempfile.TemporaryDirectory() as raw: + project = Path(raw) + for page, (kind, asset_id, spec) in enumerate(cases, 1): + svg = runtime.render_contract_slide( + page=page, + kind=kind, + title="", + summary="", + asset_id=asset_id, + accent="#4A90E2", + spec={**spec, "_deck_title": deck_title}, + report=runtime.ComponentReport(), + deck_title=deck_title, + ) + source = preview_lint.SvgSource(page=page, label=f"page-{page}", root=ET.fromstring(svg), base_dir=project) + codes = {check["code"] for check in preview_lint.lint_svg_source(project, source)} + self.assertNotIn("unlabeled_visual_system", codes) + + def test_asset_marks_are_arc_free_and_reject_unknown_ids(self) -> None: + spark = "".join(runtime.asset_mark("spark", 12, 24, 1.25, "#E63946", opacity=0.8)) + + self.assertIn(" None: + with self.assertRaisesRegex(ValueError, "arc commands"): + runtime.path("bad-arc", "M10 10 A20 20 0 0 1 30 30") + + def test_compose_cli_writes_supported_pages_and_receipts(self) -> None: + with tempfile.TemporaryDirectory() as raw: + project = Path(raw) / "runtime-project" + project.mkdir() + plan = project / "slide_plan.json" + kinds = [ + "cover", + "editor_note", + "kpi_cards", + "bar_chart", + "bubble_chart", + "donut_chart", + "sankey_chart", + "hub_spoke", + "closing", + ] + chart_assets = [ + "chart.kpi_cards", + "chart.bar_chart", + "chart.bubble_chart", + "chart.donut_chart", + "chart.sankey_chart", + "chart.hub_spoke", + ] + plan.write_text( + json.dumps( + { + "title": "Runtime Slice", + "design_pattern_selection": {"selected_assets": [{"id": asset} for asset in chart_assets]}, + "slides": [ + { + "page_kind": kind, + "title": kind.replace("_", " ").title(), + "summary": f"Compose fixture for {kind}", + } + for kind in kinds + ], + } + ), + encoding="utf-8", + ) + + stdout = io.StringIO() + with contextlib.redirect_stdout(stdout): + exit_code = runtime.main(["compose", "--project", str(project), "--plan", "slide_plan.json"]) + + self.assertEqual(exit_code, 0) + result = json.loads(stdout.getvalue()) + self.assertEqual(result["schema_version"], "svglide-gen-runtime-cache/v1") + self.assertEqual(result["page_count"], len(kinds)) + self.assertEqual([page["page_kind"] for page in result["pages"]], kinds) + + for index in range(1, len(kinds) + 1): + svg_path = project / "pages" / f"page-{index:03d}.svg" + self.assertTrue(svg_path.exists()) + svg = svg_path.read_text(encoding="utf-8") + self.assertIn('slide:role="slide"', svg) + self.assertIn('viewBox="0 0 960 540"', svg) + self.assertNotRegex(svg, r']*\sd="[^"]*[Aa](?=[\s,\d.+-])') + ET.parse(svg_path) + + component_report = json.loads((project / "receipts" / "emitted_components.json").read_text(encoding="utf-8")) + self.assertEqual(component_report["schema_version"], "svglide-component-report/v1") + self.assertEqual(len(component_report["pages"]), len(kinds)) + self.assertGreaterEqual(component_report["summary"]["component_count"], 16) + + usage = json.loads((project / "receipts" / "design-pattern-usage.json").read_text(encoding="utf-8")) + self.assertEqual(usage["schema_version"], "svglide-design-pattern-usage/v1") + used_assets = {item["asset_id"] for item in usage["page_usages"]} + self.assertTrue(set(chart_assets).issubset(used_assets)) + self.assertTrue(set(chart_assets).issubset(set(usage["used_asset_ids"]))) + + cache = json.loads((project / "receipts" / "runtime-cache.json").read_text(encoding="utf-8")) + self.assertEqual(cache, result) + self.assertTrue(set(kinds).issubset(set(cache["supported_page_kinds"]))) + self.assertIn("agenda", cache["supported_page_kinds"]) + self.assertIn("section", cache["supported_page_kinds"]) + + def test_compose_uses_real_slide_plan_fields(self) -> None: + with tempfile.TemporaryDirectory() as raw: + project = Path(raw) / "runtime-project" + project.mkdir() + plan = project / "slide_plan.json" + plan.write_text( + json.dumps( + { + "title": "Field Mapping", + "slides": [ + { + "page": 1, + "page_type": "chart", + "chart_type": "bar_chart", + "reference_asset": {"asset_id": "chart.bar_chart", "source": "svglide_design_pattern"}, + "visual_plan": { + "key_message": "Capex bar chart", + "body": "Renderer should use chart_type and nested copy.", + }, + } + ], + } + ), + encoding="utf-8", + ) + + runtime.main(["compose", "--project", str(project), "--plan", "slide_plan.json"]) + + svg = (project / "pages" / "page-001.svg").read_text(encoding="utf-8") + cache = json.loads((project / "receipts" / "runtime-cache.json").read_text(encoding="utf-8")) + usage = json.loads((project / "receipts" / "design-pattern-usage.json").read_text(encoding="utf-8")) + self.assertEqual(cache["pages"][0]["page_kind"], "bar_chart") + self.assertIn("Capex bar chart", svg) + self.assertIn("Renderer should use chart_type", svg) + self.assertEqual(usage["page_usages"][0]["asset_id"], "chart.bar_chart") + + def test_compose_uses_nested_reference_asset(self) -> None: + with tempfile.TemporaryDirectory() as raw: + project = Path(raw) / "runtime-project" + project.mkdir() + plan = project / "slide_plan.json" + plan.write_text( + json.dumps( + { + "title": "Nested Asset Mapping", + "slides": [ + { + "page": 1, + "page_type": "content", + "visual_plan": { + "page_kind": "editor_note", + "reference_asset": {"asset_id": "layout.page_type.content", "source": "svglide_design_pattern"}, + "key_message": "Nested reference should drive the receipt", + }, + } + ], + } + ), + encoding="utf-8", + ) + + runtime.main(["compose", "--project", str(project), "--plan", "slide_plan.json"]) + + cache = json.loads((project / "receipts" / "runtime-cache.json").read_text(encoding="utf-8")) + usage = json.loads((project / "receipts" / "design-pattern-usage.json").read_text(encoding="utf-8")) + self.assertEqual(cache["pages"][0]["page_kind"], "editor_note") + self.assertEqual(cache["pages"][0]["asset_id"], "layout.page_type.content") + self.assertEqual(usage["page_usages"][0]["asset_id"], "layout.page_type.content") + + def test_compose_uses_nested_asset_id(self) -> None: + with tempfile.TemporaryDirectory() as raw: + project = Path(raw) / "runtime-project" + project.mkdir() + plan = project / "slide_plan.json" + plan.write_text( + json.dumps( + { + "title": "Nested Asset ID Mapping", + "slides": [ + { + "page": 1, + "page_type": "chart", + "chart_type": "donut_chart", + "visual_plan": { + "asset_id": "chart.donut_chart", + "key_message": "Nested asset_id should drive the receipt", + }, + } + ], + } + ), + encoding="utf-8", + ) + + runtime.main(["compose", "--project", str(project), "--plan", "slide_plan.json"]) + + cache = json.loads((project / "receipts" / "runtime-cache.json").read_text(encoding="utf-8")) + usage = json.loads((project / "receipts" / "design-pattern-usage.json").read_text(encoding="utf-8")) + self.assertEqual(cache["pages"][0]["page_kind"], "donut_chart") + self.assertEqual(cache["pages"][0]["asset_id"], "chart.donut_chart") + self.assertEqual(usage["page_usages"][0]["asset_id"], "chart.donut_chart") + + def test_compose_parameterizes_design_pattern_renderers_for_non_ai_topic(self) -> None: + with tempfile.TemporaryDirectory() as raw: + project = Path(raw) / "runtime-project" + project.mkdir() + plan = project / "slide_plan.json" + plan.write_text( + json.dumps( + { + "title": "Aksu Oasis Living District", + "slides": [ + { + "page_kind": "cover", + "title": "以水为脉 四时为序", + "summary": "阿克苏城区生态居住区策划案", + "kicker": "AKSU OASIS / RESIDENTIAL STRATEGY", + "meta_1": "OASIS PLANNING", + "meta_2": "FOUR SEASONS\nLIVING MAP", + "year": "2026", + }, + { + "page_kind": "kpi_cards", + "title": "四季地块价值矩阵", + "metrics": [ + {"value": "4", "label": "四季地块", "note": "春夏秋冬差异化主题"}, + {"value": "1", "label": "水系闭环", "note": "串联全区公共空间"}, + {"value": "3", "label": "价值引擎", "note": "配套 景观 文化"}, + {"value": "全年龄", "label": "人群覆盖", "note": "儿童 青年 长者"}, + ], + "insight": { + "label": "PLANNING NOTE", + "title": "水系不是装饰,\n是组织结构。", + "copy": "用一条蓝绿生态脉络串联地块、配套和归家体验。", + "number": "4境", + }, + }, + { + "page_kind": "sankey_chart", + "title": "水系如何转化为空间价值", + "origin": {"name": "水系", "value": "1环", "label": "ecological spine"}, + "targets": [ + {"name": "春配套", "value": "繁"}, + {"name": "夏活力", "value": "乐"}, + {"name": "秋静谧", "value": "享"}, + {"name": "冬暖居", "value": "暖"}, + ], + "return_flow": {"title": "归心体验", "value": "全年", "note": "four-season loop"}, + "insight": "Water organizes the community into a legible four-season living loop.", + }, + { + "page_kind": "hub_spoke", + "title": "四境联动系统", + "hub": {"value": "水脉", "label": "OASIS LOOP"}, + "nodes": [ + {"name": "春之地块", "note": "配套入口"}, + {"name": "夏之地块", "note": "运动活力"}, + {"name": "秋之地块", "note": "胡杨静谧"}, + {"name": "冬之地块", "note": "暖廊归家"}, + {"name": "艾德莱斯", "note": "地域纹样"}, + {"name": "公共服务", "note": "全年龄覆盖"}, + ], + "side_note": "四境共生 四季归心", + "side_index": "OASIS LOOP", + }, + { + "page_kind": "closing", + "title": "总结与展望", + "takeaways": [ + "以水为脉组织归家体验", + "四季地块形成差异化记忆", + "胡杨与艾德莱斯强化地域识别", + "配套共享覆盖全年龄需求", + "生态景观提升长期溢价", + "成为阿克苏人居升级样板", + ], + "critical_copy": "四境共生,四季归心。", + "sidebar": "OASIS\nLIVING\nAKSU", + }, + ], + } + ), + encoding="utf-8", + ) + + runtime.main(["compose", "--project", str(project), "--plan", "slide_plan.json"]) + + all_svg = "\n".join( + (project / "pages" / f"page-{index:03d}.svg").read_text(encoding="utf-8") + for index in range(1, 6) + ) + self.assertIn("AKSU OASIS", all_svg) + self.assertIn("四季地块", all_svg) + self.assertIn("水系不是装饰", all_svg) + self.assertIn("归心体验", all_svg) + self.assertIn("胡杨静谧", all_svg) + self.assertIn("四境共生,四季归心", all_svg) + self.assertNotIn("Global AI", all_svg) + self.assertNotIn("Nvidia", all_svg) + + +if __name__ == "__main__": + unittest.main() diff --git a/skills/lark-slides/scripts/svglide_golden_suite.py b/skills/lark-slides/scripts/svglide_golden_suite.py new file mode 100644 index 000000000..a977eeeed --- /dev/null +++ b/skills/lark-slides/scripts/svglide_golden_suite.py @@ -0,0 +1,112 @@ +#!/usr/bin/env python3 +# Copyright (c) 2026 Lark Technologies Pte. Ltd. +# SPDX-License-Identifier: MIT + +from __future__ import annotations + +import argparse +import copy +import json +import sys +from typing import Any + + +MANIFEST_SCHEMA_VERSION = "svglide-golden-suite-manifest/v1" + +_GOLDEN_CASES: tuple[dict[str, Any], ...] = ( + { + "case_id": "ai-capital-editorial", + "theme_domain": "ai_infrastructure_finance", + "prompt_summary": "Editorial deck about capital flows, compute commitments, and investor risk signals in the AI infrastructure cycle.", + "expected_archetypes": [ + "cover", + "editor_note", + "kpi_cards", + "bar_chart", + "donut_chart", + "sankey_chart", + "bubble_chart", + "closing", + ], + "required_evidence": [ + "design_pattern_usage_receipt", + "component_report_with_chart_archetypes", + "svg_preflight_pass", + ], + }, + { + "case_id": "aksu-oasis-planning", + "theme_domain": "urban_oasis_residential_planning", + "prompt_summary": "Planning deck for an Aksu oasis living district, using water, seasonal blocks, and local identity as the organizing system.", + "expected_archetypes": [ + "cover", + "agenda", + "section", + "kpi_cards", + "sankey_chart", + "hub_spoke", + "comparison_table", + "closing", + ], + "required_evidence": [ + "domain_copy_retained", + "agenda_numbered_path", + "section_signal", + "non_ai_topic_parameterization", + "svg_preflight_pass", + ], + }, + { + "case_id": "runtime-smoke", + "theme_domain": "svglide_runtime_health", + "prompt_summary": "Small deterministic smoke case for validating runtime composition, manifest plumbing, and receipt emission before larger regressions.", + "expected_archetypes": [ + "cover", + "kpi_cards", + "bar_chart", + "closing", + ], + "required_evidence": [ + "runtime_cache_written", + "component_report_written", + "svg_preflight_pass", + ], + }, +) + + +def list_cases() -> list[dict[str, Any]]: + return copy.deepcopy(list(_GOLDEN_CASES)) + + +def build_manifest() -> dict[str, Any]: + cases = list_cases() + return { + "schema_version": MANIFEST_SCHEMA_VERSION, + "case_count": len(cases), + "cases": cases, + } + + +def build_parser() -> argparse.ArgumentParser: + parser = argparse.ArgumentParser(description="Emit the built-in SVGlide golden suite case manifest.") + subparsers = parser.add_subparsers(dest="command", required=True) + list_parser = subparsers.add_parser("list", help="list built-in golden suite cases") + list_parser.add_argument("--json", action="store_true", help="emit the case manifest as JSON") + return parser + + +def main(argv: list[str] | None = None) -> int: + parser = build_parser() + args = parser.parse_args(argv) + if args.command == "list": + if not args.json: + parser.error("list currently supports only --json") + print(json.dumps(build_manifest(), ensure_ascii=False, indent=2, sort_keys=True)) + return 0 + parser.error(f"unsupported command: {args.command}") + return 2 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/skills/lark-slides/scripts/svglide_golden_suite_test.py b/skills/lark-slides/scripts/svglide_golden_suite_test.py new file mode 100644 index 000000000..1bbbc0ac4 --- /dev/null +++ b/skills/lark-slides/scripts/svglide_golden_suite_test.py @@ -0,0 +1,78 @@ +# Copyright (c) 2026 Lark Technologies Pte. Ltd. +# SPDX-License-Identifier: MIT +from __future__ import annotations + +import contextlib +import io +import json +import sys +import unittest +from pathlib import Path + +SCRIPT_DIR = Path(__file__).resolve().parent +sys.path.insert(0, str(SCRIPT_DIR)) + +import svglide_golden_suite as golden_suite + + +class SVGlideGoldenSuiteTest(unittest.TestCase): + def test_list_json_outputs_manifest_with_required_cases(self) -> None: + stdout = io.StringIO() + + with contextlib.redirect_stdout(stdout): + exit_code = golden_suite.main(["list", "--json"]) + + self.assertEqual(exit_code, 0) + manifest = json.loads(stdout.getvalue()) + self.assertEqual(manifest["schema_version"], "svglide-golden-suite-manifest/v1") + self.assertEqual(manifest["case_count"], len(manifest["cases"])) + self.assertGreaterEqual(manifest["case_count"], 3) + case_ids = {case["case_id"] for case in manifest["cases"]} + self.assertTrue( + { + "ai-capital-editorial", + "aksu-oasis-planning", + "runtime-smoke", + }.issubset(case_ids) + ) + + for case in manifest["cases"]: + for key in ["case_id", "theme_domain", "prompt_summary", "expected_archetypes", "required_evidence"]: + self.assertIn(key, case) + self.assertIsInstance(case["expected_archetypes"], list) + self.assertTrue(case["expected_archetypes"]) + + def test_manifest_has_no_external_reference_project_words(self) -> None: + encoded = json.dumps(golden_suite.build_manifest(), ensure_ascii=False).lower() + banned_tokens = [ + "ppt" + "-master", + "ppt" + "_master", + "ppt" + " master", + "hugo" + "he3", + "ppt" + "169", + ] + + for token in banned_tokens: + with self.subTest(token=token): + self.assertNotIn(token, encoded) + + def test_each_case_declares_required_evidence(self) -> None: + for case in golden_suite.list_cases(): + with self.subTest(case_id=case["case_id"]): + evidence = case["required_evidence"] + self.assertIsInstance(evidence, list) + self.assertTrue(evidence) + self.assertTrue(all(isinstance(item, str) and item for item in evidence)) + + def test_aksu_case_locks_agenda_and_section_regression(self) -> None: + cases = {case["case_id"]: case for case in golden_suite.list_cases()} + aksu = cases["aksu-oasis-planning"] + + self.assertIn("agenda", aksu["expected_archetypes"]) + self.assertIn("section", aksu["expected_archetypes"]) + self.assertIn("agenda_numbered_path", aksu["required_evidence"]) + self.assertIn("section_signal", aksu["required_evidence"]) + + +if __name__ == "__main__": + unittest.main() diff --git a/skills/lark-slides/scripts/svglide_project_runner.py b/skills/lark-slides/scripts/svglide_project_runner.py index bc4dedc54..c36f09378 100644 --- a/skills/lark-slides/scripts/svglide_project_runner.py +++ b/skills/lark-slides/scripts/svglide_project_runner.py @@ -16,11 +16,14 @@ import struct import subprocess import sys import time +import xml.etree.ElementTree as ET import zlib from dataclasses import dataclass from pathlib import Path from typing import Any +import svg_preflight + STAGES = [ "generate", @@ -67,10 +70,20 @@ PNG_SIGNATURE = b"\x89PNG\r\n\x1a\n" STAGE_FINGERPRINT_SCHEMA = "svglide-stage-fingerprint/v1" QUALITY_GATE_SCHEMA = "svglide-quality-gate/v1" ENV_PROOF_SCHEMA = "svglide-env-proof/v1" -PPT_MASTER_ASSET_USAGE_SCHEMA = "svglide-ppt-master-asset-usage/v1" +DESIGN_PATTERN_USAGE_SCHEMA = "svglide-design-pattern-usage/v1" COMPONENT_REPORT_SCHEMA = "svglide-component-report/v1" PREVIEW_LINT_WAIVER_TTL_MS = 30 * 60 * 1000 DEFAULT_VALIDATION_PROFILE = "authoring" +STRATEGIST_CONTRACT_CODES = { + "plan_missing_page_rhythm", + "plan_missing_page_type", + "plan_missing_main_visual_anchor", + "plan_main_visual_anchor_not_met", + "plan_reference_asset_unstructured", + "plan_unknown_chart_type", + "plan_chart_type_recipe_mismatch", + "plan_chart_type_contract_not_met", +} VISUAL_SCORE_THRESHOLDS = { "authoring": 75, "production": 85, @@ -464,6 +477,95 @@ def run_manifest_command(project: Path, data: dict[str, Any], stage: str) -> dic return {"status": "passed", "command": args, "log": str(log_path(project, stage).relative_to(project))} +def manifest_page_descriptions(data: dict[str, Any]) -> list[str]: + descriptions: list[str] = [] + raw_descriptions = data.get("page_descriptions") + if isinstance(raw_descriptions, list): + descriptions.extend(text_from_any(item) for item in raw_descriptions if text_from_any(item)) + for key in ["slides", "pages"]: + raw_pages = data.get(key) + if not isinstance(raw_pages, list): + continue + for item in raw_pages: + if isinstance(item, str) and item.strip(): + descriptions.append(item.strip()) + continue + if not isinstance(item, dict): + continue + text = " ".join( + part + for part in [ + text_from_any(item.get("title") or item.get("headline") or item.get("name")), + text_from_any(item.get("description") or item.get("summary") or item.get("key_message")), + ] + if part + ) + if text: + descriptions.append(text) + return descriptions + + +def project_brief_text(project: Path, data: dict[str, Any]) -> str: + parts: list[str] = [] + for key in ["brief", "prompt", "query", "source_brief"]: + value = text_from_any(data.get(key)) + if value: + parts.append(value) + for key in ["brief_file", "prompt_file", "source_brief_file", "source_file"]: + value = text_from_any(data.get(key)) + if not value: + continue + path = safe_existing_file(project_file(project, value), root=project) + parts.append(path.read_text(encoding="utf-8")) + for relative in ["source/brief.md", "source/prompt.md", "brief.md", "prompt.md"]: + candidate = project / relative + if candidate.exists() and candidate.is_file(): + parts.append(candidate.read_text(encoding="utf-8")) + return "\n\n".join(part.strip() for part in parts if part.strip()) + + +def builtin_generate(project: Path, data: dict[str, Any]) -> dict[str, Any]: + if data.get("generated") is True: + return {"status": "skipped", "reason": "generated=true"} + + import svglide_gen_runtime + import svglide_strategist + + plan_path = safe_output_file(plan_file(project, data), root=project, suffix=".json") + existing_plan = read_json(plan_path, {}) if plan_path.exists() else None + if existing_plan is not None and not isinstance(existing_plan, dict): + raise RunnerError("slide_plan.json must contain an object") + contract = svglide_strategist.build_contract( + brief=project_brief_text(project, data), + slide_plan=existing_plan, + page_descriptions=manifest_page_descriptions(data), + ) + if not text_from_any(contract.get("title")): + contract["title"] = deck_title(project, data) + write_json(plan_path, contract) + + runtime_cache = svglide_gen_runtime.compose_project(project, plan_path) + body = { + "status": "passed", + "generator": "builtin:svglide_strategist_runtime", + "plan": rel_to_project(project, plan_path), + "page_count": runtime_cache.get("page_count", 0), + "outputs": runtime_cache.get("outputs", {}), + "runtime_cache": runtime_cache, + } + log_path(project, "generate").write_text(json.dumps(body, ensure_ascii=False, indent=2, sort_keys=True), encoding="utf-8") + return body + + +def run_generate(project: Path, data: dict[str, Any]) -> dict[str, Any]: + command = stage_command(data, "generate") + if not command or command in {"builtin:svglide_runtime", "builtin:svglide_strategist_runtime"}: + return builtin_generate(project, data) + if command.startswith("builtin:"): + raise RunnerError(f"unknown generate builtin: {command}") + return run_manifest_command(project, data, "generate") + + def current_input_digest(project: Path, data: dict[str, Any], *, prepared: bool) -> str: pieces: list[str] = [] plan = plan_file(project, data) @@ -575,6 +677,20 @@ def stage_input_fingerprint(stage: str, project: Path, args: argparse.Namespace) add_file_fingerprint(inputs, project, "manifest", manifest_path(project)) add_value_fingerprint(inputs, "stage_command", stage_command(data, stage)) + if stage == "generate": + plan = plan_file(project, data) + if plan.exists(): + add_file_fingerprint(inputs, project, "plan", plan) + for relative in ["source/brief.md", "source/prompt.md", "brief.md", "prompt.md"]: + add_file_fingerprint(inputs, project, "brief", project / relative) + scripts = repo_root() / "skills" / "lark-slides" / "scripts" + references = repo_root() / "skills" / "lark-slides" / "references" + for script_name in ["svglide_strategist.py", "svglide_gen_runtime.py"]: + add_file_fingerprint(inputs, project, "script", scripts / script_name) + for catalog in ["style-presets.json", "svg-seeds.json", "svg-recipes.json", "svglide-design-pattern-map.json"]: + add_file_fingerprint(inputs, project, "catalog", references / catalog) + add_value_fingerprint(inputs, "brief", project_brief_text(project, data)) + add_value_fingerprint(inputs, "page_descriptions", manifest_page_descriptions(data)) if stage in {"generate", "prepare"}: for page in project_pages(project, data, prepared=False): add_file_fingerprint(inputs, project, "source_svg", page) @@ -604,7 +720,7 @@ def stage_input_fingerprint(stage: str, project: Path, args: argparse.Namespace) for dependency in ["preflight", "preview_lint"]: add_file_fingerprint(inputs, project, f"{dependency}_receipt", receipt_path(project, dependency, data)) add_file_fingerprint(inputs, project, "component_report", project / "receipts" / "emitted_components.json") - add_file_fingerprint(inputs, project, "ppt_master_asset_usage", project / "receipts" / "ppt-master-asset-usage.json") + add_file_fingerprint(inputs, project, "design_pattern_usage", project / "receipts" / "design-pattern-usage.json") add_file_fingerprint(inputs, project, "component_waiver", project / "receipts" / "emitted-components-waiver.json") add_file_fingerprint(inputs, project, "allowlist_receipt", project / "receipts" / "allowlist.json") try: @@ -839,7 +955,7 @@ def run_preflight(project: Path, data: dict[str, Any]) -> dict[str, Any]: args.extend(["--input", str(safe_existing_file(path, suffix=".svg", root=project))]) result = run_command(args, cwd=repo_root()) log_path(project, "preflight").write_text(result.stdout + result.stderr, encoding="utf-8") - parsed = parse_json_output(result.stdout) + parsed = parse_json_output(result.stdout) or parse_json_output(result.stderr) if result.returncode != 0: raise RunnerError(f"preflight failed: {result.returncode}; see {log_path(project, 'preflight')}") return { @@ -860,6 +976,22 @@ def parse_json_output(text: str) -> Any: return None +def is_cli_auth_config_failure(result: CommandResult, parsed: Any) -> bool: + combined = f"{result.stdout}\n{result.stderr}".lower() + if "keychain get failed" in combined or ("keychain" in combined and "not initialized" in combined): + return True + if not isinstance(parsed, dict): + return False + error = parsed.get("error") + if not isinstance(error, dict): + return False + error_type = text_from_any(error.get("type")).lower() + message = text_from_any(error.get("message") or error.get("hint")).lower() + return error_type in {"auth", "authentication", "config"} and any( + token in message for token in ["keychain", "credential", "auth", "login"] + ) + + def build_create_svg_command(project: Path, data: dict[str, Any], cli: str, *, dry_run: bool) -> list[str]: inputs = project_pages(project, data, prepared=True) if not inputs: @@ -1018,6 +1150,33 @@ def extract_stage_counts(stage: str, receipt: dict[str, Any], *, allow_waived: b raise RunnerError(f"cannot extract counts for stage: {stage}") +def preflight_strategist_contract_summary(receipt: dict[str, Any]) -> dict[str, Any]: + issues = nested(receipt, ["summary", "plan", "issues"]) + if not isinstance(issues, list): + issues = [] + matched: list[dict[str, Any]] = [] + for item in issues: + if not isinstance(item, dict): + continue + code = text_from_any(item.get("code")) + if code in STRATEGIST_CONTRACT_CODES: + matched.append( + { + "level": text_from_any(item.get("level")) or "error", + "code": code, + "page": item.get("page"), + } + ) + return { + "status": "passed" if not any(item["level"] == "error" for item in matched) else "failed", + "issue_count": len(matched), + "error_count": sum(1 for item in matched if item["level"] == "error"), + "warning_count": sum(1 for item in matched if item["level"] == "warning"), + "issue_codes": sorted({item["code"] for item in matched}), + "issues": matched, + } + + def dry_run_waiver_allowed( data: dict[str, Any], args: argparse.Namespace | None = None, *, project: Path | None = None ) -> bool: @@ -1061,6 +1220,294 @@ def component_report_summary(project: Path, data: dict[str, Any], args: argparse raise RunnerError("quality gate requires receipts/emitted_components.json or active legacy component waiver") +def component_bbox_area(component: dict[str, Any]) -> float: + bbox = component.get("bbox") + if not isinstance(bbox, dict): + return 0.0 + width = bbox.get("width") + height = bbox.get("height") + if not isinstance(width, (int, float)) or isinstance(width, bool): + return 0.0 + if not isinstance(height, (int, float)) or isinstance(height, bool): + return 0.0 + return max(0.0, float(width)) * max(0.0, float(height)) + + +def component_tokens(component: dict[str, Any], key: str) -> set[str]: + values = component.get(key) + if not isinstance(values, list): + return set() + return {text_from_any(value) for value in values if text_from_any(value)} + + +def bbox_area(box: dict[str, float]) -> float: + return max(0.0, float(box.get("width", 0.0))) * max(0.0, float(box.get("height", 0.0))) + + +def bbox_right(box: dict[str, float]) -> float: + return float(box.get("x", 0.0)) + float(box.get("width", 0.0)) + + +def bbox_bottom(box: dict[str, float]) -> float: + return float(box.get("y", 0.0)) + float(box.get("height", 0.0)) + + +def bbox_overlap_area(left: dict[str, float], right: dict[str, float]) -> float: + x1 = max(float(left.get("x", 0.0)), float(right.get("x", 0.0))) + y1 = max(float(left.get("y", 0.0)), float(right.get("y", 0.0))) + x2 = min(bbox_right(left), bbox_right(right)) + y2 = min(bbox_bottom(left), bbox_bottom(right)) + return max(0.0, x2 - x1) * max(0.0, y2 - y1) + + +def component_bbox(component: dict[str, Any]) -> dict[str, float] | None: + raw = component.get("bbox") + if not isinstance(raw, dict): + return None + out: dict[str, float] = {} + for key in ("x", "y", "width", "height"): + value = raw.get(key) + if not isinstance(value, (int, float)) or isinstance(value, bool): + return None + out[key] = float(value) + if out["width"] <= 0 or out["height"] <= 0: + return None + return out + + +def source_element_tokens(element: ET.Element) -> set[str]: + name = svg_preflight.local_name(element.tag).lower() + identifier = svg_preflight.element_identifier_text(element).lower() + tokens: set[str] = set() + if name == "foreignobject": + tokens.add("typography") + if name in {"rect", "circle", "ellipse", "path", "line", "polygon", "polyline"}: + tokens.add("geometric_shape") + if name in {"path", "line", "polyline"} or re.search(r"(route|journey|flow|loop|path|spine|ribbon|connector)", identifier): + tokens.update({"path", "connector"}) + if re.search(r"(chart|metric|kpi|bar|plot|donut|bubble|sankey|variance|insight-strip)", identifier): + tokens.add("micro_chart") + if re.search(r"(dashboard|grid|metric|kpi|panel|card)", identifier): + tokens.add("dashboard") + if re.search(r"(label|legend|caption|note|callout|annotation|insight)", identifier): + tokens.add("annotation") + if re.search(r"(icon|node|satellite|glyph|hub)", identifier): + tokens.add("icon") + if re.search(r"(spotlight|hotspot|highlight|focus)", identifier): + tokens.add("spotlight") + return tokens + + +def collect_source_page_elements(project: Path, data: dict[str, Any]) -> dict[int, list[dict[str, Any]]]: + elements_by_page: dict[int, list[dict[str, Any]]] = {} + manifest_pages = data.get("pages") + if isinstance(manifest_pages, list) and manifest_pages: + page_paths: list[tuple[int, Path]] = [] + for index, page in enumerate(manifest_pages, 1): + if not isinstance(page, dict): + continue + try: + page_number = int(page.get("page", index)) + except (TypeError, ValueError): + page_number = index + raw_path = text_from_any(page.get("prepared_svg")) + if raw_path: + page_paths.append((page_number, project_file(project, raw_path))) + if not page_paths: + page_paths = [(index, path) for index, path in enumerate(project_pages(project, data, prepared=True), 1)] + else: + page_paths = [(index, path) for index, path in enumerate(project_pages(project, data, prepared=True), 1)] + for page_number, path in page_paths: + try: + root = ET.parse(path).getroot() + except ET.ParseError: + continue + entries: list[dict[str, Any]] = [] + for element in root.iter(): + if svg_preflight.svg_role(element) not in {"shape", "image"}: + continue + bbox = svg_preflight.bbox_for_element(element) + if bbox is None or bbox_area(bbox) <= 0: + continue + entries.append( + { + "id": svg_preflight.element_identifier_text(element), + "tag": svg_preflight.local_name(element.tag).lower(), + "bbox": bbox, + "tokens": source_element_tokens(element), + } + ) + elements_by_page[page_number] = entries + return elements_by_page + + +def source_tokens_for_component(component: dict[str, Any], source_elements: list[dict[str, Any]]) -> tuple[set[str], dict[str, float]]: + box = component_bbox(component) + if box is None: + return set(), {} + tokens: set[str] = set() + token_area: dict[str, float] = {} + for entry in source_elements: + source_bbox = entry.get("bbox") + if not isinstance(source_bbox, dict): + continue + overlap = bbox_overlap_area(box, source_bbox) + if overlap <= 4: + continue + for token in entry.get("tokens", set()): + if not token: + continue + tokens.add(token) + token_area[token] = token_area.get(token, 0.0) + overlap + return tokens, token_area + + +def source_supports_visual_evidence(component: dict[str, Any], source_elements: list[dict[str, Any]], evidence: str) -> bool: + tokens, token_area = source_tokens_for_component(component, source_elements) + area = component_bbox_area(component) + if evidence in {"chart_geometry", "metric_hierarchy", "dashboard_grid"}: + return bool({"micro_chart", "dashboard"} & tokens) and max(token_area.get("micro_chart", 0.0), token_area.get("dashboard", 0.0)) >= min(area, 5_000) + if evidence in {"numbered_path", "hero_route", "connector_flow", "flow_lanes", "phase_spine", "closing_ribbon"}: + return bool({"path", "connector"} & tokens) and max(token_area.get("path", 0.0), token_area.get("connector", 0.0)) >= min(area, 5_000) + if evidence in {"full_page_archetype", "hero_signal", "decision_matrix", "contrast_panels"}: + return bool({"geometric_shape", "path", "connector"} & tokens) and max(token_area.get("geometric_shape", 0.0), token_area.get("path", 0.0), token_area.get("connector", 0.0)) >= min(area, 20_000) + if evidence == "sector_field": + return "geometric_shape" in tokens and token_area.get("geometric_shape", 0.0) >= min(area, 10_000) + if evidence == "hub_spoke": + return bool({"connector", "icon", "geometric_shape"} & tokens) and sum(token_area.get(token, 0.0) for token in {"connector", "icon", "geometric_shape"}) >= min(area, 10_000) + if evidence == "insight_strip": + return bool({"micro_chart", "geometric_shape"} & tokens) and max(token_area.get("micro_chart", 0.0), token_area.get("geometric_shape", 0.0)) >= min(area, 5_000) + if evidence in {"title_field", "section_index", "semantic_labels", "action_cards"}: + return "typography" in tokens and token_area.get("typography", 0.0) >= 1_000 + if evidence == "spotlight": + return bool({"annotation", "geometric_shape", "spotlight"} & tokens) and sum(token_area.get(token, 0.0) for token in {"annotation", "geometric_shape", "spotlight"}) >= min(area, 5_000) + return evidence in tokens + + +def component_supports_visual_evidence(component: dict[str, Any], evidence: str) -> bool: + primitives = component_tokens(component, "primitives") + effects = component_tokens(component, "effects") + area = component_bbox_area(component) + if evidence in {"chart_geometry", "metric_hierarchy", "dashboard_grid"}: + return bool({"micro_chart", "dashboard"} & primitives) and area >= 5_000 + if evidence in {"numbered_path", "hero_route", "connector_flow", "flow_lanes", "phase_spine", "closing_ribbon"}: + return bool({"path", "connector"} & primitives) and area >= 5_000 + if evidence in {"full_page_archetype", "hero_signal", "decision_matrix", "contrast_panels"}: + return bool({"geometric_shape", "path", "connector"} & primitives) and area >= 20_000 + if evidence == "sector_field": + return "geometric_shape" in primitives and area >= 10_000 + if evidence == "hub_spoke": + return bool({"connector", "icon", "geometric_shape"} & primitives) and area >= 10_000 + if evidence == "insight_strip": + return bool({"micro_chart", "geometric_shape"} & primitives) and area >= 5_000 + if evidence in {"title_field", "section_index", "semantic_labels", "action_cards"}: + return "typography" in primitives and area >= 1_000 + if evidence == "spotlight": + return bool({"annotation", "geometric_shape"} & primitives) and area >= 5_000 + return evidence in primitives or evidence in effects + + +def component_proves_visual_evidence(component: dict[str, Any], source_elements: list[dict[str, Any]], evidence: str) -> bool: + return component_supports_visual_evidence(component, evidence) and source_supports_visual_evidence(component, source_elements, evidence) + + +def collect_component_page_evidence(report: dict[str, Any], source_elements_by_page: dict[int, list[dict[str, Any]]] | None = None) -> dict[int, list[dict[str, Any]]]: + components_by_page: dict[int, list[dict[str, Any]]] = {} + pages = report.get("pages") + if not isinstance(pages, list): + return components_by_page + for page in pages: + if not isinstance(page, dict): + continue + try: + page_number = int(page.get("page")) + except (TypeError, ValueError): + continue + components_by_page.setdefault(page_number, []) + components = page.get("components") + if not isinstance(components, list): + continue + for component in components: + if not isinstance(component, dict): + continue + components_by_page[page_number].append(component) + return components_by_page + + +def visual_contract_required_for_plan(plan: dict[str, Any]) -> bool: + return text_from_any(plan.get("schema_version")).startswith("svglide-strategist-contract/") + + +def visual_design_contract_summary(project: Path, data: dict[str, Any], args: argparse.Namespace | None = None) -> dict[str, Any]: + plan = read_json(plan_file(project, data), {}) + if not isinstance(plan, dict): + return {"status": "not_configured", "page_count": 0, "error_count": 0, "warning_count": 0} + slides = plan.get("slides") + if not isinstance(slides, list): + slides = [] + required_for_plan = visual_contract_required_for_plan(plan) + configured_count = 0 + issues: list[dict[str, Any]] = [] + page_requirements: dict[int, list[str]] = {} + required_fields = ("visual_thesis", "composition_archetype", "primary_motif", "required_visual_evidence") + for index, slide in enumerate(slides, 1): + if not isinstance(slide, dict): + continue + page = slide.get("page", index) + try: + page_number = int(page) + except (TypeError, ValueError): + page_number = index + contract = slide.get("visual_design_contract") + if not isinstance(contract, dict): + if required_for_plan: + issues.append({"page": page_number, "code": "missing_visual_design_contract"}) + continue + configured_count += 1 + for field in required_fields: + value = contract.get(field) + if value in (None, "", []): + issues.append({"page": page_number, "code": f"missing_{field}"}) + raw_evidence = contract.get("required_visual_evidence") + evidence = [text_from_any(item) for item in raw_evidence] if isinstance(raw_evidence, list) else [] + evidence = [item for item in evidence if item] + page_requirements[page_number] = evidence + + if configured_count == 0 and not issues: + return {"status": "not_configured", "page_count": 0, "error_count": 0, "warning_count": 0} + + report_path = project / "receipts" / "emitted_components.json" + if not report_path.exists(): + return { + "status": "failed", + "page_count": configured_count, + "error_count": 1, + "warning_count": 0, + "issues": [{"code": "missing_component_report"}], + "path": rel_to_project(project, report_path), + } + report = read_json(report_path, {}) + if not isinstance(report, dict): + raise RunnerError("emitted_components.json must contain an object") + source_elements_by_page = collect_source_page_elements(project, data) + components_by_page = collect_component_page_evidence(report, source_elements_by_page) + for page_number, required_evidence in sorted(page_requirements.items()): + components = components_by_page.get(page_number, []) + source_elements = source_elements_by_page.get(page_number, []) + for evidence in required_evidence: + if not any(component_proves_visual_evidence(component, source_elements, evidence) for component in components): + issues.append({"page": page_number, "code": "missing_visual_evidence", "evidence": evidence}) + + return { + "status": "passed" if not issues else "failed", + "page_count": configured_count, + "error_count": len(issues), + "warning_count": 0, + "issues": issues, + "path": rel_to_project(project, report_path), + } + + def asset_id_from(value: Any) -> str: if isinstance(value, str): return value.strip() @@ -1081,12 +1528,12 @@ def add_asset_ids(output: set[str], value: Any) -> None: output.add(asset_id) -def collect_ppt_master_asset_ids(project: Path, data: dict[str, Any]) -> set[str]: +def collect_design_pattern_ids(project: Path, data: dict[str, Any]) -> set[str]: plan = read_json(plan_file(project, data), {}) if not isinstance(plan, dict): return set() asset_ids: set[str] = set() - selection = plan.get("ppt_master_asset_selection") + selection = plan.get("design_pattern_selection") if isinstance(selection, dict): add_asset_ids(asset_ids, selection.get("selected_assets")) slides = plan.get("slides") @@ -1095,20 +1542,20 @@ def collect_ppt_master_asset_ids(project: Path, data: dict[str, Any]) -> set[str for slide in slides: if not isinstance(slide, dict): continue - add_asset_ids(asset_ids, slide.get("ppt_master_reference_assets")) + add_asset_ids(asset_ids, slide.get("design_reference_assets")) visual_plan = slide.get("visual_plan") if isinstance(visual_plan, dict): - add_asset_ids(asset_ids, visual_plan.get("ppt_master_reference_assets")) + add_asset_ids(asset_ids, visual_plan.get("design_reference_assets")) svg_files = plan.get("svg_files") if not isinstance(svg_files, list): svg_files = [] for slide in svg_files: if isinstance(slide, dict): - add_asset_ids(asset_ids, slide.get("ppt_master_reference_assets")) + add_asset_ids(asset_ids, slide.get("design_reference_assets")) return asset_ids -def collect_used_ppt_master_asset_ids(report: dict[str, Any]) -> set[str]: +def collect_used_design_pattern_ids(report: dict[str, Any]) -> set[str]: used: set[str] = set() usages = report.get("page_usages") if isinstance(usages, list): @@ -1126,7 +1573,7 @@ def collect_used_ppt_master_asset_ids(report: dict[str, Any]) -> set[str]: return used -def active_ppt_master_asset_waivers(report: dict[str, Any]) -> tuple[set[str], list[dict[str, Any]]]: +def active_design_pattern_waivers(report: dict[str, Any]) -> tuple[set[str], list[dict[str, Any]]]: waived: set[str] = set() waivers: list[dict[str, Any]] = [] raw_waivers = report.get("waived_assets") @@ -1139,16 +1586,23 @@ def active_ppt_master_asset_waivers(report: dict[str, Any]) -> tuple[set[str], l validation = validate_legacy_waiver(item) if asset_id and validation.get("active"): waived.add(asset_id) - waivers.append({"asset_id": asset_id, "type": "ppt_master_asset_usage", **validation}) + waivers.append({"asset_id": asset_id, "type": "design_pattern_usage", **validation}) return waived, waivers -def ppt_master_asset_usage_summary(project: Path, data: dict[str, Any], args: argparse.Namespace | None = None) -> dict[str, Any]: - expected = collect_ppt_master_asset_ids(project, data) +def strict_design_pattern_usage_required( + data: dict[str, Any], args: argparse.Namespace | None = None, project: Path | None = None +) -> bool: + profile = resolved_validation_profile(data, args, project=project) + return is_production_lane(data, args, project=project) or profile == "golden" + + +def design_pattern_usage_summary(project: Path, data: dict[str, Any], args: argparse.Namespace | None = None) -> dict[str, Any]: + expected = collect_design_pattern_ids(project, data) if not expected: return {"status": "not_configured", "selected_count": 0, "error_count": 0, "warning_count": 0} - report_path = project / "receipts" / "ppt-master-asset-usage.json" + report_path = project / "receipts" / "design-pattern-usage.json" if not report_path.exists(): return { "status": "missing", @@ -1161,9 +1615,11 @@ def ppt_master_asset_usage_summary(project: Path, data: dict[str, Any], args: ar report = read_json(report_path) if not isinstance(report, dict): - raise RunnerError("ppt-master-asset-usage.json must contain an object") - if report.get("schema_version") not in {"", None, PPT_MASTER_ASSET_USAGE_SCHEMA}: - raise RunnerError("ppt-master-asset-usage.json has unsupported schema_version") + raise RunnerError("design-pattern-usage.json must contain an object") + if strict_design_pattern_usage_required(data, args, project=project) and report.get("schema_version") != DESIGN_PATTERN_USAGE_SCHEMA: + raise RunnerError(f"design-pattern-usage.json schema_version must be {DESIGN_PATTERN_USAGE_SCHEMA}") + if report.get("schema_version") not in {"", None, DESIGN_PATTERN_USAGE_SCHEMA}: + raise RunnerError("design-pattern-usage.json has unsupported schema_version") status = text_from_any(report.get("status") or "passed") if status not in {"passed", "verified", "ok"}: return { @@ -1174,11 +1630,11 @@ def ppt_master_asset_usage_summary(project: Path, data: dict[str, Any], args: ar "path": rel_to_project(project, report_path), } - used = collect_used_ppt_master_asset_ids(report) - waived, waivers = active_ppt_master_asset_waivers(report) + used = collect_used_design_pattern_ids(report) + waived, waivers = active_design_pattern_waivers(report) missing = sorted(expected - used - waived) - error_count = required_int(report.get("error_count", 0), "ppt_master_asset_usage.error_count") - warning_count = required_int(report.get("warning_count", 0), "ppt_master_asset_usage.warning_count") + error_count = required_int(report.get("error_count", 0), "design_pattern_usage.error_count") + warning_count = required_int(report.get("warning_count", 0), "design_pattern_usage.warning_count") if missing: error_count += 1 if waivers and not dry_run_waiver_allowed(data, args, project=project): @@ -1248,9 +1704,11 @@ def run_quality_gate(project: Path, data: dict[str, Any], args: argparse.Namespa data, args, project=project ) preflight_counts = extract_stage_counts("preflight", preflight) + strategist_contract = preflight_strategist_contract_summary(preflight) preview_counts = extract_stage_counts("preview_lint", preview_lint, allow_waived=preview_waiver_allowed) component_counts = component_report_summary(project, data, args) - ppt_master_usage = ppt_master_asset_usage_summary(project, data, args) + visual_contract = visual_design_contract_summary(project, data, args) + design_usage = design_pattern_usage_summary(project, data, args) raster_counts = raster_summary(project, data) allowlist = allowlist_summary(project, data) profile = quality_validation_profile(resolved_validation_profile(data, args, project=project)) @@ -1262,9 +1720,9 @@ def run_quality_gate(project: Path, data: dict[str, Any], args: argparse.Namespa waivers.append({"type": "preview_lint", **preview_lint.get("waiver", {})}) if component_counts["status"] == "waived" and isinstance(component_counts.get("waiver"), dict): waivers.append(component_counts["waiver"]) - ppt_master_waivers = ppt_master_usage.get("waivers") - if isinstance(ppt_master_waivers, list): - for waiver in ppt_master_waivers: + design_waivers = design_usage.get("waivers") + if isinstance(design_waivers, list): + for waiver in design_waivers: if isinstance(waiver, dict): waivers.append(waiver) @@ -1275,8 +1733,10 @@ def run_quality_gate(project: Path, data: dict[str, Any], args: argparse.Namespa failures.append("preview_lint.error_count must be 0") if component_counts["error_count"] != 0: failures.append("component_report.error_count must be 0") - if ppt_master_usage["error_count"] != 0: - failures.append("ppt-master asset usage must be proven") + if visual_contract["error_count"] != 0: + failures.append("SVGlide visual design contract must be proven") + if design_usage["error_count"] != 0: + failures.append("SVGlide design pattern usage must be proven") if raster_counts["error_count"] != 0: failures.append("raster.error_count must be 0") preview_score = preview_counts.get("visual_score") @@ -1293,7 +1753,8 @@ def run_quality_gate(project: Path, data: dict[str, Any], args: argparse.Namespa preflight_counts["warning_count"] + preview_counts["warning_count"] + component_counts["warning_count"] - + ppt_master_usage["warning_count"] + + visual_contract["warning_count"] + + design_usage["warning_count"] + raster_counts["warning_count"] ) if production or profile == "golden": @@ -1312,9 +1773,11 @@ def run_quality_gate(project: Path, data: dict[str, Any], args: argparse.Namespa "schema_version": QUALITY_GATE_SCHEMA, "status": status, "preflight": preflight_counts, + "strategist_contract": strategist_contract, "preview_lint": preview_counts, "component_report": component_counts, - "ppt_master_asset_usage": ppt_master_usage, + "visual_design_contract": visual_contract, + "design_pattern_usage": design_usage, "raster": raster_counts, "allowlist": allowlist, "validation_profile": profile, @@ -1329,12 +1792,21 @@ def run_dry_run(project: Path, data: dict[str, Any], cli: str, args: argparse.Na if args is None: args = default_runner_args() args.cli = cli + ensure_project_dirs(project) require_latest_quality_gate(project, data, args) args = build_create_svg_command(project, data, cli, dry_run=True) result = run_command(args, cwd=repo_root()) log_path(project, "dry_run").write_text(result.stdout + result.stderr, encoding="utf-8") - parsed = parse_json_output(result.stdout) + parsed = parse_json_output(result.stdout) or parse_json_output(result.stderr) if result.returncode != 0: + if is_cli_auth_config_failure(result, parsed): + return { + "status": "blocked_by_auth", + "command": args, + "log": str(log_path(project, "dry_run").relative_to(project)), + "reason": "lark-cli auth/config is unavailable in this local execution environment", + "summary": parsed, + } raise RunnerError(f"dry-run failed: {result.returncode}; see {log_path(project, 'dry_run')}") return { "status": "passed", @@ -2177,7 +2649,7 @@ def execute_stage(stage: str, project: Path, data: dict[str, Any], args: argpars started = now_ms() ensure_project_dirs(project) if stage == "generate": - body = run_manifest_command(project, data, "generate") + body = run_generate(project, data) return write_stage_receipt(project, data, stage, started, body, prepared_digest=False, args=args) if stage == "prepare": body = run_prepare(project, data) diff --git a/skills/lark-slides/scripts/svglide_project_runner_test.py b/skills/lark-slides/scripts/svglide_project_runner_test.py index 5c6b3a805..1b077fb1d 100644 --- a/skills/lark-slides/scripts/svglide_project_runner_test.py +++ b/skills/lark-slides/scripts/svglide_project_runner_test.py @@ -6,15 +6,21 @@ import contextlib import io import json import shutil +import sys import tempfile import unittest from argparse import Namespace from pathlib import Path +SCRIPT_DIR = Path(__file__).resolve().parent +sys.path.insert(0, str(SCRIPT_DIR)) + +import svg_preflight import svglide_project_runner as runner SVG = """""" +CHART_SVG = """
核心洞察
""" def write_json(path: Path, data: object) -> None: @@ -68,6 +74,41 @@ class SVGlideProjectRunnerTest(unittest.TestCase): body = runner.builtin_prepare(project, data) runner.write_stage_receipt(project, data, "prepare", runner.now_ms(), body, prepared_digest=False, args=self.args(project)) + def test_builtin_generate_composes_plan_pages_and_receipts(self) -> None: + root = Path(tempfile.mkdtemp(dir=runner.repo_root())) + self.addCleanup(lambda: shutil.rmtree(root, ignore_errors=True)) + project = root / "runtime-generate" + project.mkdir(parents=True) + write_json( + project / "project_manifest.json", + { + "deck_id": "runtime-generate", + "title": "Runtime Generate", + "plan": "slide_plan.json", + "brief": "Create an operations review with KPI dashboard and closing next steps.", + "page_descriptions": [ + "Cover: operating thesis and review scope", + "KPI dashboard: four health metrics with micro trends", + "Closing: next steps and decision request", + ], + }, + ) + data = runner.manifest(project) + + receipt = runner.execute_stage("generate", project, data, self.args(project)) + + self.assertEqual("passed", receipt["status"]) + self.assertEqual("builtin:svglide_strategist_runtime", receipt["generator"]) + self.assertTrue((project / "slide_plan.json").exists()) + self.assertTrue((project / "pages" / "page-001.svg").exists()) + self.assertTrue((project / "receipts" / "emitted_components.json").exists()) + self.assertTrue((project / "receipts" / "design-pattern-usage.json").exists()) + plan = runner.read_json(project / "slide_plan.json", {}) + self.assertEqual(3, plan["page_count"]) + self.assertIn("design_pattern_selection", plan) + self.assertEqual("briefing", plan["mode"]) + self.assertEqual({"error_count": 0, "warning_count": 0}, svg_preflight.lint_plan(plan)["summary"]) + def write_gate_inputs( self, project: Path, @@ -77,6 +118,7 @@ class SVGlideProjectRunnerTest(unittest.TestCase): preview_warning_count: int = 0, visual_score: int = 100, validation_profile: str | None = None, + preflight_plan_issues: list[dict[str, object]] | None = None, ) -> None: profile = validation_profile or runner.quality_validation_profile( runner.resolved_validation_profile(data, args, project=project) @@ -87,7 +129,16 @@ class SVGlideProjectRunnerTest(unittest.TestCase): data, "preflight", runner.now_ms(), - {"status": "passed", "summary": {"summary": {"error_count": 0, "warning_count": 0}}}, + { + "status": "passed", + "summary": { + "summary": { + "error_count": sum(1 for item in preflight_plan_issues or [] if item.get("level") == "error"), + "warning_count": sum(1 for item in preflight_plan_issues or [] if item.get("level") == "warning"), + }, + "plan": {"issues": preflight_plan_issues or [], "summary": {"error_count": 0, "warning_count": 0}}, + }, + }, args=args, ) runner.write_stage_receipt( @@ -150,6 +201,12 @@ class SVGlideProjectRunnerTest(unittest.TestCase): cli.chmod(cli.stat().st_mode | 0o111) return cli + def write_fake_cli_body(self, project: Path, body: str) -> Path: + cli = project / "fake-lark-cli" + cli.write_text(body, encoding="utf-8") + cli.chmod(cli.stat().st_mode | 0o111) + return cli + def test_builtin_prepare_copies_source_svg_to_prepared(self) -> None: project = self.make_project() data = runner.manifest(project) @@ -470,6 +527,30 @@ class SVGlideProjectRunnerTest(unittest.TestCase): self.assertEqual(body["visual_score_threshold"], 75) self.assertEqual(body["visual_score_mode"], "advisory") + def test_quality_gate_records_preflight_strategist_contract_issues(self) -> None: + project = self.make_project() + data = runner.manifest(project) + args = self.args(project) + self.write_gate_inputs( + project, + data, + args, + preflight_plan_issues=[ + { + "level": "warning", + "code": "plan_missing_page_rhythm", + "message": "multi-page SVGlide plans must declare page_rhythm", + } + ], + ) + self.write_component_report(project) + + body = runner.run_quality_gate(project, data, args) + + self.assertEqual(body["status"], "passed") + self.assertEqual(body["strategist_contract"]["warning_count"], 1) + self.assertIn("plan_missing_page_rhythm", body["strategist_contract"]["issue_codes"]) + def test_quality_gate_rejects_production_score_below_threshold(self) -> None: project = self.make_project() data = runner.manifest(project) @@ -494,7 +575,7 @@ class SVGlideProjectRunnerTest(unittest.TestCase): with self.assertRaisesRegex(runner.RunnerError, "golden warning_budget must be 0"): runner.run_quality_gate(project, data, args) - def test_quality_gate_rejects_unproven_ppt_master_asset_selection(self) -> None: + def test_quality_gate_rejects_unproven_design_pattern_selection(self) -> None: project = self.make_project() data = runner.manifest(project) write_json( @@ -502,7 +583,7 @@ class SVGlideProjectRunnerTest(unittest.TestCase): { "output_mode": "svglide-svg", "title": "Demo Deck", - "ppt_master_asset_selection": { + "design_pattern_selection": { "selected_assets": [ {"id": "chart.timeline", "kind": "chart_template"}, ], @@ -513,10 +594,10 @@ class SVGlideProjectRunnerTest(unittest.TestCase): self.write_gate_inputs(project, data, args) self.write_component_report(project) - with self.assertRaisesRegex(runner.RunnerError, "ppt-master asset usage"): + with self.assertRaisesRegex(runner.RunnerError, "SVGlide design pattern usage"): runner.run_quality_gate(project, data, args) - def test_quality_gate_accepts_proven_ppt_master_asset_selection(self) -> None: + def test_quality_gate_accepts_proven_design_pattern_selection(self) -> None: project = self.make_project() data = runner.manifest(project) write_json( @@ -524,7 +605,7 @@ class SVGlideProjectRunnerTest(unittest.TestCase): { "output_mode": "svglide-svg", "title": "Demo Deck", - "ppt_master_asset_selection": { + "design_pattern_selection": { "selected_assets": [ {"id": "chart.timeline", "kind": "chart_template"}, ], @@ -535,9 +616,9 @@ class SVGlideProjectRunnerTest(unittest.TestCase): self.write_gate_inputs(project, data, args) self.write_component_report(project) write_json( - project / "receipts" / "ppt-master-asset-usage.json", + project / "receipts" / "design-pattern-usage.json", { - "schema_version": "svglide-ppt-master-asset-usage/v1", + "schema_version": "svglide-design-pattern-usage/v1", "status": "passed", "used_asset_ids": ["chart.timeline"], "page_usages": [ @@ -556,7 +637,300 @@ class SVGlideProjectRunnerTest(unittest.TestCase): body = runner.run_quality_gate(project, data, args) self.assertEqual(body["status"], "passed") - self.assertEqual(body["ppt_master_asset_usage"]["used_count"], 1) + self.assertEqual(body["design_pattern_usage"]["used_count"], 1) + + def test_quality_gate_rejects_unproven_visual_design_contract(self) -> None: + project = self.make_project() + data = runner.manifest(project) + write_json( + project / "slide_plan.json", + { + "schema_version": "svglide-strategist-contract/v1", + "output_mode": "svglide-svg", + "slides": [ + { + "page": 1, + "visual_design_contract": { + "visual_thesis": "Show the capital flow", + "composition_archetype": "data_stage", + "primary_motif": "takeaway_chart", + "required_visual_evidence": ["chart_geometry", "insight_strip"], + }, + } + ], + }, + ) + args = self.args(project) + self.write_gate_inputs(project, data, args) + self.write_structured_component_report(project) + + with self.assertRaisesRegex(runner.RunnerError, "visual design contract"): + runner.run_quality_gate(project, data, args) + + def test_quality_gate_accepts_proven_visual_design_contract(self) -> None: + project = self.make_project() + (project / "pages" / "page-001.svg").write_text(CHART_SVG, encoding="utf-8") + data = runner.manifest(project) + write_json( + project / "slide_plan.json", + { + "schema_version": "svglide-strategist-contract/v1", + "output_mode": "svglide-svg", + "slides": [ + { + "page": 1, + "visual_design_contract": { + "visual_thesis": "Show the capital flow", + "composition_archetype": "data_stage", + "primary_motif": "takeaway_chart", + "required_visual_evidence": ["chart_geometry", "insight_strip"], + }, + } + ], + }, + ) + args = self.args(project) + self.write_gate_inputs(project, data, args) + write_json( + project / "receipts" / "emitted_components.json", + { + "schema_version": "svglide-component-report/v1", + "status": "passed", + "pages": [ + { + "page": 1, + "components": [ + { + "id": "contract-bar", + "renderer_id": "contract.bar", + "bbox": {"x": 80, "y": 120, "width": 760, "height": 300}, + "primitives": ["typography", "micro_chart"], + "effects": ["chart_geometry", "insight_strip"], + } + ], + } + ], + "summary": {"error_count": 0, "warning_count": 0}, + }, + ) + + body = runner.run_quality_gate(project, data, args) + + self.assertEqual(body["status"], "passed") + self.assertEqual(body["visual_design_contract"]["status"], "passed") + self.assertEqual(body["visual_design_contract"]["page_count"], 1) + + def test_quality_gate_rejects_visual_contract_receipt_not_backed_by_prepared_svg(self) -> None: + project = self.make_project() + data = runner.manifest(project) + write_json( + project / "slide_plan.json", + { + "schema_version": "svglide-strategist-contract/v1", + "output_mode": "svglide-svg", + "slides": [ + { + "page": 1, + "visual_design_contract": { + "visual_thesis": "Show the capital flow", + "composition_archetype": "data_stage", + "primary_motif": "takeaway_chart", + "required_visual_evidence": ["chart_geometry", "insight_strip"], + }, + } + ], + }, + ) + args = self.args(project) + self.write_gate_inputs(project, data, args) + write_json( + project / "receipts" / "emitted_components.json", + { + "schema_version": "svglide-component-report/v1", + "status": "passed", + "pages": [ + { + "page": 1, + "components": [ + { + "id": "contract-bar", + "renderer_id": "contract.bar", + "bbox": {"x": 80, "y": 120, "width": 760, "height": 300}, + "primitives": ["typography", "micro_chart"], + "effects": ["chart_geometry", "insight_strip"], + } + ], + } + ], + "summary": {"error_count": 0, "warning_count": 0}, + }, + ) + + with self.assertRaisesRegex(runner.RunnerError, "visual design contract"): + runner.run_quality_gate(project, data, args) + + def test_quality_gate_rejects_visual_contract_primitive_only_receipt(self) -> None: + project = self.make_project() + data = runner.manifest(project) + write_json( + project / "slide_plan.json", + { + "schema_version": "svglide-strategist-contract/v1", + "output_mode": "svglide-svg", + "slides": [ + { + "page": 1, + "visual_design_contract": { + "visual_thesis": "Show typography", + "composition_archetype": "text_stage", + "primary_motif": "title_field", + "required_visual_evidence": ["typography"], + }, + } + ], + }, + ) + args = self.args(project) + self.write_gate_inputs(project, data, args) + write_json( + project / "receipts" / "emitted_components.json", + { + "schema_version": "svglide-component-report/v1", + "status": "passed", + "pages": [ + { + "page": 1, + "components": [ + { + "id": "fake-typography", + "renderer_id": "contract.fake", + "bbox": {"x": 80, "y": 120, "width": 760, "height": 300}, + "primitives": ["typography"], + "effects": [], + } + ], + } + ], + "summary": {"error_count": 0, "warning_count": 0}, + }, + ) + + with self.assertRaisesRegex(runner.RunnerError, "visual design contract"): + runner.run_quality_gate(project, data, args) + + def test_quality_gate_binds_source_proof_by_manifest_page_number(self) -> None: + project = self.make_project() + write_json( + project / "project_manifest.json", + { + "deck_id": "demo", + "title": "Demo Deck", + "plan": "slide_plan.json", + "pages": [ + {"page": 2, "source_svg": "pages/page-002.svg", "prepared_svg": "prepared/page-002.svg"}, + {"page": 1, "source_svg": "pages/page-001.svg", "prepared_svg": "prepared/page-001.svg"}, + ], + "stage_commands": {"prepare": "builtin:copy_and_normalize_svg"}, + }, + ) + (project / "pages" / "page-001.svg").write_text(SVG, encoding="utf-8") + (project / "pages" / "page-002.svg").write_text(CHART_SVG, encoding="utf-8") + data = runner.manifest(project) + write_json( + project / "slide_plan.json", + { + "schema_version": "svglide-strategist-contract/v1", + "output_mode": "svglide-svg", + "slides": [ + { + "page": 2, + "visual_design_contract": { + "visual_thesis": "Show the capital flow", + "composition_archetype": "data_stage", + "primary_motif": "takeaway_chart", + "required_visual_evidence": ["chart_geometry"], + }, + } + ], + }, + ) + args = self.args(project) + self.write_gate_inputs(project, data, args) + write_json( + project / "receipts" / "emitted_components.json", + { + "schema_version": "svglide-component-report/v1", + "status": "passed", + "pages": [ + { + "page": 2, + "components": [ + { + "id": "contract-bar", + "renderer_id": "contract.bar", + "bbox": {"x": 80, "y": 120, "width": 760, "height": 300}, + "primitives": ["typography", "micro_chart"], + "effects": ["chart_geometry"], + } + ], + } + ], + "summary": {"error_count": 0, "warning_count": 0}, + }, + ) + + body = runner.run_quality_gate(project, data, args) + + self.assertEqual(body["visual_design_contract"]["status"], "passed") + + def test_quality_gate_rejects_evidence_effect_without_matching_geometry(self) -> None: + project = self.make_project() + data = runner.manifest(project) + write_json( + project / "slide_plan.json", + { + "schema_version": "svglide-strategist-contract/v1", + "output_mode": "svglide-svg", + "slides": [ + { + "page": 1, + "visual_design_contract": { + "visual_thesis": "Show a chart takeaway", + "composition_archetype": "data_stage", + "primary_motif": "takeaway_chart", + "required_visual_evidence": ["chart_geometry"], + }, + } + ], + }, + ) + args = self.args(project) + self.write_gate_inputs(project, data, args) + write_json( + project / "receipts" / "emitted_components.json", + { + "schema_version": "svglide-component-report/v1", + "status": "passed", + "pages": [ + { + "page": 1, + "components": [ + { + "id": "ordinary-text-box", + "renderer_id": "demo.text", + "bbox": {"x": 80, "y": 120, "width": 360, "height": 48}, + "primitives": ["typography"], + "effects": ["chart_geometry"], + } + ], + } + ], + "summary": {"error_count": 0, "warning_count": 0}, + }, + ) + + with self.assertRaisesRegex(runner.RunnerError, "visual design contract"): + runner.run_quality_gate(project, data, args) def test_quality_gate_rejects_selected_assets_not_used_by_receipt(self) -> None: project = self.make_project() @@ -566,7 +940,7 @@ class SVGlideProjectRunnerTest(unittest.TestCase): { "output_mode": "svglide-svg", "title": "Demo Deck", - "ppt_master_asset_selection": { + "design_pattern_selection": { "selected_assets": [ {"id": "chart.bubble_chart", "kind": "chart_template"}, {"id": "chart.hub_spoke", "kind": "chart_template"}, @@ -578,9 +952,9 @@ class SVGlideProjectRunnerTest(unittest.TestCase): self.write_gate_inputs(project, data, args) self.write_component_report(project) write_json( - project / "receipts" / "ppt-master-asset-usage.json", + project / "receipts" / "design-pattern-usage.json", { - "schema_version": "svglide-ppt-master-asset-usage/v1", + "schema_version": "svglide-design-pattern-usage/v1", "status": "passed", "page_usages": [ { @@ -595,10 +969,10 @@ class SVGlideProjectRunnerTest(unittest.TestCase): }, ) - with self.assertRaisesRegex(runner.RunnerError, "ppt-master asset usage"): + with self.assertRaisesRegex(runner.RunnerError, "SVGlide design pattern usage"): runner.run_quality_gate(project, data, args) - def test_quality_gate_ignores_disabled_ppt_master_selected_assets(self) -> None: + def test_quality_gate_ignores_disabled_design_pattern_selected_assets(self) -> None: project = self.make_project() data = runner.manifest(project) write_json( @@ -606,7 +980,7 @@ class SVGlideProjectRunnerTest(unittest.TestCase): { "output_mode": "svglide-svg", "title": "Demo Deck", - "ppt_master_asset_selection": { + "design_pattern_selection": { "selected_assets": [ {"id": "chart.bubble_chart", "kind": "chart_template", "enabled": True}, {"id": "chart.hub_spoke", "kind": "chart_template", "enabled": False}, @@ -618,9 +992,9 @@ class SVGlideProjectRunnerTest(unittest.TestCase): self.write_gate_inputs(project, data, args) self.write_component_report(project) write_json( - project / "receipts" / "ppt-master-asset-usage.json", + project / "receipts" / "design-pattern-usage.json", { - "schema_version": "svglide-ppt-master-asset-usage/v1", + "schema_version": "svglide-design-pattern-usage/v1", "status": "passed", "page_usages": [ { @@ -638,8 +1012,8 @@ class SVGlideProjectRunnerTest(unittest.TestCase): body = runner.run_quality_gate(project, data, args) self.assertEqual(body["status"], "passed") - self.assertEqual(body["ppt_master_asset_usage"]["selected_count"], 1) - self.assertNotIn("chart.hub_spoke", body["ppt_master_asset_usage"]["missing_asset_ids"]) + self.assertEqual(body["design_pattern_usage"]["selected_count"], 1) + self.assertNotIn("chart.hub_spoke", body["design_pattern_usage"]["missing_asset_ids"]) def test_quality_gate_rejects_shallow_component_report_for_golden(self) -> None: project = self.make_project() @@ -653,6 +1027,46 @@ class SVGlideProjectRunnerTest(unittest.TestCase): with self.assertRaisesRegex(runner.RunnerError, "component report schema_version"): runner.run_quality_gate(project, data, args) + def test_quality_gate_rejects_legacy_design_usage_receipt_for_golden(self) -> None: + project = self.make_project() + data = runner.manifest(project) + data["validation_profile"] = {"profile": "golden"} + write_json(project / "project_manifest.json", data) + write_json( + project / "slide_plan.json", + { + "output_mode": "svglide-svg", + "title": "Demo Deck", + "design_pattern_selection": { + "selected_assets": [ + {"id": "chart.timeline", "kind": "chart_template"}, + ], + }, + }, + ) + args = self.args(project) + self.write_gate_inputs(project, data, args) + self.write_structured_component_report(project) + write_json( + project / "receipts" / "design-pattern-usage.json", + { + "status": "passed", + "page_usages": [ + { + "page": 1, + "asset_id": "chart.timeline", + "component_ids": ["component.timeline.1"], + "source_trace": "derived_renderer_contract", + } + ], + "error_count": 0, + "warning_count": 0, + }, + ) + + with self.assertRaisesRegex(runner.RunnerError, "design-pattern-usage.json schema_version"): + runner.run_quality_gate(project, data, args) + def test_quality_gate_accepts_structured_component_report_for_golden(self) -> None: project = self.make_project() data = runner.manifest(project) @@ -688,7 +1102,7 @@ class SVGlideProjectRunnerTest(unittest.TestCase): self.assertEqual(body["status"], "passed") self.assertEqual(body["component_report"]["status"], "passed") - def test_quality_gate_rejects_ppt_master_usage_without_page_trace(self) -> None: + def test_quality_gate_rejects_design_usage_without_page_trace(self) -> None: project = self.make_project() data = runner.manifest(project) write_json( @@ -696,7 +1110,7 @@ class SVGlideProjectRunnerTest(unittest.TestCase): { "output_mode": "svglide-svg", "title": "Demo Deck", - "ppt_master_asset_selection": { + "design_pattern_selection": { "selected_assets": [ {"id": "chart.timeline", "kind": "chart_template"}, ], @@ -707,9 +1121,9 @@ class SVGlideProjectRunnerTest(unittest.TestCase): self.write_gate_inputs(project, data, args) self.write_component_report(project) write_json( - project / "receipts" / "ppt-master-asset-usage.json", + project / "receipts" / "design-pattern-usage.json", { - "schema_version": "svglide-ppt-master-asset-usage/v1", + "schema_version": "svglide-design-pattern-usage/v1", "status": "passed", "used_asset_ids": ["chart.timeline"], "error_count": 0, @@ -717,10 +1131,10 @@ class SVGlideProjectRunnerTest(unittest.TestCase): }, ) - with self.assertRaisesRegex(runner.RunnerError, "ppt-master asset usage"): + with self.assertRaisesRegex(runner.RunnerError, "SVGlide design pattern usage"): runner.run_quality_gate(project, data, args) - def test_quality_gate_rejects_unproven_slide_level_ppt_master_reference(self) -> None: + def test_quality_gate_rejects_unproven_slide_level_design_pattern_reference(self) -> None: project = self.make_project() data = runner.manifest(project) write_json( @@ -732,7 +1146,7 @@ class SVGlideProjectRunnerTest(unittest.TestCase): { "page": 1, "visual_plan": { - "ppt_master_reference_assets": [ + "design_reference_assets": [ {"id": "layout.government_blue", "kind": "layout_template"}, ], }, @@ -744,7 +1158,7 @@ class SVGlideProjectRunnerTest(unittest.TestCase): self.write_gate_inputs(project, data, args) self.write_component_report(project) - with self.assertRaisesRegex(runner.RunnerError, "ppt-master asset usage"): + with self.assertRaisesRegex(runner.RunnerError, "SVGlide design pattern usage"): runner.run_quality_gate(project, data, args) def test_dry_run_requires_latest_quality_gate(self) -> None: @@ -754,6 +1168,54 @@ class SVGlideProjectRunnerTest(unittest.TestCase): with self.assertRaisesRegex(runner.RunnerError, "quality_gate"): runner.run_dry_run(project, data, "./lark-cli", self.args(project)) + def test_dry_run_marks_keychain_config_failure_as_blocked_by_auth(self) -> None: + project = self.make_project() + data = runner.manifest(project) + cli = self.write_fake_cli_body( + project, + """#!/bin/sh +if [ "$1" = "--version" ]; then + echo "lark-cli 1.2.3" + exit 0 +fi +echo '{"ok":false,"error":{"type":"config","message":"keychain Get failed: keychain not initialized"}}' +exit 1 +""", + ) + args = self.args(project, cli=str(cli)) + self.write_gate_inputs(project, data, args) + self.write_component_report(project) + quality = runner.run_quality_gate(project, data, args) + runner.write_stage_receipt(project, data, "quality_gate", runner.now_ms(), quality, args=args) + + body = runner.run_dry_run(project, data, str(cli), args) + + self.assertEqual(body["status"], "blocked_by_auth") + self.assertEqual(body["summary"]["error"]["type"], "config") + + def test_dry_run_still_fails_for_create_svg_errors(self) -> None: + project = self.make_project() + data = runner.manifest(project) + cli = self.write_fake_cli_body( + project, + """#!/bin/sh +if [ "$1" = "--version" ]; then + echo "lark-cli 1.2.3" + exit 0 +fi +echo '{"ok":false,"error":{"type":"validation","message":"bad svg"}}' +exit 1 +""", + ) + args = self.args(project, cli=str(cli)) + self.write_gate_inputs(project, data, args) + self.write_component_report(project) + quality = runner.run_quality_gate(project, data, args) + runner.write_stage_receipt(project, data, "quality_gate", runner.now_ms(), quality, args=args) + + with self.assertRaisesRegex(runner.RunnerError, "dry-run failed"): + runner.run_dry_run(project, data, str(cli), args) + def test_ppe_proof_normalizes_raw_env_proof(self) -> None: project = self.make_project() data = runner.manifest(project) diff --git a/skills/lark-slides/scripts/svglide_strategist.py b/skills/lark-slides/scripts/svglide_strategist.py new file mode 100644 index 000000000..c71b2b9c9 --- /dev/null +++ b/skills/lark-slides/scripts/svglide_strategist.py @@ -0,0 +1,675 @@ +#!/usr/bin/env python3 +# Copyright (c) 2026 Lark Technologies Pte. Ltd. +# SPDX-License-Identifier: MIT + +from __future__ import annotations + +import argparse +import copy +import json +import re +import sys +from pathlib import Path +from typing import Any + + +CONTRACT_SCHEMA_VERSION = "svglide-strategist-contract/v1" +TOKEN_RE = re.compile(r"[a-z0-9]+|[\u4e00-\u9fff]+", re.IGNORECASE) +HEX_COLOR_RE = re.compile(r"#[0-9A-Fa-f]{6}") + +CANVAS = {"width": 960, "height": 540, "viewBox": "0 0 960 540"} +SAFE_AREA = {"x": 48, "y": 40, "width": 864, "height": 460} +DEFAULT_GUARDRAILS = [ + "renderer_id must change actual geometry, not only the name", + "visual_recipe must map to SVGlide-safe primitives present in the SVG source", + "main text and chart labels stay inside safe area", + "dense page uses a structured visual carrier, not a long bullet box", + "avoid XML-like card layout unless the page has real SVG-native visual structure", +] + + +PAGE_PROFILES: dict[str, dict[str, Any]] = { + "cover": { + "seed_id": "cover_hero_statement", + "page_type": "cover", + "composition_archetype": "full_bleed_field", + "required_visual_evidence": ["full_page_archetype", "hero_route", "title_field"], + "primary_motif": "hero_route", + "chart_type": "", + "page_rhythm": "anchor", + "renderer_id": "cover_hero_statement", + "main_visual_anchor": {"layout_box_role": "visual", "description": "large thesis text paired with one abstract SVG motif"}, + "svg_effects": ["typography", "path"], + "asset_id": "chart.vertical_list", + "density_contract": "one thesis plus one visual motif", + }, + "agenda": { + "seed_id": "agenda_numbered_path", + "page_type": "agenda", + "composition_archetype": "indexed_path", + "required_visual_evidence": ["numbered_path", "section_index", "semantic_labels"], + "primary_motif": "numbered_route", + "chart_type": "", + "page_rhythm": "breathing", + "renderer_id": "agenda_numbered_path", + "main_visual_anchor": {"layout_box_role": "timeline", "description": "numbered agenda route with compact section labels"}, + "svg_effects": ["typography", "connector_flow", "path"], + "asset_id": "chart.agenda_list", + "density_contract": "agenda route >= 4 section labels", + }, + "section": { + "seed_id": "section_divider_index", + "page_type": "section_divider", + "composition_archetype": "section_signal", + "required_visual_evidence": ["section_index", "hero_signal", "full_page_archetype"], + "primary_motif": "section_index", + "chart_type": "", + "page_rhythm": "anchor", + "renderer_id": "section_divider_index", + "main_visual_anchor": {"layout_box_role": "visual", "description": "oversized chapter index paired with a full-page signal field"}, + "svg_effects": ["typography", "gradient"], + "asset_id": "chart.numbered_steps", + "density_contract": "one chapter signal plus one transition sentence", + }, + "dashboard": { + "seed_id": "dashboard_kpi_grid", + "page_type": "kpi_overview", + "composition_archetype": "data_stage", + "required_visual_evidence": ["metric_hierarchy", "chart_geometry", "dashboard_grid"], + "primary_motif": "metric_grid", + "chart_type": "", + "page_rhythm": "dense", + "renderer_id": "dashboard_kpi_grid", + "main_visual_anchor": {"layout_box_role": "chart", "description": "KPI dashboard grid with hero metrics and micro trends"}, + "svg_effects": ["typography", "chart_geometry"], + "asset_id": "chart.kpi_cards", + "density_contract": "dashboard >= 4 metrics", + }, + "roadmap": { + "seed_id": "timeline_roadmap", + "page_type": "roadmap", + "composition_archetype": "layered_timeline", + "required_visual_evidence": ["connector_flow", "phase_spine", "full_page_archetype"], + "primary_motif": "phase_spine", + "chart_type": "", + "page_rhythm": "dense", + "renderer_id": "timeline_roadmap", + "main_visual_anchor": {"layout_box_role": "timeline", "description": "milestone spine with compact phase labels"}, + "svg_effects": ["typography", "connector_flow", "path"], + "asset_id": "chart.timeline", + "density_contract": "timeline >= 3 milestones", + }, + "process": { + "seed_id": "process_pipeline", + "page_type": "process_flow", + "composition_archetype": "layered_timeline", + "required_visual_evidence": ["connector_flow", "flow_lanes", "full_page_archetype"], + "primary_motif": "flow_route", + "chart_type": "", + "page_rhythm": "dense", + "renderer_id": "process_pipeline", + "main_visual_anchor": {"layout_box_role": "flow", "description": "left-to-right process path with input and output anchors"}, + "svg_effects": ["typography", "connector_flow", "path"], + "asset_id": "chart.process_flow", + "density_contract": "process path >= 4 steps", + }, + "comparison": { + "seed_id": "comparison_two_column_decision", + "page_type": "comparison", + "composition_archetype": "comparison_matrix", + "required_visual_evidence": ["decision_matrix", "contrast_panels", "semantic_labels"], + "primary_motif": "decision_axis", + "chart_type": "", + "page_rhythm": "dense", + "renderer_id": "comparison_two_column_decision", + "main_visual_anchor": {"layout_box_role": "table", "description": "two-column decision matrix with dimension rail"}, + "svg_effects": ["typography", "path"], + "asset_id": "chart.comparison_table", + "density_contract": "comparison table >= 4 cells", + }, + "capability": { + "seed_id": "capability_icon_map", + "page_type": "capability_map", + "composition_archetype": "radial_system", + "required_visual_evidence": ["hub_spoke", "sector_field", "semantic_labels"], + "primary_motif": "radial_hub", + "chart_type": "hub_spoke", + "page_rhythm": "dense", + "renderer_id": "capability_icon_map", + "main_visual_anchor": {"layout_box_role": "visual", "description": "central capability node with surrounding module grid"}, + "svg_effects": ["typography", "connector_flow"], + "asset_id": "chart.hub_spoke", + "density_contract": "capability map >= 4 nodes", + }, + "chart": { + "seed_id": "single_chart_takeaway", + "page_type": "chart_takeaway", + "composition_archetype": "data_stage", + "required_visual_evidence": ["chart_geometry", "insight_strip", "full_page_archetype"], + "primary_motif": "takeaway_chart", + "chart_type": "bar_chart", + "page_rhythm": "dense", + "renderer_id": "single_chart_takeaway", + "main_visual_anchor": {"layout_box_role": "chart", "description": "single chart area with one takeaway annotation"}, + "svg_effects": ["typography", "chart_geometry"], + "asset_id": "chart.bar_chart", + "density_contract": "chart >= 3 visible marks", + }, + "closing": { + "seed_id": "closing_summary", + "page_type": "closing", + "composition_archetype": "closing_manifesto", + "required_visual_evidence": ["closing_ribbon", "action_cards", "full_page_archetype"], + "primary_motif": "closing_route", + "chart_type": "", + "page_rhythm": "anchor", + "renderer_id": "closing_summary", + "main_visual_anchor": {"layout_box_role": "callout", "description": "closing statement plus next-action callout"}, + "svg_effects": ["typography"], + "asset_id": "chart.numbered_steps", + "density_contract": "one closing message plus one next action", + }, + "annotation": { + "seed_id": "spotlight_diagnosis_callout", + "page_type": "insight_callout", + "composition_archetype": "annotated_spotlight", + "required_visual_evidence": ["spotlight", "annotation", "semantic_labels"], + "primary_motif": "spotlight_field", + "chart_type": "", + "page_rhythm": "breathing", + "renderer_id": "spotlight_diagnosis_callout", + "main_visual_anchor": {"layout_box_role": "spotlight", "description": "annotated visual field with one spotlight and side note"}, + "svg_effects": ["typography", "spotlight"], + "asset_id": "chart.labeled_card", + "density_contract": "spotlight callout <= 2 targets", + }, +} + +KEYWORD_PROFILES: tuple[tuple[str, tuple[str, ...]], ...] = ( + ("agenda", ("agenda", "contents", "table of contents", "toc", "目录", "议程")), + ("section", ("section", "chapter", "section divider", "divider", "transition", "章节", "过渡", "转场", "第1章", "第2章", "第3章", "01 ", "02 ", "03 ")), + ("dashboard", ("dashboard", "kpi", "metric", "metrics", "status", "scorecard", "health", "看板", "指标", "状态")), + ("roadmap", ("roadmap", "timeline", "milestone", "phase", "plan", "规划", "里程碑", "阶段")), + ("process", ("process", "pipeline", "workflow", "flow", "funnel", "步骤", "流程", "链路")), + ("comparison", ("compare", "comparison", "versus", "vs", "matrix", "table", "decision", "对比", "比较", "矩阵")), + ("capability", ("capability", "module", "architecture", "system", "hub", "spoke", "能力", "模块", "架构")), + ("chart", ("chart", "bar", "line", "trend", "data", "evidence", "数据", "图表", "趋势")), + ("closing", ("closing", "summary", "next", "thanks", "q&a", "结尾", "总结", "下一步")), + ("cover", ("cover", "opening", "title", "thesis", "封面", "开场", "标题")), +) + +STYLE_HINTS: tuple[tuple[str, tuple[str, ...]], ...] = ( + ("raw_grid", ("dashboard", "kpi", "metric", "technical", "dense", "status", "ops", "operation", "看板", "指标")), + ("long_table", ("process", "pipeline", "responsibility", "plan", "workflow", "流程", "责任")), + ("riptide_cobalt", ("technology", "architecture", "flow", "system", "tech", "技术", "架构")), + ("data_journalism_editorial", ("financial", "market", "report", "data", "analysis", "finance", "数据", "分析")), + ("monochrome", ("serious", "formal", "minimal", "decision", "compare", "正式", "决策")), +) + +NARRATIVE_MODES = {"briefing", "instructional", "narrative", "pyramid", "showcase"} + +MODE_HINTS: tuple[tuple[str, tuple[str, ...]], ...] = ( + ("instructional", ("training", "tutorial", "course", "lesson", "how-to", "playbook", "教学", "培训", "教程")), + ("pyramid", ("decision", "board", "strategy", "proposal", "investment", "consulting", "决策", "战略", "提案", "投资")), + ("showcase", ("launch", "showcase", "portfolio", "brand", "event", "product reveal", "发布", "展示", "作品集", "品牌")), + ("narrative", ("story", "journey", "case", "vision", "future", "体验", "旅程", "故事", "案例", "愿景")), + ("briefing", ("briefing", "review", "report", "status", "operations", "weekly", "汇报", "报告", "复盘", "经营", "周报")), +) + + +def script_path() -> Path: + return Path(__file__).resolve() + + +def references_dir() -> Path: + return script_path().parents[1] / "references" + + +def read_json(path: Path) -> Any: + return json.loads(path.read_text(encoding="utf-8")) + + +def write_json(path: Path, value: Any) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(json.dumps(value, ensure_ascii=False, indent=2, sort_keys=True) + "\n", encoding="utf-8") + + +def clone_json(value: Any) -> Any: + return copy.deepcopy(value) + + +def tokenize(*values: object) -> set[str]: + tokens: set[str] = set() + for value in values: + if value is None: + continue + if isinstance(value, dict): + tokens.update(tokenize(*value.values())) + continue + if isinstance(value, (list, tuple, set)): + tokens.update(tokenize(*value)) + continue + tokens.update(match.group(0).lower() for match in TOKEN_RE.finditer(str(value))) + return tokens + + +def compact_text(value: Any) -> str: + if value is None: + return "" + if isinstance(value, str): + return value.strip() + if isinstance(value, dict): + return " ".join(compact_text(item) for item in value.values() if compact_text(item)) + if isinstance(value, (list, tuple, set)): + return " ".join(compact_text(item) for item in value if compact_text(item)) + return str(value).strip() + + +def load_catalogs(ref_dir: Path | None = None) -> dict[str, Any]: + root = ref_dir or references_dir() + style_data = read_json(root / "style-presets.json") + seed_data = read_json(root / "svg-seeds.json") + recipe_data = read_json(root / "svg-recipes.json") + pattern_data = read_json(root / "svglide-design-pattern-map.json") + return { + "style_presets": {item["style_id"]: item for item in style_data.get("presets", []) if isinstance(item, dict) and item.get("style_id")}, + "seeds": seed_data.get("seeds", {}), + "recipes": recipe_data.get("recipes", {}), + "chart_type_contracts": recipe_data.get("chart_type_contracts", {}), + "pattern_ids": {item.get("id") for item in pattern_data.get("resources", []) if isinstance(item, dict) and item.get("id")}, + } + + +def first_present(data: dict[str, Any], keys: tuple[str, ...]) -> str: + for key in keys: + value = compact_text(data.get(key)) + if value: + return value + visual_plan = data.get("visual_plan") + if isinstance(visual_plan, dict): + for key in keys: + value = compact_text(visual_plan.get(key)) + if value: + return value + return "" + + +def classify_profile(text: str, *, index: int, total: int) -> str: + lowered = text.lower() + if index == 0 and any(keyword in lowered for keyword in ("cover", "opening", "title", "thesis", "封面", "开场")): + return "cover" + if total > 1 and index == total - 1 and any(keyword in lowered for keyword in ("closing", "summary", "next", "thanks", "q&a", "结尾", "总结", "下一步")): + return "closing" + for profile, keywords in KEYWORD_PROFILES: + if any(keyword in lowered for keyword in keywords): + return profile + if index == 0: + return "cover" + if total > 1 and index == total - 1: + return "closing" + return "annotation" + + +def style_preset_from_brief(brief: str, catalogs: dict[str, Any]) -> str: + lowered = brief.lower() + for style_id, keywords in STYLE_HINTS: + if style_id in catalogs["style_presets"] and any(keyword in lowered for keyword in keywords): + return style_id + return "raw_grid" if "raw_grid" in catalogs["style_presets"] else sorted(catalogs["style_presets"])[0] + + +def narrative_mode_from_brief(brief: str, slide_plan: dict[str, Any] | None = None) -> str: + lowered = " ".join([brief, compact_text(slide_plan or {})]).lower() + tokens = tokenize(lowered) + for mode, keywords in MODE_HINTS: + if any((keyword in tokens if re.fullmatch(r"[a-z0-9-]+", keyword) else keyword in lowered) for keyword in keywords): + return mode + return "briefing" + + +def style_system_from_preset(style_id: str, catalogs: dict[str, Any]) -> dict[str, Any]: + preset = catalogs["style_presets"].get(style_id, {}) + palette = preset.get("palette") if isinstance(preset.get("palette"), dict) else {} + shape_language = preset.get("shape_language") if isinstance(preset.get("shape_language"), dict) else {} + density = preset.get("density") if isinstance(preset.get("density"), dict) else {} + return { + "palette": { + "background": palette.get("background", "#F5F5F5"), + "text": palette.get("text", "#0A0A0A"), + "accent": palette.get("accent", "#2563EB"), + "support": clone_json(palette.get("support", [])), + }, + "typography": "strong title, readable native text labels", + "background_strategy": shape_language.get("panel_treatment", "structured panels with explicit text surfaces"), + "motif": shape_language.get("texture", "local SVG motif derived from selected page recipe"), + "density": { + "text_density": density.get("text_density", "medium"), + "label_density": density.get("label_density", "medium"), + "connector_density": density.get("connector_density", "medium"), + }, + } + + +def apply_brief_palette(style_system: dict[str, Any], brief: str) -> dict[str, Any]: + colors: list[str] = [] + for match in HEX_COLOR_RE.finditer(brief): + color = match.group(0).upper() + if color not in colors: + colors.append(color) + if not colors: + return style_system + output = clone_json(style_system) + palette = output.setdefault("palette", {}) + if isinstance(palette, dict): + palette["accent"] = colors[0] + if len(colors) > 1: + palette["support"] = colors[1:] + output["palette_source"] = "brief_hex_colors" + return output + + +def slide_text(slide: dict[str, Any], fallback_description: str) -> str: + parts = [ + first_present(slide, ("title", "headline", "name")), + first_present(slide, ("description", "summary", "body", "key_message", "page_type", "chart_type", "visual_recipe")), + fallback_description, + ] + return " ".join(part for part in parts if part) + + +def seed_for_profile(profile: str, catalogs: dict[str, Any]) -> tuple[str, dict[str, Any]]: + seed_id = PAGE_PROFILES[profile]["seed_id"] + seeds = catalogs["seeds"] + if seed_id not in seeds: + raise ValueError(f"missing SVG seed: {seed_id}") + return seed_id, seeds[seed_id] + + +def seed_for_slide(slide: dict[str, Any], profile: str, catalogs: dict[str, Any]) -> tuple[str, dict[str, Any], dict[str, Any]]: + explicit_seed = compact_text(slide.get("seed_id")) + seeds = catalogs["seeds"] + if explicit_seed and explicit_seed in seeds: + seed = seeds[explicit_seed] + profile_data = dict(PAGE_PROFILES[profile]) + profile_data["seed_id"] = explicit_seed + return explicit_seed, seed, profile_data + + chart_type = compact_text(slide.get("chart_type")).replace("-", "_").lower() + visual_recipe = compact_text(slide.get("visual_recipe")).replace("-", "_").lower() + for candidate, data in PAGE_PROFILES.items(): + if chart_type and chart_type == data["chart_type"]: + seed_id, seed = seed_for_profile(candidate, catalogs) + return seed_id, seed, data + if visual_recipe and seeds.get(data["seed_id"], {}).get("visual_recipe") == visual_recipe: + seed_id, seed = seed_for_profile(candidate, catalogs) + return seed_id, seed, data + + seed_id, seed = seed_for_profile(profile, catalogs) + return seed_id, seed, PAGE_PROFILES[profile] + + +def list_union(*values: Any) -> list[str]: + out: list[str] = [] + for value in values: + if isinstance(value, str): + candidates = [value] + elif isinstance(value, (list, tuple, set)): + candidates = [str(item) for item in value if str(item).strip()] + else: + candidates = [] + for item in candidates: + if item not in out: + out.append(item) + return out + + +def setdefault_clone(target: dict[str, Any], key: str, value: Any) -> None: + if key not in target or target[key] in (None, "", []): + target[key] = clone_json(value) + + +def normalize_empty_chart_type(value: Any) -> str: + normalized = compact_text(value).replace("-", "_").lower() + return "" if normalized in {"none", "na", "n_a", "not_applicable", "no_chart", "no"} else normalized + + +def known_profile_evidence() -> set[str]: + evidence: set[str] = set() + for profile in PAGE_PROFILES.values(): + raw = profile.get("required_visual_evidence") + if isinstance(raw, list): + evidence.update(str(item) for item in raw if str(item).strip()) + return evidence + + +def complete_visual_design_contract(completed: dict[str, Any], profile_data: dict[str, Any]) -> dict[str, Any]: + existing = completed.get("visual_design_contract") + contract = clone_json(existing) if isinstance(existing, dict) else {} + thesis = first_present(completed, ("visual_thesis", "key_message", "one_idea", "title", "headline", "description")) + existing_required = list_union(contract.get("required_visual_evidence")) + manual_required = [item for item in existing_required if item not in known_profile_evidence()] + required = list_union(manual_required, profile_data.get("required_visual_evidence")) + pattern_bundle = list_union(contract.get("pattern_bundle"), profile_data.get("asset_id")) + setdefault_clone(contract, "schema_version", "svglide-visual-design-contract/v1") + setdefault_clone(contract, "page_kind", profile_data["page_type"]) + setdefault_clone(contract, "visual_thesis", thesis) + setdefault_clone(contract, "composition_archetype", profile_data["composition_archetype"]) + contract["pattern_bundle"] = pattern_bundle + setdefault_clone(contract, "density", profile_data["page_rhythm"]) + setdefault_clone(contract, "primary_motif", profile_data["primary_motif"]) + contract["required_visual_evidence"] = required + setdefault_clone(contract, "renderer_id", profile_data["renderer_id"]) + setdefault_clone(contract, "layout_seed_id", completed.get("seed_id")) + setdefault_clone(contract, "visual_recipe", completed.get("visual_recipe")) + return contract + + +def complete_slide(slide: dict[str, Any], *, brief: str, fallback_description: str, index: int, total: int, catalogs: dict[str, Any]) -> dict[str, Any]: + completed = clone_json(slide) + if "chart_type" in completed: + completed["chart_type"] = normalize_empty_chart_type(completed.get("chart_type")) + text = slide_text(completed, fallback_description) or brief + profile = classify_profile(text, index=index, total=total) + seed_id, seed, profile_data = seed_for_slide(completed, profile, catalogs) + recipe = compact_text(completed.get("visual_recipe")) or compact_text(seed.get("visual_recipe")) + recipe_contract = catalogs["recipes"].get(recipe, {}) + required_primitives = list_union(recipe_contract.get("required_primitives"), seed.get("required_primitives"), completed.get("required_primitives")) + svg_primitives = list_union(completed.get("svg_primitives"), required_primitives, ["typography", "geometric_shape"]) + + setdefault_clone(completed, "page", index + 1) + setdefault_clone(completed, "key_message", first_present(completed, ("key_message", "one_idea", "title", "headline", "description")) or text) + setdefault_clone(completed, "renderer_id", profile_data["renderer_id"]) + setdefault_clone(completed, "page_rhythm", profile_data["page_rhythm"]) + setdefault_clone(completed, "page_type", profile_data["page_type"]) + setdefault_clone(completed, "chart_type", profile_data["chart_type"]) + setdefault_clone(completed, "main_visual_anchor", profile_data["main_visual_anchor"]) + setdefault_clone(completed, "seed_id", seed_id) + completed["layout_skeleton_id"] = clone_json(seed.get("layout_skeleton", {}).get("id", f"{seed_id}_skeleton")) + completed["layout_family"] = clone_json(seed.get("layout_family", profile)) + setdefault_clone(completed, "visual_recipe", recipe) + setdefault_clone(completed, "visual_signature", f"{profile_data['page_type']} / {profile_data['renderer_id']} / {profile_data['asset_id']}") + completed["svg_effects"] = clone_json(profile_data["svg_effects"]) + completed["layout_boxes"] = clone_json(seed.get("layout_boxes", [])) + completed["content_budget"] = clone_json(seed.get("content_budget", {})) + completed["text_capacity"] = clone_json(seed.get("default_text_capacity") or seed.get("content_budget", {})) + completed["text_budget_by_role"] = clone_json(seed.get("text_budget_by_role", {})) + completed["reserved_bands"] = clone_json(seed.get("reserved_bands", {})) + completed["footer_safe_zone"] = clone_json(seed.get("footer_safe_zone", {})) + completed["vertical_text_policy"] = clone_json(seed.get("vertical_text_policy", {"mode": "deny", "allowed_roles": [], "max_chars": 0, "max_lines": 0})) + setdefault_clone( + completed, + "reference_asset", + { + "source": "svglide_design_pattern", + "asset_id": profile_data["asset_id"], + "usage": "page-type geometry only; do not copy raw SVG paths", + }, + ) + setdefault_clone(completed, "visual_intent", f"use {recipe.replace('_', ' ')} structure to make the page readable as SVG-native content") + setdefault_clone(completed, "visual_focal_point", profile_data["main_visual_anchor"]) + setdefault_clone(completed, "required_primitives", required_primitives) + setdefault_clone(completed, "svg_primitives", svg_primitives) + setdefault_clone(completed, "xml_like_risk", "without the declared SVG primitives this page would degrade into ordinary text boxes") + setdefault_clone(completed, "content_density_contract", profile_data["density_contract"]) + setdefault_clone(completed, "asset_contract", "none_required") + setdefault_clone(completed, "risk_flags", []) + setdefault_clone(completed, "source_status", "user_prompt_only") + setdefault_clone(completed, "source_policy", "when source material is missing, mark missing evidence and avoid numeric claims") + setdefault_clone(completed, "layout_guardrails", DEFAULT_GUARDRAILS) + completed["visual_design_contract"] = complete_visual_design_contract(completed, profile_data) + return completed + + +def normalize_slide_inputs(slide_plan: dict[str, Any] | None, page_descriptions: list[str]) -> list[dict[str, Any]]: + plan = slide_plan or {} + raw_slides = plan.get("slides") + if isinstance(raw_slides, list) and raw_slides: + slides = [clone_json(item) for item in raw_slides if isinstance(item, dict)] + else: + pages = plan.get("pages") + slides = [clone_json(item) for item in pages if isinstance(item, dict)] if isinstance(pages, list) and pages else [] + if not slides: + slides = [{"description": description} for description in page_descriptions] + if not slides: + slides = [{"description": "Cover: core message"}, {"description": "Closing: next steps"}] + for index, description in enumerate(page_descriptions): + if index >= len(slides): + slides.append({"description": description}) + elif description and not compact_text(slides[index].get("description")): + slides[index]["description"] = description + return slides + + +def selected_asset_id(slide: dict[str, Any]) -> str: + reference = slide.get("reference_asset") + if isinstance(reference, dict): + asset_id = compact_text(reference.get("asset_id") or reference.get("id")) + if asset_id: + return asset_id + asset_id = compact_text(slide.get("asset_id") or slide.get("design_pattern_id")) + return asset_id + + +def deck_rhythm_from_slides(slides: list[dict[str, Any]]) -> list[dict[str, Any]]: + rhythm: list[dict[str, Any]] = [] + for index, slide in enumerate(slides, 1): + item = { + "page": slide.get("page", index), + "rhythm": compact_text(slide.get("page_rhythm")) or "breathing", + "page_type": compact_text(slide.get("page_type")) or "content", + } + rhythm.append(item) + return rhythm + + +def build_design_pattern_selection(slides: list[dict[str, Any]], existing: Any, catalogs: dict[str, Any]) -> dict[str, Any]: + selection = clone_json(existing) if isinstance(existing, dict) else {} + raw_assets = selection.get("selected_assets") + selected_assets: list[dict[str, Any]] = [clone_json(item) for item in raw_assets if isinstance(item, dict)] if isinstance(raw_assets, list) else [] + seen = {compact_text(asset.get("id") or asset.get("asset_id")) for asset in selected_assets} + pattern_ids = catalogs.get("pattern_ids", set()) + for slide in slides: + asset_id = selected_asset_id(slide) + if not asset_id or asset_id in seen or (pattern_ids and asset_id not in pattern_ids): + continue + selected_assets.append( + { + "id": asset_id, + "kind": "chart_template", + "usage": "geometry_contract", + "copy_policy": "derive_contract_only", + } + ) + seen.add(asset_id) + setdefault_clone(selection, "schema_version", "svglide-design-pattern-selection/v1") + setdefault_clone(selection, "mode", "local_contract") + setdefault_clone(selection, "selected_assets", selected_assets) + setdefault_clone(selection, "proof_status", "pending_component_report") + return selection + + +def build_contract( + *, + brief: str = "", + slide_plan: dict[str, Any] | None = None, + page_descriptions: list[str] | None = None, + ref_dir: Path | None = None, +) -> dict[str, Any]: + catalogs = load_catalogs(ref_dir) + descriptions = page_descriptions or [] + source_plan = clone_json(slide_plan) if isinstance(slide_plan, dict) else {} + output = source_plan + slides = normalize_slide_inputs(source_plan, descriptions) + completed_slides = [ + complete_slide(slide, brief=brief, fallback_description=descriptions[index] if index < len(descriptions) else "", index=index, total=len(slides), catalogs=catalogs) + for index, slide in enumerate(slides) + ] + + style_id = compact_text(output.get("style_preset")) or style_preset_from_brief(brief or compact_text(output), catalogs) + if style_id not in catalogs["style_presets"]: + style_id = style_preset_from_brief(brief or compact_text(output), catalogs) + explicit_mode = compact_text(output.get("narrative_mode") or output.get("mode")) + narrative_mode = explicit_mode if explicit_mode in NARRATIVE_MODES else narrative_mode_from_brief(brief, output) + + setdefault_clone(output, "schema_version", CONTRACT_SCHEMA_VERSION) + setdefault_clone(output, "output_mode", "svglide-svg") + output["mode"] = narrative_mode + setdefault_clone(output, "narrative_mode", narrative_mode) + setdefault_clone(output, "canvas", CANVAS) + setdefault_clone(output, "safe_area", SAFE_AREA) + output["style_preset"] = style_id + setdefault_clone(output, "style_selection_reason", f"{style_id} matches the brief and the selected SVG page recipes") + output["style_system"] = apply_brief_palette(style_system_from_preset(style_id, catalogs), brief) + for slide in completed_slides: + contract = slide.get("visual_design_contract") + if isinstance(contract, dict): + setdefault_clone(contract, "style_preset", style_id) + output["slides"] = completed_slides + output["page_count"] = len(completed_slides) + setdefault_clone(output, "page_rhythm", deck_rhythm_from_slides(completed_slides)) + output["design_pattern_selection"] = build_design_pattern_selection(completed_slides, output.get("design_pattern_selection"), catalogs) + return output + + +def read_text_arg(path: str | None, inline: str | None) -> str: + parts: list[str] = [] + if inline: + parts.append(inline) + if path: + parts.append(Path(path).expanduser().read_text(encoding="utf-8")) + return "\n".join(part.strip() for part in parts if part.strip()) + + +def build_parser() -> argparse.ArgumentParser: + parser = argparse.ArgumentParser(description="Build or complete a conservative SVGlide plan contract.") + parser.add_argument("--brief", help="path to a brief text file") + parser.add_argument("--brief-text", help="inline brief text") + parser.add_argument("--plan", help="existing slide_plan.json to complete") + parser.add_argument("--page-description", action="append", default=[], help="simple page description; may be repeated") + parser.add_argument("--output", help="output JSON path; defaults to stdout") + parser.add_argument("--in-place", action="store_true", help="write the completed contract back to --plan") + return parser + + +def main(argv: list[str] | None = None) -> int: + parser = build_parser() + args = parser.parse_args(argv) + if args.in_place and not args.plan: + parser.error("--in-place requires --plan") + + brief = read_text_arg(args.brief, args.brief_text) + slide_plan = read_json(Path(args.plan).expanduser()) if args.plan else None + if slide_plan is not None and not isinstance(slide_plan, dict): + raise ValueError("--plan must point to a JSON object") + + result = build_contract(brief=brief, slide_plan=slide_plan, page_descriptions=args.page_description) + output_path = Path(args.output).expanduser() if args.output else (Path(args.plan).expanduser() if args.in_place else None) + if output_path: + write_json(output_path, result) + else: + print(json.dumps(result, ensure_ascii=False, indent=2, sort_keys=True)) + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/skills/lark-slides/scripts/svglide_strategist_test.py b/skills/lark-slides/scripts/svglide_strategist_test.py new file mode 100644 index 000000000..6896ae3a1 --- /dev/null +++ b/skills/lark-slides/scripts/svglide_strategist_test.py @@ -0,0 +1,231 @@ +# Copyright (c) 2026 Lark Technologies Pte. Ltd. +# SPDX-License-Identifier: MIT +from __future__ import annotations + +import json +import sys +import tempfile +import unittest +from pathlib import Path + +SCRIPT_DIR = Path(__file__).resolve().parent +sys.path.insert(0, str(SCRIPT_DIR)) + +import svglide_strategist as strategist + + +class SVGlideStrategistTest(unittest.TestCase): + def test_build_contract_from_brief_and_page_descriptions(self) -> None: + plan = strategist.build_contract( + brief="Create an operations review with KPI dashboard, roadmap, and closing next steps.", + page_descriptions=[ + "Cover: operating thesis and review scope", + "KPI dashboard: four health metrics with micro trends", + "Roadmap: three milestone phases and ownership", + "Closing: next steps and decision request", + ], + ) + + self.assertEqual("svglide-svg", plan["output_mode"]) + self.assertEqual("briefing", plan["mode"]) + self.assertEqual("briefing", plan["narrative_mode"]) + self.assertEqual("raw_grid", plan["style_preset"]) + self.assertEqual({"background", "text", "accent"}, set(plan["style_system"]["palette"]).intersection({"background", "text", "accent"})) + self.assertEqual(4, plan["page_count"]) + self.assertEqual( + [ + {"page": 1, "rhythm": "anchor", "page_type": "cover"}, + {"page": 2, "rhythm": "dense", "page_type": "kpi_overview"}, + {"page": 3, "rhythm": "dense", "page_type": "roadmap"}, + {"page": 4, "rhythm": "anchor", "page_type": "closing"}, + ], + plan["page_rhythm"], + ) + self.assertEqual(["chart.vertical_list", "chart.kpi_cards", "chart.timeline", "chart.numbered_steps"], [asset["id"] for asset in plan["design_pattern_selection"]["selected_assets"]]) + + dashboard = plan["slides"][1] + self.assertEqual("KPI dashboard: four health metrics with micro trends", dashboard["key_message"]) + self.assertEqual("dense", dashboard["page_rhythm"]) + self.assertEqual("kpi_overview", dashboard["page_type"]) + self.assertEqual("", dashboard["chart_type"]) + self.assertEqual({"layout_box_role": "chart", "description": "KPI dashboard grid with hero metrics and micro trends"}, dashboard["main_visual_anchor"]) + self.assertEqual("kpi_overview / dashboard_kpi_grid / chart.kpi_cards", dashboard["visual_signature"]) + self.assertEqual(["typography", "chart_geometry"], dashboard["svg_effects"]) + self.assertEqual("fake_ui_dashboard", dashboard["visual_recipe"]) + self.assertEqual("chart.kpi_cards", dashboard["reference_asset"]["asset_id"]) + self.assertIn("main text and chart labels stay inside safe area", dashboard["layout_guardrails"]) + self.assertEqual( + { + "schema_version": "svglide-visual-design-contract/v1", + "page_kind": "kpi_overview", + "visual_thesis": "KPI dashboard: four health metrics with micro trends", + "composition_archetype": "data_stage", + "pattern_bundle": ["chart.kpi_cards"], + "density": "dense", + "primary_motif": "metric_grid", + "required_visual_evidence": ["metric_hierarchy", "chart_geometry", "dashboard_grid"], + "renderer_id": "dashboard_kpi_grid", + "layout_seed_id": "dashboard_kpi_grid", + "visual_recipe": "fake_ui_dashboard", + "style_preset": "raw_grid", + }, + dashboard["visual_design_contract"], + ) + + chart_contracts = strategist.load_catalogs()["chart_type_contracts"] + allowed_rhythms = {"anchor", "breathing", "dense"} + for slide in plan["slides"]: + self.assertIn(slide["page_rhythm"], allowed_rhythms) + if slide["chart_type"]: + self.assertIn(slide["chart_type"], chart_contracts) + + def test_brief_hex_colors_drive_style_system_palette(self) -> None: + plan = strategist.build_contract( + brief="新疆阿克苏城区居住区策划案,主色澄澈水蓝 #4A90E2,辅色春芽嫩绿 #8BC34A,强调色艾德莱斯绸朱红 #E91E63。", + page_descriptions=["Cover: 以水为脉·四时为序"], + ) + + palette = plan["style_system"]["palette"] + self.assertEqual("#4A90E2", palette["accent"]) + self.assertEqual(["#8BC34A", "#E91E63"], palette["support"]) + self.assertEqual("brief_hex_colors", plan["style_system"]["palette_source"]) + + def test_complete_existing_plan_preserves_manual_fields(self) -> None: + base = { + "title": "Pipeline Decision", + "style_preset": "monochrome", + "slides": [ + { + "page": 1, + "title": "Keep this title", + "page_type": "custom_decision", + "chart_type": "comparison_table", + "visual_recipe": "geometric_composition", + "reference_asset": {"source": "manual", "asset_id": "chart.comparison_table", "usage": "approved"}, + } + ], + } + + plan = strategist.build_contract(brief="Compare two launch options.", slide_plan=base) + slide = plan["slides"][0] + + self.assertEqual("monochrome", plan["style_preset"]) + self.assertEqual("pyramid", plan["mode"]) + self.assertEqual("Keep this title", slide["title"]) + self.assertEqual("custom_decision", slide["page_type"]) + self.assertEqual("comparison_table", slide["chart_type"]) + self.assertEqual("geometric_composition", slide["visual_recipe"]) + self.assertEqual({"source": "manual", "asset_id": "chart.comparison_table", "usage": "approved"}, slide["reference_asset"]) + self.assertIn("layout_boxes", slide) + self.assertIn("footer_safe_zone", slide) + self.assertIn("layout_guardrails", slide) + + def test_existing_plan_refreshes_seed_derived_contract_fields(self) -> None: + base = { + "slides": [ + { + "description": "KPI dashboard with secondary metric cards and chart labels", + "seed_id": "dashboard_kpi_grid", + "layout_boxes": [ + {"id": "title", "role": "title", "x": 48, "y": 34, "width": 864, "height": 48}, + {"id": "primary-kpi", "role": "metric", "x": 64, "y": 106, "width": 260, "height": 128}, + {"id": "secondary-grid", "role": "grid", "x": 348, "y": 106, "width": 548, "height": 128}, + ], + "text_budget_by_role": { + "metric": {"max_chars": 80, "max_lines": 3, "max_boxes": 1, "min_font_px": 12}, + }, + } + ] + } + + plan = strategist.build_contract(brief="KPI dashboard", slide_plan=base) + slide = plan["slides"][0] + + self.assertIn("secondary-metric-grid", [box["id"] for box in slide["layout_boxes"]]) + self.assertEqual(7, slide["text_budget_by_role"]["metric"]["max_boxes"]) + self.assertEqual(4, slide["text_budget_by_role"]["chart"]["max_boxes"]) + + def test_normalizes_none_chart_type_to_empty_contract(self) -> None: + plan = strategist.build_contract( + brief="Brand showcase opening.", + slide_plan={"slides": [{"title": "Opening", "chart_type": "none"}]}, + ) + + self.assertEqual("", plan["slides"][0]["chart_type"]) + + def test_classifies_agenda_and_section_as_first_class_profiles(self) -> None: + plan = strategist.build_contract( + brief="新疆阿克苏城区居住区策划案,包含目录和章节过渡。", + page_descriptions=[ + "Cover: 以水为脉·四时为序", + "目录:项目核心定位、春之地块、夏之地块、秋之地块、冬之地块", + "章节过渡页:01 项目核心定位与愿景", + ], + ) + + agenda = plan["slides"][1] + section = plan["slides"][2] + + self.assertEqual("agenda", agenda["page_type"]) + self.assertEqual("agenda_numbered_path", agenda["seed_id"]) + self.assertEqual("indexed_path", agenda["visual_design_contract"]["composition_archetype"]) + self.assertEqual(["numbered_path", "section_index", "semantic_labels"], agenda["visual_design_contract"]["required_visual_evidence"]) + self.assertEqual("chart.agenda_list", agenda["reference_asset"]["asset_id"]) + + self.assertEqual("section_divider", section["page_type"]) + self.assertEqual("section_divider_index", section["seed_id"]) + self.assertEqual("section_signal", section["visual_design_contract"]["composition_archetype"]) + self.assertEqual(["section_index", "hero_signal", "full_page_archetype"], section["visual_design_contract"]["required_visual_evidence"]) + self.assertEqual("chart.numbered_steps", section["reference_asset"]["asset_id"]) + + def test_cli_reads_text_and_plan_and_writes_clean_contract(self) -> None: + with tempfile.TemporaryDirectory() as tmpdir: + tmp = Path(tmpdir) + brief_path = tmp / "brief.txt" + plan_path = tmp / "slide_plan.json" + out_path = tmp / "contract.json" + brief_path.write_text("Technical KPI dashboard for weekly status.", encoding="utf-8") + plan_path.write_text(json.dumps({"slides": [{"title": "Status", "description": "KPI dashboard with micro bars"}]}), encoding="utf-8") + + exit_code = strategist.main(["--brief", str(brief_path), "--plan", str(plan_path), "--output", str(out_path)]) + + self.assertEqual(0, exit_code) + result = json.loads(out_path.read_text(encoding="utf-8")) + encoded = json.dumps(result, ensure_ascii=False).lower() + self.assertEqual("fake_ui_dashboard", result["slides"][0]["visual_recipe"]) + self.assertNotIn("source" + "_token", encoded) + self.assertNotIn("beautiful" + "-feishu-whiteboard", encoded) + self.assertNotIn("ppt" + "-master", encoded) + self.assertNotIn("hugo" + "he3", encoded) + + def test_preserves_manual_visual_design_contract_and_fills_missing_fields(self) -> None: + plan = strategist.build_contract( + brief="AI capital market briefing with one chart.", + slide_plan={ + "slides": [ + { + "title": "Capital Flow", + "chart_type": "bar_chart", + "visual_design_contract": { + "visual_thesis": "Manual thesis", + "composition_archetype": "manual_data_stage", + "required_visual_evidence": ["custom_evidence"], + }, + } + ] + }, + ) + + contract = plan["slides"][0]["visual_design_contract"] + + self.assertEqual("Manual thesis", contract["visual_thesis"]) + self.assertEqual("manual_data_stage", contract["composition_archetype"]) + self.assertEqual(["custom_evidence", "chart_geometry", "insight_strip", "full_page_archetype"], contract["required_visual_evidence"]) + self.assertEqual(["chart.bar_chart"], contract["pattern_bundle"]) + self.assertEqual("dense", contract["density"]) + self.assertEqual("takeaway_chart", contract["primary_motif"]) + self.assertEqual(plan["style_preset"], contract["style_preset"]) + + +if __name__ == "__main__": + unittest.main() diff --git a/skills_embed.go b/skills_embed.go index ac5102a58..a2e2cd2e5 100644 --- a/skills_embed.go +++ b/skills_embed.go @@ -24,9 +24,12 @@ import ( //go:embed skills/lark-slides/scripts/svg_safe_rewrite.py //go:embed skills/lark-slides/scripts/svg_raster_renderer.py //go:embed skills/lark-slides/scripts/svglide_project_runner.py +//go:embed skills/lark-slides/scripts/svg_preflight.py //go:embed skills/lark-slides/scripts/svg_preview_lint.py -//go:embed skills/lark-slides/scripts/ppt_master_asset_inventory.py //go:embed skills/lark-slides/scripts/svglide_asset_selector.py +//go:embed skills/lark-slides/scripts/svglide_strategist.py +//go:embed skills/lark-slides/scripts/svglide_gen_runtime.py +//go:embed skills/lark-slides/scripts/svglide_golden_suite.py var skillsEmbedFS embed.FS // init wires the embedded tree in as the default skill content. It compiles into