dsh-ppt-studio 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (179) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +480 -0
  3. package/agent-presets/ppt/agent.cordis.yml +308 -0
  4. package/agent-presets/ppt/preset.yml +3 -0
  5. package/cordis.patch.yml +29 -0
  6. package/docs/00-/350/256/276/350/256/241/346/272/220.md +537 -0
  7. package/docs/01-/351/234/200/346/261/202/344/270/216/347/233/256/346/240/207.md +136 -0
  8. package/docs/02-/346/212/200/346/234/257/346/212/245/345/221/212.md +483 -0
  9. package/docs/03-/346/233/264/346/226/260/346/227/245/345/277/227.md +2898 -0
  10. package/docs/04-/350/267/257/347/272/277/345/233/276/344/270/216/351/207/214/347/250/213/347/242/221.md +113 -0
  11. package/docs/05-/350/277/255/344/273/243/346/265/201/347/250/213.md +210 -0
  12. package/docs/06-/350/257/204/345/256/241/344/270/216/346/265/213/350/257/225.md +296 -0
  13. package/docs/fidelity-report.md +225 -0
  14. package/examples/fx/fx-mini/deck.yaml +39 -0
  15. package/examples/fx/fx-mini/pages/01.yaml +6 -0
  16. package/examples/fx/fx-mini/pages/02.yaml +2 -0
  17. package/examples/fx/fx-mini/pages/03.yaml +6 -0
  18. package/examples/fx/fx-pro/deck.yaml +48 -0
  19. package/examples/fx/fx-pro/pages/01_cover.yaml +40 -0
  20. package/examples/fx/fx-pro/pages/02_arch.yaml +55 -0
  21. package/examples/fx/fx-pro/pages/03_table.yaml +20 -0
  22. package/examples/fx/fx-pro/pages/04_chart.yaml +40 -0
  23. package/examples/fx/fx-pro/pages/05_timeline.yaml +71 -0
  24. package/examples/fx/fx-pro/pages/06_longtext.yaml +13 -0
  25. package/examples/fx/fx-pro/pages/07_safearea.yaml +33 -0
  26. package/examples/fx/fx-pro/pages/08_contrast.yaml +22 -0
  27. package/examples/fx/fx-pro/pages/09_custgeom.yaml +32 -0
  28. package/examples/fx/fx-pro/pages/10_numbers.yaml +25 -0
  29. package/examples/fx/fx-pro/pages/11_section.yaml +20 -0
  30. package/examples/fx/fx-pro/pages/12_summary.yaml +57 -0
  31. package/examples/fx/fx-pro/preview/01_pages_01_cover.html +1 -0
  32. package/examples/fx/fx-pro/preview/02_pages_02_arch.html +3 -0
  33. package/examples/fx/fx-pro/preview/03_pages_03_table.html +1 -0
  34. package/examples/fx/fx-pro/preview/04_pages_04_chart.html +3 -0
  35. package/examples/fx/fx-pro/preview/05_pages_05_timeline.html +2 -0
  36. package/examples/fx/fx-pro/preview/06_pages_06_longtext.html +4 -0
  37. package/examples/fx/fx-pro/preview/07_pages_07_safearea.html +2 -0
  38. package/examples/fx/fx-pro/preview/08_pages_08_contrast.html +1 -0
  39. package/examples/fx/fx-pro/preview/09_pages_09_custgeom.html +1 -0
  40. package/examples/fx/fx-pro/preview/10_pages_10_numbers.html +1 -0
  41. package/examples/fx/fx-pro/preview/11_pages_11_section.html +1 -0
  42. package/examples/fx/fx-pro/preview/12_pages_12_summary.html +1 -0
  43. package/examples/fx/fx-pro/preview/deck.html +10 -0
  44. package/examples/fx/fx-pro/preview/layout.json +2325 -0
  45. package/examples/fx/fx-pro/preview/measured.json +814 -0
  46. package/examples/fx/fx-pro.pptx +0 -0
  47. package/examples/fx/ren-single/01.png +0 -0
  48. package/examples/fx/ren-spliced/01.png +0 -0
  49. package/examples/fx/ren-spliced/03.png +0 -0
  50. package/examples/fx/seed.pptx +0 -0
  51. package/examples/fx/single.pptx +0 -0
  52. package/examples/fx/spliced.pptx +0 -0
  53. package/examples/smoke/deck.yaml +26 -0
  54. package/examples/smoke/imported/deck.yaml +30 -0
  55. package/examples/smoke/imported/import-styles.json +126 -0
  56. package/examples/smoke/imported/pages/slide_01.yaml +31 -0
  57. package/examples/smoke/imported/pages/slide_02.yaml +38 -0
  58. package/examples/smoke/imported/pages/slide_03.yaml +55 -0
  59. package/examples/smoke/imported/preview-rt/01_pages_slide_01.html +1 -0
  60. package/examples/smoke/imported/preview-rt/02_pages_slide_02.html +1 -0
  61. package/examples/smoke/imported/preview-rt/03_pages_slide_03.html +1 -0
  62. package/examples/smoke/imported/preview-rt/deck.html +1 -0
  63. package/examples/smoke/imported/preview-rt/layout.json +434 -0
  64. package/examples/smoke/imported/reference/previews/01.png +0 -0
  65. package/examples/smoke/imported/reference/previews/02.png +0 -0
  66. package/examples/smoke/imported/reference/previews/03.png +0 -0
  67. package/examples/smoke/imported/source.pptx +0 -0
  68. package/examples/smoke/media-test.png +0 -0
  69. package/examples/smoke/out-smoke.pptx +0 -0
  70. package/examples/smoke/pages/01_cover.yaml +27 -0
  71. package/examples/smoke/pages/02_content.yaml +42 -0
  72. package/examples/smoke/pages/03_chart.yaml +35 -0
  73. package/examples/smoke/preview/01_pages_01_cover.html +1 -0
  74. package/examples/smoke/preview/02_pages_02_content.html +1 -0
  75. package/examples/smoke/preview/03_pages_03_chart.html +1 -0
  76. package/examples/smoke/preview/deck.html +1 -0
  77. package/examples/smoke/preview/layout.json +378 -0
  78. package/lib/autodeclare.js +40 -0
  79. package/lib/commands.js +161 -0
  80. package/lib/crosscheck.js +300 -0
  81. package/lib/imgmeta.js +55 -0
  82. package/lib/index.js +239 -0
  83. package/lib/measurement.js +209 -0
  84. package/lib/msrender.js +123 -0
  85. package/lib/pptd/export-pptx.js +686 -0
  86. package/lib/pptd/import-pptx.js +774 -0
  87. package/lib/pptd/layout.js +209 -0
  88. package/lib/pptd/render-html.js +254 -0
  89. package/lib/pptd/schema.js +518 -0
  90. package/lib/pptd/svgCharts.js +147 -0
  91. package/lib/pptxPy.js +205 -0
  92. package/lib/preset-delivery.js +195 -0
  93. package/lib/preview-server.js +164 -0
  94. package/lib/router.js +165 -0
  95. package/lib/scaffold.js +315 -0
  96. package/lib/skill.js +174 -0
  97. package/lib/splice.js +219 -0
  98. package/lib/state.js +88 -0
  99. package/lib/surgical.js +369 -0
  100. package/lib/templates.js +400 -0
  101. package/lib/tools.js +955 -0
  102. package/lib/verify.js +828 -0
  103. package/lib/xmljs.js +112 -0
  104. package/lib/zips.js +117 -0
  105. package/package.json +81 -0
  106. package/scripts/audit-manual-facts.mjs +294 -0
  107. package/scripts/audit-template-fidelity.mjs +162 -0
  108. package/scripts/bench-m2.mjs +48 -0
  109. package/scripts/build-templates.mjs +63 -0
  110. package/scripts/build.mjs +14 -0
  111. package/scripts/build.sh +7 -0
  112. package/scripts/check-lib-freshness.mjs +70 -0
  113. package/scripts/check-preset.mjs +189 -0
  114. package/scripts/check-tracked-lf.mjs +84 -0
  115. package/scripts/dbg-import.mjs +13 -0
  116. package/scripts/dbg-media.mjs +10 -0
  117. package/scripts/dbg-tpl-inventory.mjs +23 -0
  118. package/scripts/dump-workflow-baseline.mjs +38 -0
  119. package/scripts/e2e-1.0.mjs +129 -0
  120. package/scripts/eval-skills-blind.mjs +91 -0
  121. package/scripts/eval-skills-score.mjs +129 -0
  122. package/scripts/export-orig-templates.ps1 +17 -0
  123. package/scripts/fixtures/workflow-baseline.json +300 -0
  124. package/scripts/fixtures.mjs +501 -0
  125. package/scripts/import-user-templates.mjs +60 -0
  126. package/scripts/install-desktop.mjs +312 -0
  127. package/scripts/install.mjs +209 -0
  128. package/scripts/migrate-templates-dualtrack.mjs +71 -0
  129. package/scripts/preflight-1.0.mjs +198 -0
  130. package/scripts/probe-asset-freshness.mjs +57 -0
  131. package/scripts/probe-effect-semantics.mjs +69 -0
  132. package/scripts/probe-reparse-safety.mjs +32 -0
  133. package/scripts/rebuild-templates-c11.mjs +69 -0
  134. package/scripts/regression-real.mjs +84 -0
  135. package/scripts/release-sync.mjs +425 -0
  136. package/scripts/shot-templates.mjs +49 -0
  137. package/scripts/smoke.mjs +2512 -0
  138. package/scripts/verify-bundle-install.mjs +281 -0
  139. package/scripts/verify-fresh-install.mjs +260 -0
  140. package/skills/ppt-studio-copy/SKILL.md +139 -0
  141. package/skills/ppt-studio-craft/SKILL.md +171 -0
  142. package/skills/ppt-studio-data/SKILL.md +173 -0
  143. package/skills/ppt-studio-manual/SKILL.md +104 -0
  144. package/templates/academic-white/deck.yaml +24 -0
  145. package/templates/academic-white/pages/_chart.yaml +43 -0
  146. package/templates/academic-white/pages/_compare.yaml +37 -0
  147. package/templates/academic-white/pages/_content.yaml +35 -0
  148. package/templates/academic-white/pages/_cover.yaml +28 -0
  149. package/templates/academic-white/pages/_ending.yaml +28 -0
  150. package/templates/academic-white/pages/_flow.yaml +61 -0
  151. package/templates/academic-white/preview.png +0 -0
  152. package/templates/academic-white/template.yaml +14 -0
  153. package/templates/business-blue/deck.yaml +28 -0
  154. package/templates/business-blue/pages/_chart.yaml +36 -0
  155. package/templates/business-blue/pages/_compare.yaml +45 -0
  156. package/templates/business-blue/pages/_content.yaml +80 -0
  157. package/templates/business-blue/pages/_cover.yaml +39 -0
  158. package/templates/business-blue/pages/_ending.yaml +32 -0
  159. package/templates/business-blue/pages/_flow.yaml +87 -0
  160. package/templates/business-blue/preview.png +0 -0
  161. package/templates/business-blue/template.yaml +14 -0
  162. package/templates/pitch-bold/deck.yaml +23 -0
  163. package/templates/pitch-bold/pages/_chart.yaml +32 -0
  164. package/templates/pitch-bold/pages/_compare.yaml +36 -0
  165. package/templates/pitch-bold/pages/_content.yaml +58 -0
  166. package/templates/pitch-bold/pages/_cover.yaml +31 -0
  167. package/templates/pitch-bold/pages/_ending.yaml +27 -0
  168. package/templates/pitch-bold/pages/_flow.yaml +46 -0
  169. package/templates/pitch-bold/preview.png +0 -0
  170. package/templates/pitch-bold/template.yaml +14 -0
  171. package/templates/tech-dark/deck.yaml +24 -0
  172. package/templates/tech-dark/pages/_chart.yaml +27 -0
  173. package/templates/tech-dark/pages/_compare.yaml +38 -0
  174. package/templates/tech-dark/pages/_content.yaml +51 -0
  175. package/templates/tech-dark/pages/_cover.yaml +30 -0
  176. package/templates/tech-dark/pages/_ending.yaml +25 -0
  177. package/templates/tech-dark/pages/_flow.yaml +80 -0
  178. package/templates/tech-dark/preview.png +0 -0
  179. package/templates/tech-dark/template.yaml +14 -0
package/lib/router.js ADDED
@@ -0,0 +1,165 @@
1
+ /**
2
+ * ppt-studio 路由:语义检测(双通道之"语义")+ 工作流提示词注入。
3
+ * 规则(设计定稿):明确 PPT 任务意图才进、明确无关才退、弱信号不切;
4
+ * /ppt on|off 命令强制(命令>语义);工作流是"语境"而非能力切换。
5
+ */
6
+
7
+ /**
8
+ * PPT 意图判据(本轮修正):
9
+ * 旧实现是"动词紧邻名词"的固定式正则(`做\s*(一个)?\s*ppt`),只能命中"做个 PPT"这类最短句式;
10
+ * 真实说法「帮我做一个 5 页的产品介绍 PPT」因为中间有修饰语而**判为无意向**——工作流提示词从不注入
11
+ * (0.1.5-rc.2 适配期实测:28 次 system-prompt/assemble 全部无 ppt-workflow 段)。
12
+ * 现改为"名词 + 任务动词在邻近窗口内共现",两个方向都算:
13
+ * - 仍要求**显式任务动词**(保持"弱信号不切":只提一句 PPT 不激活);
14
+ * - 允许中间夹修饰语(页数/主题/定语),窗口 32 字符;
15
+ * - 附件是 .pptx/.ppt 时直接视为强意图(用户把原稿递过来了)。
16
+ */
17
+ const PPT_NOUN_RE = /(?:ppt|pptx|powerpoint|幻灯片|演示文稿|演示文档|汇报文稿|deck|slides?)/i
18
+ const PPT_VERB_RE = /(?:做|制作|生成|创建|撰写|编写|设计|搞|整|改|修改|调整|调|补|补充|补完|添加|新增|追加|增页|加页|加几页|插入|美化|优化|统一|整理|替换|重做|换|总结|提炼|梳理|复盘|概述|浓缩|汇报|转换|复刻|导出|排版|配图|配色)/
19
+ /** 名词与动词之间允许的最大间隔(修饰语长度)。 */
20
+ const PPT_INTENT_WINDOW = 32
21
+
22
+ export function isPptIntent(text, attachments) {
23
+ const t = text ?? ''
24
+ if (!t && !attachments?.length) return false
25
+ // 附件是 ppt/pptx:强意图(用户把原稿/模板递过来了)
26
+ if (attachments?.some((a) => /\.pptx?$/i.test(a.name ?? a.path ?? a.filename ?? ''))) return true
27
+ if (!t) return false
28
+ return nearCooccur(t, PPT_NOUN_RE, PPT_VERB_RE, PPT_INTENT_WINDOW)
29
+ }
30
+
31
+ /** 两个模式是否在 text 中以 ≤ window 字符的间隔共现(与先后顺序无关)。 */
32
+ function nearCooccur(text, aRe, bRe, window) {
33
+ const a = allMatches(text, aRe)
34
+ if (a.length === 0) return false
35
+ const b = allMatches(text, bRe)
36
+ if (b.length === 0) return false
37
+ for (const x of a) {
38
+ for (const y of b) {
39
+ const gap = x.index <= y.index ? y.index - (x.index + x[0].length) : x.index - (y.index + y[0].length)
40
+ if (gap <= window) return true
41
+ }
42
+ }
43
+ return false
44
+ }
45
+
46
+ function allMatches(text, re) {
47
+ const out = []
48
+ const g = new RegExp(re.source, re.flags.includes('g') ? re.flags : `${re.flags}g`)
49
+ let m
50
+ while ((m = g.exec(text)) !== null) {
51
+ out.push(m)
52
+ if (m.index === g.lastIndex) g.lastIndex++
53
+ if (out.length > 64) break
54
+ }
55
+ return out
56
+ }
57
+
58
+ /** 判断用户消息是否"明确无关"(触发工作流退出)。仅在工作流已激活时生效。 */
59
+ const PPT_OFF_RE = new RegExp(
60
+ [
61
+ // 否定/停止 + 名词:「不用做 PPT 了」「先不做幻灯片」
62
+ String.raw`(?:别|不用|不需要|不要|取消|停止|退出|不做|不搞|不写|先不|别再|放弃).{0,8}(?:ppt|pptx|幻灯片|演示文稿|演示文档|deck|slides?)`,
63
+ // 名词 + 收尾语:「PPT 不做了」「演示文稿先放一放」
64
+ String.raw`(?:ppt|pptx|幻灯片|演示文稿|演示文档|deck|slides?).{0,8}(?:不做了|不弄了|不用了|先不做|先放一放|暂停|算了|到此为止)`,
65
+ // 明确划清界限:「这跟 PPT 无关」「不是幻灯片」
66
+ String.raw`(?:这|此|那|该)?(?:和|跟|与)?.{0,6}(?:ppt|pptx|幻灯片|演示文稿).{0,6}(?:无关|没关系|不是|无关紧要)`,
67
+ String.raw`(?:无关|不是|不属于).{0,6}(?:ppt|pptx|幻灯片|演示文稿)`,
68
+ String.raw`(?:ppt|pptx|幻灯片|演示文稿).{0,6}(?:无关|之外|以外)`,
69
+ ].join('|'),
70
+ 'i',
71
+ )
72
+
73
+ export function isPptOff(text) {
74
+ if (!text) return false
75
+ return PPT_OFF_RE.test(text)
76
+ }
77
+
78
+ const TASK_RULES = [
79
+ { task: 'summarize', re: /(总结|提炼|梳理|复盘|概述|浓缩)/ },
80
+ { task: 'edit', re: /(修改|调整|改动|统一|美化|优化|替换|重做|换)/ },
81
+ { task: 'augment', re: /(补(充|完)?|添加|新增|追加|增页|加几页|插入)/ },
82
+ ]
83
+
84
+ export function detectTaskType(text) {
85
+ for (const rule of TASK_RULES) {
86
+ if (rule.re.test(text)) return rule.task
87
+ }
88
+ return 'from-scratch'
89
+ }
90
+
91
+ /** 快速生成(Quick Mode)语义:quick 词 + ppt 语境(进入时需 ppt 意图;工作流内自动成立)。 */
92
+ const QUICK_RE = /(简单|快速|快点|随手|简版|简介|大概|随便做|弄个|quick|simple|minimal|brief)/i
93
+
94
+ export function isQuickIntent(text, inPptContext = false) {
95
+ if (!QUICK_RE.test(text ?? '')) return false
96
+ return inPptContext || isPptIntent(text)
97
+ }
98
+
99
+ /** 工作流激活段(注入 assembled.sections)。 */
100
+ export function workflowSection(taskType, cfg) {
101
+ if (cfg.quick) {
102
+ return {
103
+ name: 'ppt-workflow',
104
+ text: [
105
+ '=== PPT 工作流已激活(dsh-ppt-studio · 快速模式) ===',
106
+ `任务类型:${taskType ?? 'unknown'}(from-scratch 从头 / augment 补完 / edit 修改 / summarize 总结)|协作 ${cfg.mode}|引擎 ${cfg.engine}(常见值见下)`,
107
+ '',
108
+ '【快速模式铁律】(低 token 快交付,质量底线不变)',
109
+ '0. 不熟悉 DSL 语法:先调 ppt_schema;要现成骨架:ppt_new 生成样例再改。',
110
+ '1. S0 最小化:按消息推断,最多一次简短确认(≤1 问);不 grill、不做多轮讨论。',
111
+ '2. 版式:用中性三件套骨架(封面/内容/结尾),**不套内置模板**、不做视觉定调;配色与字号按本册主题(按题材定)走,下限以用户指令为准(未给下限不做强制)。',
112
+ '3. 素材:只使用用户提供或纯色块/图形表达;不做素材搜索、不生成复杂图表(需要时用表格或简单柱形)。',
113
+ '4. 页数:≤8 页(用户指定除外);每页文本精简(每块 ≤40 字为默认参考,用户要求可放宽)。',
114
+ '5. 审阅:ppt_render + ppt_verify 必跑——内容互压/出界/文本溢出/预期外重叠(设计意图声明制)必须清零;出界分级同标准模式(超页面不可声明;安全区外有意元素逐项加 expectedOutOfSafeArea);autoDeclare 后附一句意图说明;跳过截图视觉审阅与美学迭代。',
115
+ '6. 交付:ppt_export(引擎 auto=pptd)→ 交付说明标注"快速模式:未经视觉审阅"。',
116
+ '7. 引擎:auto 默认 = pptd(自研主引擎,图表矢量拼绘);python-pptx 仅显式指定或 pptd 失败时使用(其图表降级为表格)。不要自行切换引擎。',
117
+ '8. 用户明确要求始终优先;回到完整模式:/ppt normal。',
118
+ ].join('\n'),
119
+ }
120
+ }
121
+ return {
122
+ name: 'ppt-workflow',
123
+ text: [
124
+ '=== PPT 制作工作流已激活(dsh-ppt-studio) ===',
125
+ `任务类型:${taskType ?? 'unknown'}(from-scratch 从头 / augment 补完 / edit 修改 / summarize 总结)`,
126
+ `协作模式:${cfg.mode}|内容忠实度 ${cfg.fidelity}|审阅 ${cfg.review}|质量 ${cfg.quality}|引擎 ${cfg.engine}|暂停点 ${cfg.pauseAfter.length ? cfg.pauseAfter.join(',') : '无'}(/ppt 可改)`,
127
+ '',
128
+ '【执行阶段】(按序推进,已完成可跳过,循环至交付):',
129
+ ' S0 规格澄清 → S1 大纲与素材准备 → S2 视觉定调(样例页)→ S3 逐页制作 → S4 页审循环 → S5 整体审 → S6 导出交付',
130
+ '',
131
+ '【入门(减少试错,反馈整合)】',
132
+ ' - 语法速查:先调 ppt_schema(deck.yaml/元素/主题 token/声明/安全区速查);要完整可跑样例:ppt_new 生成示例工程(含 expectedOverlaps 与 safeArea 范本);回归样例在插件 examples/smoke。',
133
+ ' - 制作手册(内置技能,按需加载;纯增益,不改任何铁律与门禁):定纲/定版式前加载 `ppt-studio-craft`;页面要放数字与图表时加载 `ppt-studio-data`;写标题与要点时加载 `ppt-studio-copy`。三本只给启发式与反例——**门禁数值一律以 ppt_verify 输出与用户指令为准**;不加载也照常工作。',
134
+ ' - 答疑手册 `ppt-studio-manual`(内置技能):**只在用户提问时加载**(问用法/写法/报错/交付路径)——**制作开工时不要加载**。制作中只是自己拿不准某个写法/工具语义 → 调 ppt_schema(DSL 权威速查)/ ppt_check / ppt_verify 拿输出,不要靠读手册。',
135
+ ' - line 元素可省略 bounds(由 points 的 AABB 自动推导,w/h≥1px)。',
136
+ ' - 模板背景带 logo/页眉/页脚带:在 deck.yaml theme.safeArea(或页面级 safeArea)配置上/下/左/右安全边距,verify 会把安全区外的元素判为出界;ppt_render debug=true 会画出安全区参考框。',
137
+ ' - 文本溢出:verify 与导出共用同一保守度量;**字号下限 = 用户指令**——用户给出最小字号(如"不得小于12号")→ 把该值写入 theme.minFontSize 并严格执行——导出 auto-fit 缩字不得低于它;到下限仍溢出会明确报告(✗),请扩大容器或精简文案;用户未给下限 → 不做强制(auto-fit 仅 60% 原字号保底,绝不升字)。',
138
+ ' - 批量声明:色块衬底/图片标注/箭头跨越等"警告级"未声明重叠,用 ppt_verify autoDeclare=true 一键写入 expectedOverlaps;内容互压(content-collision)不支持自动声明,必须手工改布局。写入后输出**声明清单**(每对附一句意图:色块衬底/图上标注/箭头跨越),说不清意图的对子必须改布局而不是声明(D1)。',
139
+ ' - 出界声明(expectedOutOfSafeArea)不批量、必须手工:验证报安全区出界错误时,确认"这是有意的 logo/角标"才加入声明;不确定就先移动元素。',
140
+ '',
141
+ '【铁律】',
142
+ '1. 产物 = 中间层项目(deck.yaml + pages/*.yaml + media/)。页面用元素描述(bounds 为 [x,y,w,h] px,1px=1pt,原点左上);不要直接写 pptx。',
143
+ '2. 每页制作后依次运行:ppt_render → ppt_verify(数字审阅)→ ppt_shot(有读图能力时视觉审阅);overlap/out-of-page/text-overflow 错误清零后才算通过。**重叠语义(设计意图声明制)**:设计页面时把**有意**重叠的元素对记入页面 `expectedOverlaps`(图片上标注、色块衬底、箭头跨越等);审阅时警告级重叠逐对与声明对照——命中 = 确认(✓ 预期重叠),未命中 = 设计预期外错误(修正布局,或确认是有意的 → 补声明再验);**嵌套承载(面板→框→文字)只需声明相邻层对,隔层由声明闭包自动通过**;**内容互压(文字/表格/图表相互遮挡)不支持声明豁免,永远错误**;纯装饰元素可加 `role: decoration` 豁免**重叠**(装饰性=设计意图声明)。**出界分级(与声明制同构)**:超页面边界=永远错误不可声明;超安全区(模板页眉页脚带)=声明制——有意落在带上的 logo/角标/水印逐元素写入页面 `expectedOutOfSafeArea`(不批量、必须手工,id 必须存在);未命中=错误。declaration 之后若要免声明批量使用:仅重叠可用 `ppt_verify autoDeclare=true`(见入门段),出界声明永远手工。',
144
+ '3. 样式只用 theme token($colors/$textStyles 引用);新增风格先改 theme,不允许页面里出现脱离主题的颜色/字号。',
145
+ '4. 文本必须给出文本框实际容纳能力:先算文本面积再放框(字体字号→行宽行数),溢出用更小字号或更大框修复,不要靠视觉猜。',
146
+ '5. 用户明确要求(hardConstraints)任何模式下严格遵循;未指定处可自决。',
147
+ '6. 素材:用户提供的 > 从用户内容提取(文档/网页)> 自行生成/搜索;素材理解(物理规格程序化读取 + 语义理解),无读图能力时请用户描述后写入素材说明。',
148
+ '6a. **视觉理解通道(v0.8.0→v0.9.1,先看后做;参考任务 = 参考双轨)**:用户提供的 .pptx(参考稿/待改稿)经 `ppt_import` 后工作区**自动带参考层**——`reference/source.pptx`(零失真真相)+ `reference/previews/NN.png`(Office 真渲染整页,COM 可用时)+ deck.yaml 顶部 `referenceSource`。**创作前先 read_image 看 reference/previews/*.png(整页真身:配色/版式/形状语言/字体气质/渐变阴影)→ 视觉理解写入设计摘要,再动手**——与模板双轨(6b)"按模板做"是同一通道、完全等价;无 Office 时自动降级(XML 推断 + source.pptx 可用 `ppt_visual` 补渲 + 标注"未经 Office 真渲染理解")。**"完整明白原稿长什么样"是参考类任务的第一步,不能跳过**。',
149
+ '6b. **模板决策(S1 前完成,需求 2/4)**:按**来源优先级**决定参考谁——① **用户给了自己的模板,或任务是对既有 ppt 补完/修改** ⇒ **只以用户那份为参考**(先 `ppt_import` 读真身:`reference/previews/*.png` + `audit.yaml`),**内置模板一律不参与**(也别拿它当风格种子);② **用户明确要求用内置模板** ⇒ 由用户指令决定是他自己挑、还是你按**主题适配度**挑一套(`ppt_templates` 展示清单与预览图),选定后 `ppt_new dir=<工作区> template=<id>` 复制工作区;③ **用户没提模板** ⇒ **默认按题材自己设计风格**(配色/版式/字体都从主题来),**不要把内置模板当兜底**;只有某个内置模板**明显贴合**该题材时才考虑参考它(低优先级,并说明为什么贴合)。内置模板现状:business-blue 商务蓝 / academic-white 学术会议 / tech-dark 科技深色 / pitch-bold 路演大字。**用户自建模板库(v1.0.6)**:用户交 .pptx 要入库 → `ppt_template_add pptx=<绝对路径>`(内部先 import,保留原始 pptx 作真相层;写进**用户层** `<dshHome>/ppt-studio/templates`,升级不丢);要删 → `ppt_template_remove id=<id>`(**只能删自建的**,随包模板拒删)。用户自己攒的模板**视同"用户给的模板"**,在优先级里属最高档。**模板工作区语义(v0.7)**:`pages/_*.yaml` 是参考母版(**不注册进 deck.pages,不进 render/verify 门禁**,仅作版式参照);`pages/01_opening.yaml` 是模板首母版的**正式副本**(注册进门禁)——先 `ppt_verify autoDeclare=true` 声明模板固有有意叠层,剩余错误是模板原文案残留,替换/删除后自然干净;新增正式页 = 复制母版去 `_` 前缀 + 注册进 deck.yaml pages。"统一模板保持基础样式"要求走同一路径(模板文件先 ppt_import 并启用其 theme 聚合块)。**模板一致性(v0.5)**:verify theme-conformance strict 默认门禁——页面颜色必须 ∈ theme.colors 或中性灰;页面要新颜色就先加进 theme。**模板双轨(v0.9)**:模板 = 骨架层(pages/_*.yaml 母版,结构机器可验证)+ 真相层(原始 pptx + Office 真渲染整页 PNG + styleAudit)。materialize 后工作区含 `reference/`(template.pptx、previews/NN.png、audit.yaml),deck.yaml 顶部记 `referenceTemplate`(id/previews 路径/audit 引用)。**"按模板做" = "参考用户给的 ppt 制作",完全等价(同一参考双轨通道)**:创作前先 read_image 看 `reference/previews/*.png`(整页真身:配色/版式/形状语言/字体气质/渐变阴影)→ 读 `reference/audit.yaml`(视觉审计缓存,`ppt_template_styleaudit` 一次性生成)→ 视觉理解写入设计意图 → 再动手创作;无 reference(无 Office 收纳/内置模板)则退化为骨架层 + audit 缺失说明,不再有"从 40% 保真近似里猜"的失真环节。**手术模式(v0.10,候选 B)**:成品要"看起来就是模板原样"时用 `ppt_patch`(模板真身 + 工作区内容——只改 <a:t>,rPr/几何/渐变/字体/图片原样保留;未动页内容 sha256 验证不变;与 ppt_export 互不影响,常规导出仍走渲染)。模板工作区自带 reference/template.pptx 时直接用它作底版。',
150
+ '7. 交付前:S4 全通过 + S6 导出 .pptx + 交付说明(素材来源、未视觉审阅项、修改指引)。**内容审阅(M3 重写 2026-09-18)**:有具体数字/事实主张的稿子,交付前用 `ppt_crosscheck` 生成「审阅材料包」(全页正文 + 素材清单 + 审阅协议)→ 再按协议做一次内容审阅:**默认用独立子代理**(全新上下文、只给材料包与素材文件;**用户明确禁止子代理时改自审并标注「自审(同上下文)」**),逐条判"一致 / 冲突 / 无来源支撑 / 无法核实",重点核对同一指标跨页值是否一致、数字是否有依据、结论是否强于数据;**没有外部素材时只能判"无法核实",不得报绿**。该工具**只汇总材料、不做判定、不构成门禁**(旧版 grounded/unmapped 状态与跨页数字分组已废除)。制作进程中**供用户看稿优先用 ppt_preview(dir)**(返回同源预览链接,用户点击即看,附在交付信息里);导出前/后均可预览。**修改既有精美 PPT 的某一页(edit/augment 高频场景)**:ppt_import 参考层读真身 → 改工作区页并审阅清零 → **`ppt_splice`**(只把该页替换进原稿:保留源母版横幅/页脚/备注关系/媒体,其余页条目 SHA256 逐字节一致——不要整册近似重渲伤及其他页)→ 交付整册副本;用户要"单页独立版"再 **`ppt_slice`**;抽查该页观感用 **`ppt_visual pages=N`**(按页真渲染,不必整册);"看起来就是模板原样"用 `ppt_patch`(手术贴模板)。',
151
+ '8. 版式系统(审美基线,用户风格优先):写页面之前先定版式规则——默认基线:8px 网格对齐、3-4 级字号阶梯、4-5 色主题(只在 $theme 内取色)、统一间距节奏(8/16/24px)、左右外边界一致、构图重心居中;**用户对风格有要求(字体/字号/配色/密度)一律优先,默认基线仅为未指定时的风格参考**。文字:短句化(每块 ≤60 字为默认参考,可依内容放宽),**文本最小字号以用户指令为准**(hardConstraints;未给下限不设强制),长句在语义断点显式 \\\\n 换行。',
152
+ '9. 审美建议(ppt_verify 输出的 [·] 建议)不是门禁,但请逐条斟酌采纳;overlap/out-of-page/text-overflow 错误仍必须清零(审美改进不得引入元素区块冲突)。',
153
+ '10. 引擎:auto 默认 = pptd(自研主引擎,图表矢量拼绘);pptd 硬失败时自动回退 python-pptx(报告醒目标注降级);python-pptx 仅用户显式指定时使用(其图表降级为表格)——除非用户要求,不要自行切换引擎。',
154
+ '11. 质量档 audit(/ppt quality audit,从严门禁):禁 autoDeclare(工具已拦);每页必须 ppt_shot + 读图视觉审阅(无读图能力则降级结构 Lint 并在交付说明标注"未经视觉审阅");导出报告必须包含并核对 audit 回读断言(页数/尺寸/最小字号 ≥ minFontSize);有 Office 时导出后自动 **Office 真渲染**(成品视觉审核,逐页 read_image);交付说明含素材来源与数据脱敏/假设标注。',
155
+ '12. **真渲染抽检铁律(P1 事故教训)**:有 Office 时,真渲染抽查**必须覆盖所有含 table/chart/image/custGeom 的页**(每类至少一页,其余页轮换)——不要只挑"美观重点页";抽查后逐页 read_image(渲染图已带 1px=1pt 比例标注水印,降低坐标误判)。导出报告会给出建议覆盖页清单(P8 行)。',
156
+ ].join('\n'),
157
+ }
158
+ }
159
+
160
+ /** 协作各维度 x 提示的轻量文案(供 S0 使用,已含在 section)。 */
161
+ export function modeBrief(cfg) {
162
+ const mode = cfg.mode === 'auto' ? '自适应' : cfg.mode
163
+ const fidelity = cfg.fidelity === 'auto' ? '自适应' : cfg.fidelity
164
+ return `PPT 工作流(${mode} / ${fidelity} / review=${cfg.review} / quality=${cfg.quality})`
165
+ }
@@ -0,0 +1,315 @@
1
+ /**
2
+ * 新手引导(反馈 B2 ★):语法速查 + 一键样例工程。
3
+ * - SCHEMA_REF:pptd DSL 语法速查(ppt_schema 工具输出)。
4
+ * - scaffoldProject(dir):生成一个可跑通全链路的样例工程(ppt_new 工具)。
5
+ */
6
+ import { writeFile, mkdir, access } from 'node:fs/promises'
7
+ import { join } from 'node:path'
8
+
9
+ export const SCHEMA_REF = `# PPTD v1 语法速查(deck 工程 = deck.yaml + pages/*.yaml + media/)
10
+
11
+ ## 工程骨架(deck.yaml)
12
+ \`\`\`yaml
13
+ version: 1
14
+ title: 我的演示
15
+ size: [960, 540] # 或 {width, height};1px = 1pt,原点左上
16
+ theme: # 样式只在这里定义,页面元素引用 token
17
+ colors: {primary: "#2563EB", ink: "#1F2937"}
18
+ textStyles: {title: {fontSize: 32, color: "$ink", bold: true}, body: {fontSize: 16, color: "$ink"}}
19
+ safeArea: {top: 20, bottom: 20} # 可选:模板背景非内容区(logo/页眉页脚带)
20
+ minFontSize: 12 # 可选:用户给出字号下限时设置(如"不得小于14号" → 14);缺省无强制下限
21
+ pages:
22
+ - pages/01_cover.yaml
23
+ \`\`\`
24
+
25
+ ## 页面元素(元素通用字段)
26
+ \`\`\`yaml
27
+ elements:
28
+ - elementId: 页内唯一字符串 # 必须
29
+ elementType: text|shape|line|image|table|chart
30
+ bounds: [x, y, w, h] # 必须(line 可省略:由 points 的 AABB 自动推导)
31
+ role: content|background|decoration # 可选:层叠语义;decoration 完全豁免"重叠报告"(**不豁免出界**——要落在页眉页脚带仍须 expectedOutOfSafeArea 声明)
32
+ \`\`\`
33
+
34
+ ### text
35
+ \`\`\`yaml
36
+ - elementId: t1
37
+ elementType: text
38
+ bounds: [60, 60, 400, 50]
39
+ content:
40
+ text: "正文,长句在语义断点显式 \\n 换行"
41
+ style: "$body" # 引用 theme.textStyles;或直接写字段
42
+ # 也可内联:fontSize: 14 / color: "$ink" / bold / align: left|center|right / lineHeight / wrap: false
43
+ # ⚠ 样式键必须写在 content 内部(上例)。写在元素级(elementType 同级)无效——
44
+ # v0.15.0 起 ppt_check 直接报错(此前静默忽略按默认 18pt 度量,曾产生 146 条假错误)
45
+ # ✗ 错误示范(元素级样式键):
46
+ # elementType: text
47
+ # fontSize: 14 ← 无效!应放在 content: {text: "...", fontSize: 14} 内
48
+ \`\`\`
49
+
50
+ ### shape(kind = rect|roundRect|ellipse|triangle|custGeom + 常见 prst:rightArrow/leftArrow/upArrow/downArrow/leftRightArrow/pentagon/hexagon/chevron/parallelogram/diamond/octagon/star5/flowchartProcess|Decision|Data|Terminator)
51
+ \`\`\`yaml
52
+ - elementId: card
53
+ elementType: shape
54
+ kind: roundRect
55
+ bounds: [60, 60, 400, 200]
56
+ fill: "$colors.primary" # #hex | {color, alpha}(v0.11 透明度)或渐变对象:
57
+ # fill:
58
+ # type: gradient
59
+ # stops: [{pos: 0, color: "#79C9E2"}, {pos: 100, color: "#0485A8", alpha: 40}]
60
+ # angle: 90 # OOXML lin@ang 顺时针
61
+ line: {color: "#FFFFFF", width: 1}
62
+ rotation: 0 # 度
63
+ # 自定义几何(v0.11 候选 C,模板曲线装饰保真):
64
+ # - elementId: ribbon
65
+ # elementType: shape
66
+ # kind: custGeom
67
+ # bounds: [40, 40, 400, 160]
68
+ # path: {w: 100000, h: 40000, commands: [{cmd: moveTo, pts: [[0,0]]}, {cmd: cubicBezTo, pts: [[30000,0],[70000,40000],[100000,40000]]}, {cmd: arcTo, wR: 10000, hR: 10000, stAng: 0, swAng: 5400000}, {cmd: close}]}
69
+ # # commands: moveTo|lnTo|quadBezTo|cubicBezTo|arcTo(wR/hR/stAng/swAng)|close;坐标 = 路径空间抽象单位
70
+ \`\`\`
71
+
72
+ ### line(bounds 可省略;**仅 2 点**,多点折线请拆成多条 line——P2-3)
73
+ \`\`\`yaml
74
+ - elementId: arrow
75
+ elementType: line
76
+ points: [[100, 100], [300, 100]] # 恰好 2 点;或 {x1,y1,x2,y2}
77
+ arrow: true
78
+ line: {color: "#2563EB", width: 2}
79
+ \`\`\`
80
+
81
+ ### image / table / chart
82
+ \`\`\`yaml
83
+ - elementId: img
84
+ elementType: image
85
+ bounds: [60, 100, 400, 300]
86
+ src: media/pic.png # 相对 deck 根
87
+ fit: cover # cover|contain|fill
88
+ - elementId: tbl
89
+ elementType: table
90
+ bounds: [60, 420, 840, 80]
91
+ cols: ["指标", "数值"]
92
+ rows: [["A", "1"], ["B", "2"]]
93
+ header: true
94
+ - elementId: bar
95
+ elementType: chart
96
+ bounds: [60, 80, 400, 240]
97
+ chart: {type: bar, data: {cols: ["指标", "值"], rows: [["甲", 10], ["乙", 20]]}}
98
+ \`\`\` # chart.type: bar|line|pie;数据格式 = 宽表(cols: [分类, 值1, ...];多系列用 series 映射)。
99
+ # 单列 pairs 格式(cols 只写类别列、每行 [类别, 值])自动兼容(v0.6.1),但**建议用宽表**;
100
+ # 图表数据解析为全零时 render/export 会给出显式警告(不再静默)。
101
+ # 配色:colors: ["$primary", "$accent"]($ref 在预览与成品两层都解析);不写则用内置调色板(**不在 theme.colors 里**,
102
+ # 显式写了主题外颜色会出 [·] aesthetic-theme 建议——建议级,不是门禁)。chart 本身不含分类名/数值/图例,需自己用 text 补。
103
+
104
+ ## 页面级字段
105
+ \`\`\`yaml
106
+ pageType: cover|content
107
+ background: "#FFFFFF" # 或 "$themeRef" / {type: solid, color} / {type: image, src, fit}
108
+ safeArea: {top: 20} # 可选:覆盖主题安全区
109
+ notes: | # 可选:讲稿 → 导出成 pptx 备注页(notesSlide+notesMaster);单行也可 notes: "…"
110
+ 开场先给结论。
111
+ 第二行:再给证据。
112
+ 没有 notes 的页不产生任何备注部件(产物与旧版一致)。
113
+ expectedOverlaps: # 设计阶段声明"有意"重叠(审阅与声明对照)
114
+ - {pair: [card, t1]} # 色块衬底/图片标注等;内容互压 text×text 不可声明,永远错误
115
+ expectedOutOfSafeArea: # 有意落在"安全区外/模板页眉页脚带"的元素(logo/角标/水印)
116
+ - logo # 逐元素手工声明(autoDeclare 不自动生成出界声明);id 必须存在
117
+ contrastExempt: # P1-1:已确认对比度正常但启发式误报 → 豁免对比度建议
118
+ - inText # id 必须存在;用于"色块上再嵌深色框"的架构场景
119
+ overlapMode: declared # declared(默认)| lenient(草稿缓冲:未声明仅提示)
120
+ \`\`\`
121
+
122
+ ## 层叠角色推断(不在 expectedOverlaps 的规则)
123
+ - text/table/chart = content;shape/image = background(承载);line = line(引脚线/箭头)。
124
+ - content×content 重叠 → ERROR content-collision(不可声明豁免)。
125
+ - content×background / line×任意 → 警告级,未声明则 ERROR unexpected-overlap(修正布局或补声明)。
126
+ - role: decoration → 只豁免**重叠**(装饰性是设计意图声明);**不豁免出界**(要落在页眉页脚带 → 走 expectedOutOfSafeArea 声明)。
127
+ - **声明闭包(P0-2)**:嵌套承载只需声明**相邻层**(card×inBox、inBox×inText),隔层组合(card×inText)由包含关系传递自动通过——声明 45 对变 30 对,传递对不再人肉补。
128
+ - **出界分级**:超页面边界(放映不可见)→ 永远 ERROR,不可声明;超安全区(模板带内)→ 声明制(expectedOutOfSafeArea 命中 ✓ 预期出界,未命中 ERROR);页面 overlapMode: lenient → 未声明仅提示。
129
+ - 声明自证(D1):autoDeclare 写入后应输出"声明清单"(每对附一句意图,如色块衬底/图上标注/箭头跨越);说不清意图的对子必须改布局而不是声明。
130
+
131
+ ## 审阅/导出约定
132
+ - ppt_render → preview/*.html + layout.json;ppt_verify 错误([✗])清零是门禁,
133
+ [·] 为审美建议(非门禁,但请斟酌)。宽 > 窄 的元素安全:加 safeArea 后安全区外 = 出界。
134
+ - ppt_verify autoDeclare=true:把警告级未声明重叠一键写入 expectedOverlaps(内容互压仍要手工修)。
135
+ - ppt_export 的 out 支持绝对路径(原样使用)或文件名(相对 deck 目录)。
136
+ - 度量:预览/校验/导出共用同一保守估算;**字号下限 = 用户指令**(写入 theme.minFontSize,导出缩字同步遵守;未给下限不设强制,仅 60% 保底防荒谬)。
137
+ - 需要完整可跑样例:ppt_new 生成示例工程;回归样例在插件 examples/smoke。
138
+ `
139
+
140
+ /** 一键生成可跑通全链路的样例工程。已存在 deck.yaml 时拒绝(防覆盖)。 */
141
+ export async function scaffoldProject(dir, { name = 'my-deck' } = {}) {
142
+ const deckFile = join(dir, 'deck.yaml')
143
+ try {
144
+ await access(deckFile)
145
+ throw new Error(`目标目录已存在 deck.yaml(${deckFile}),拒绝覆盖;请换一个空目录`)
146
+ } catch (error) {
147
+ if (error?.code !== 'ENOENT') throw error
148
+ }
149
+ const pagesDir = join(dir, 'pages')
150
+ await mkdir(pagesDir, { recursive: true })
151
+ await mkdir(join(dir, 'media'), { recursive: true })
152
+ await writeFile(deckFile, [
153
+ 'version: 1',
154
+ `title: ${name}`,
155
+ 'size: [960, 540]',
156
+ 'theme:',
157
+ ' colors:',
158
+ ' primary: "#2563EB"',
159
+ ' ink: "#1F2937"',
160
+ ' lighter: "#E8EEFB"',
161
+ ' accent: "#FFD966"',
162
+ ' textStyles:',
163
+ ' title: {fontSize: 32, color: "$ink", bold: true}',
164
+ ' body: {fontSize: 16, color: "$ink"}',
165
+ ' label: {fontSize: 12, color: "$ink"}',
166
+ ' safeArea: {top: 16, bottom: 16}',
167
+ 'pages:',
168
+ ' - pages/01_cover.yaml',
169
+ ' - pages/02_content.yaml',
170
+ ' - pages/03_cardgrid.yaml',
171
+ ' - pages/04_architecture.yaml',
172
+ '',
173
+ ].join('\n'))
174
+ await writeFile(join(pagesDir, '01_cover.yaml'), [
175
+ 'pageType: cover',
176
+ 'background: {type: solid, color: "$primary"}',
177
+ 'safeArea: {top: 24, bottom: 24}',
178
+ 'elements:',
179
+ ' - elementId: title',
180
+ ' elementType: text',
181
+ ' bounds: [160, 200, 640, 70]',
182
+ ' content: {text: "标题:PPT 工作室一键样例", style: "$title", color: "#FFFFFF"}',
183
+ ' - elementId: sub',
184
+ ' elementType: text',
185
+ ' bounds: [160, 280, 640, 30]',
186
+ ' content: {text: "副标题:日期 / 作者", style: "$body", color: "#E8EEFB"}',
187
+ ' - elementId: rule',
188
+ ' elementType: line',
189
+ ' points: [[160, 260], [800, 260]]',
190
+ ' line: {color: "#FFFFFF", width: 1}',
191
+ '',
192
+ ].join('\n'))
193
+ await writeFile(join(pagesDir, '02_content.yaml'), [
194
+ 'pageType: content',
195
+ 'elements:',
196
+ ' - elementId: head',
197
+ ' elementType: text',
198
+ ' bounds: [60, 48, 840, 50]',
199
+ ' content: {text: "内容页:色块衬底 + 文字", style: "$title"}',
200
+ ' - elementId: panel',
201
+ ' elementType: shape',
202
+ ' kind: roundRect',
203
+ ' bounds: [60, 120, 420, 240]',
204
+ ' fill: "$lighter"',
205
+ ' - elementId: ptext',
206
+ ' elementType: text',
207
+ ' bounds: [90, 150, 360, 180]',
208
+ ' content: {text: "文字放在色块卡片上是有意设计:\\n本页演示 expectedOverlaps 声明。", style: "$body"}',
209
+ ' - elementId: note',
210
+ ' elementType: text',
211
+ ' bounds: [60, 440, 840, 26]',
212
+ ' content: {text: "提示:卡片上的文字需要声明预期重叠(见本页 expectedOverlaps)。", style: "$label"}',
213
+ 'expectedOverlaps:',
214
+ ' - {pair: [panel, ptext]}',
215
+ '',
216
+ ].join('\n'))
217
+ await writeFile(join(pagesDir, '03_cardgrid.yaml'), [
218
+ 'pageType: content',
219
+ 'elements:',
220
+ ' - elementId: head2',
221
+ ' elementType: text',
222
+ ' bounds: [60, 48, 840, 50]',
223
+ ' content: {text: "要素卡片:8px 网格 + 统一间距", style: "$title"}',
224
+ ' - elementId: c1',
225
+ ' elementType: shape',
226
+ ' kind: rect',
227
+ ' bounds: [60, 130, 260, 160]',
228
+ ' fill: "$lighter"',
229
+ ' - elementId: c2',
230
+ ' elementType: shape',
231
+ ' kind: rect',
232
+ ' bounds: [350, 130, 260, 160]',
233
+ ' fill: "$lighter"',
234
+ ' - elementId: c3',
235
+ ' elementType: shape',
236
+ ' kind: rect',
237
+ ' bounds: [640, 130, 260, 160]',
238
+ ' fill: "$lighter"',
239
+ ' - elementId: t1',
240
+ ' elementType: text',
241
+ ' bounds: [80, 150, 220, 40]',
242
+ ' content: {text: "要点一", style: "$body", bold: true}',
243
+ ' - elementId: t2',
244
+ ' elementType: text',
245
+ ' bounds: [370, 150, 220, 40]',
246
+ ' content: {text: "要点二", style: "$body", bold: true}',
247
+ ' - elementId: t3',
248
+ ' elementType: text',
249
+ ' bounds: [660, 150, 220, 40]',
250
+ ' content: {text: "要点三", style: "$body", bold: true}',
251
+ 'expectedOverlaps:',
252
+ ' - {pair: [c1, t1]}',
253
+ ' - {pair: [c2, t2]}',
254
+ ' - {pair: [c3, t3]}',
255
+ '',
256
+ ].join('\n'))
257
+ await writeFile(join(pagesDir, '04_architecture.yaml'), [
258
+ 'pageType: content',
259
+ '# P2-1/P0-2 常见模式:架构图 = 面板 → 深色框 → 文字 的三层嵌套;',
260
+ '# 只需声明【相邻层】对(panel×inBox、inBox×inText),隔层组合 (panel×inText) 由声明闭包自动通过',
261
+ 'expectedOverlaps:',
262
+ ' - {pair: [panel, inBox]}',
263
+ ' - {pair: [inBox, inText]}',
264
+ ' - {pair: [panel, chip]}',
265
+ ' - {pair: [chip, chipT]}',
266
+ ' - {pair: [panel, arrow]}',
267
+ ' - {pair: [badge, badgeT]}',
268
+ 'contrastExempt: [badgeT]',
269
+ '# P1-1:白字衬在浅色徽标上是既有设计 → contrastExempt 豁免对比度建议(id 必须存在)',
270
+ 'elements:',
271
+ ' - elementId: head',
272
+ ' elementType: text',
273
+ ' bounds: [60, 48, 840, 50]',
274
+ ' content: {text: "架构页:嵌套承载 + 声明闭包(声明相邻层即可)", style: "$title"}',
275
+ ' - elementId: panel',
276
+ ' elementType: shape',
277
+ ' kind: roundRect',
278
+ ' bounds: [60, 120, 840, 340]',
279
+ ' fill: "$lighter"',
280
+ ' - elementId: inBox',
281
+ ' elementType: shape',
282
+ ' kind: rect',
283
+ ' bounds: [100, 180, 300, 220]',
284
+ ' fill: "$primary"',
285
+ ' - elementId: inText',
286
+ ' elementType: text',
287
+ ' bounds: [130, 240, 240, 90]',
288
+ ' content: {text: "深色框上的白字(最上层承载)", style: "$body", color: "#FFFFFF"}',
289
+ ' - elementId: chip',
290
+ ' elementType: shape',
291
+ ' kind: rect',
292
+ ' bounds: [460, 180, 120, 80]',
293
+ ' fill: "#FFD966"',
294
+ ' - elementId: chipT',
295
+ ' elementType: text',
296
+ ' bounds: [470, 200, 100, 40]',
297
+ ' content: {text: "芯片", style: "$body"}',
298
+ ' - elementId: arrow',
299
+ ' elementType: line',
300
+ ' points: [[400, 290], [450, 290]]',
301
+ ' arrow: true',
302
+ ' line: {color: "#2563EB", width: 2}',
303
+ ' - elementId: badge',
304
+ ' elementType: shape',
305
+ ' kind: rect',
306
+ ' bounds: [700, 470, 120, 44]',
307
+ ' fill: "$lighter"',
308
+ ' - elementId: badgeT',
309
+ ' elementType: text',
310
+ ' bounds: [720, 482, 80, 22]',
311
+ ' content: {text: "徽标", style: "$label", color: "#FFFFFF"}',
312
+ '',
313
+ ].join('\n'))
314
+ return { dir, deckFile, files: [deckFile, join(pagesDir, '01_cover.yaml'), join(pagesDir, '02_content.yaml'), join(pagesDir, '03_cardgrid.yaml'), join(pagesDir, '04_architecture.yaml')] }
315
+ }