dsh-ppt-studio 1.1.4 → 1.2.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 (95) hide show
  1. package/README.md +5 -2
  2. package/docs/00-/350/256/276/350/256/241/346/272/220.md +4 -0
  3. package/docs/02-/346/212/200/346/234/257/346/212/245/345/221/212.md +114 -1
  4. package/docs/03-/346/233/264/346/226/260/346/227/245/345/277/227.md +171 -0
  5. package/docs/05-/350/277/255/344/273/243/346/265/201/347/250/213.md +24 -1
  6. package/docs/06-/350/257/204/345/256/241/344/270/216/346/265/213/350/257/225.md +2 -1
  7. package/docs/07-/345/244/215/346/235/202/345/233/276/350/203/275/345/212/233/347/253/213/351/241/271/344/270/216/351/243/216/351/231/251/347/231/273/350/256/260.md +319 -0
  8. package/docs/08-/351/230/266/346/256/265A/350/256/276/350/256/241/350/257/264/346/230/216-/347/273/223/346/236/204/345/205/263/347/263/273/344/270/216/345/233/276/344/270/223/345/261/236/346/240/241/351/252/214.md +306 -0
  9. package/docs/09-/344/273/243/347/240/201/345/234/260/345/233/276/344/270/216/346/250/241/345/235/227/350/201/214/350/264/243.md +148 -0
  10. package/docs/10-/345/256/277/344/270/273/350/200/246/345/220/210/347/202/271/346/270/205/345/215/225.md +49 -0
  11. package/docs/11-/346/234/257/350/257/255/350/241/250.md +42 -0
  12. package/docs/12-/345/244/215/346/235/202/345/233/276/351/200/232/350/267/257/347/232/204/351/200/211/346/213/251/344/270/216/351/252/214/346/224/266.md +65 -0
  13. package/docs/13-/345/217/221/347/211/210/345/207/206/345/244/207/350/256/241/345/210/222.md +79 -0
  14. package/docs/14-/345/217/221/345/270/203/346/270/205/345/215/225/357/274/210/350/215/211/347/250/277/357/274/211.md +37 -0
  15. package/docs/17-/346/211/213/347/224/27313/351/241/265/350/255/246/345/221/212/351/200/220/346/235/241/345/244/204/347/275/256.md +102 -0
  16. package/examples/fx/fx-pro/preview/layout.json +12 -0
  17. package/examples/fx/fx-pro/preview/measured.json +1 -1
  18. package/examples/fx/fx-pro.pptx +0 -0
  19. package/examples/fx/seed.pptx +0 -0
  20. package/examples/fx/single.pptx +0 -0
  21. package/examples/fx/spliced.pptx +0 -0
  22. package/examples/smoke/imported/preview-rt/layout.json +3 -0
  23. package/examples/smoke/imported/source.pptx +0 -0
  24. package/examples/smoke/out-smoke.pptx +0 -0
  25. package/examples/smoke/preview/layout.json +3 -0
  26. package/lib/group-render.js +177 -0
  27. package/lib/msrender.js +27 -12
  28. package/lib/pptd/blocks.js +52 -0
  29. package/lib/pptd/export-pptx.js +266 -11
  30. package/lib/pptd/layout.js +54 -3
  31. package/lib/pptd/line-rules.js +259 -0
  32. package/lib/pptd/line-text-rule.js +90 -0
  33. package/lib/pptd/relations.js +615 -0
  34. package/lib/pptd/render-html.js +19 -1
  35. package/lib/pptd/route-geometry.js +120 -0
  36. package/lib/pptd/schema.js +146 -11
  37. package/lib/pptd/style-profile.js +133 -0
  38. package/lib/router.js +2 -2
  39. package/lib/scaffold.js +54 -1
  40. package/lib/tools.js +22 -4
  41. package/lib/verify.js +69 -1
  42. package/package.json +20 -3
  43. package/scripts/audit-manual-facts.mjs +15 -3
  44. package/scripts/bench-diagram-scale.mjs +165 -0
  45. package/scripts/check-generators.mjs +22 -0
  46. package/scripts/check-lib-sync.mjs +33 -0
  47. package/scripts/check-line-text.mjs +30 -0
  48. package/scripts/check-package.mjs +60 -0
  49. package/scripts/check-removed-traces.mjs +37 -0
  50. package/scripts/check-syntax.mjs +44 -0
  51. package/scripts/check-unknown-keys.mjs +114 -0
  52. package/scripts/fixtures/handdrawn-baseline.json +177 -0
  53. package/scripts/fixtures/prompt-cost-baseline.json +178 -0
  54. package/scripts/fixtures/workflow-baseline.json +20 -15
  55. package/scripts/handdrawn-option-probe.mjs +78 -0
  56. package/scripts/probe-prompt-cost.mjs +127 -0
  57. package/scripts/red-probe.mjs +75 -0
  58. package/scripts/render-group.mjs +72 -0
  59. package/scripts/smoke.mjs +183 -12
  60. package/scripts/test-fast.mjs +108 -0
  61. package/scripts/test-map.mjs +162 -0
  62. package/scripts/test-release.mjs +64 -0
  63. package/scripts/verify-diagram-relations.mjs +405 -0
  64. package/scripts/verify-handdrawn.mjs +115 -0
  65. package/scripts/verify-handwritten.mjs +203 -0
  66. package/scripts/verify-native-charts.mjs +25 -10
  67. package/scripts/verify-primitives.mjs +714 -0
  68. package/scripts/verify-products.mjs +37 -0
  69. package/scripts/verify-shape.mjs +84 -0
  70. package/scripts/visual-pair-batch.mjs +77 -0
  71. package/skills/diagram-drawing/SKILL.md +23 -0
  72. package/skills/diagram-drawing/capability-matrix.md +22 -0
  73. package/skills/diagram-drawing/composition.md +22 -0
  74. package/skills/diagram-drawing/mechanisms.md +141 -0
  75. package/skills/diagram-drawing/pitfalls.md +89 -0
  76. package/skills/diagram-drawing/routing.md +17 -0
  77. package/skills/diagram-drawing/text-and-spacing.md +16 -0
  78. package/skills/diagram-drawing/verify.md +50 -0
  79. package/skills/diagrams/compare.md +24 -0
  80. package/skills/diagrams/flow.md +24 -0
  81. package/skills/diagrams/funnel.md +25 -0
  82. package/skills/diagrams/layers.md +18 -0
  83. package/skills/diagrams/loop.md +25 -0
  84. package/skills/diagrams/matrix.md +23 -0
  85. package/skills/diagrams/sequence.md +17 -0
  86. package/skills/diagrams/state.md +34 -0
  87. package/skills/diagrams/steps-ring.md +24 -0
  88. package/skills/diagrams/swimlane.md +23 -0
  89. package/skills/diagrams/timeline.md +24 -0
  90. package/skills/diagrams/tree.md +24 -0
  91. package/skills/ppt-studio-manual/SKILL.md +2 -2
  92. package/templates/academic-white/preview.png +0 -0
  93. package/templates/business-blue/preview.png +0 -0
  94. package/templates/pitch-bold/preview.png +0 -0
  95. package/templates/tech-dark/preview.png +0 -0
@@ -0,0 +1,203 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * 手写路径自证(阶段 A3,泛用性优先):`node scripts/verify-handwritten.mjs`
4
+ *
5
+ * 为什么要有这个脚本:族库再全,也总有"不属于任何族"的图;那时助手的唯一手段是**手写 elements**。
6
+ * 本脚本用**合成夹具**证明手写路径是一等公民,而且手写也有机械反馈:
7
+ * ① 一张合法的"手写复杂图"(分组底板 decoration + 容器 contains + >2 点折线 + attach + 标签 + 虚线反馈回路)
8
+ * ⇒ **0 错误**,且折线规则**不误报**,导出 parity 自证通过;
9
+ * ② 斜段连接线 ⇒ 报 `line-diagonal-segment`(提示怎么改);
10
+ * ③ 箭头端直段过短 ⇒ 报 `arrow-end-short-run`;
11
+ * ④ 刻意的斜线(环形/放射)用 `role: decoration` 声明 ⇒ **豁免、不再提示**。
12
+ * 全部断言 0 失败则退出 0。
13
+ */
14
+ import { mkdirSync, writeFileSync, rmSync, readFileSync } from 'node:fs'
15
+ import { join } from 'node:path'
16
+ import { tmpdir } from 'node:os'
17
+ import { resolveDeck } from '../lib/pptd/schema.js'
18
+ import { renderDeck } from '../lib/pptd/render-html.js'
19
+ import { exportPptx } from '../lib/pptd/export-pptx.js'
20
+ import { verifyDeck } from '../lib/verify.js'
21
+ import { checkLineRules } from '../lib/pptd/line-rules.js'
22
+ import { renderGroupIsolated } from '../lib/group-render.js'
23
+
24
+ let pass = 0
25
+ let fail = 0
26
+ const failures = []
27
+ const ok = (name, cond, detail = '') => {
28
+ if (cond) { pass++; console.log(`✓ ${name}${detail ? ` — ${detail}` : ''}`) }
29
+ else { fail++; failures.push(name); console.log(`✗ ${name}${detail ? ` — ${detail}` : ''}`) }
30
+ }
31
+
32
+ const dir = join(tmpdir(), `pptd-handwritten-${Date.now()}`)
33
+ rmSync(dir, { recursive: true, force: true })
34
+ mkdirSync(join(dir, 'pages'), { recursive: true })
35
+ writeFileSync(join(dir, 'deck.yaml'), [
36
+ 'version: 1', 'title: 手写自由发挥', 'size: [960, 540]', 'theme:',
37
+ // 暖色系(顺便验证"指定色系"这条泛用性诉求)
38
+ ' colors: {primary: "#B45309", accent: "#EA580C", soft: "#FED7AA", text: "#431407", bg: "#FFFBEB"}',
39
+ ' textStyles:', ' body: {fontSize: 14, color: "$text"}', ' line: {width: 2}',
40
+ ' safeArea: {top: 40, bottom: 40, left: 40, right: 40}',
41
+ 'pages:', ' - pages/01.yaml', ' - pages/02.yaml', ' - pages/03.yaml', ' - pages/04.yaml', ' - pages/05.yaml', '',
42
+ ].join('\n'))
43
+
44
+ // ① 合法的手写复杂图(完全不依赖任何族)
45
+ writeFileSync(join(dir, 'pages', '01.yaml'), [
46
+ 'pageType: content', 'elements:',
47
+ ' - {elementId: band, elementType: shape, kind: roundRect, bounds: [60, 90, 840, 150], fill: "$soft", role: decoration, roleReason: 分组底板}',
48
+ ' - {elementId: a, elementType: shape, kind: roundRect, bounds: [90, 120, 180, 90], fill: "$primary", contains: [a_lab]}',
49
+ ' - {elementId: a_lab, elementType: text, bounds: [100, 150, 160, 30], content: {text: "采集", fontSize: 16, color: "$bg", align: center}}',
50
+ ' - {elementId: b, elementType: shape, kind: roundRect, bounds: [390, 120, 180, 90], fill: "$primary", contains: [b_lab]}',
51
+ ' - {elementId: b_lab, elementType: text, bounds: [400, 150, 160, 30], content: {text: "仿真", fontSize: 16, color: "$bg", align: center}}',
52
+ ' - {elementId: c, elementType: shape, kind: roundRect, bounds: [690, 120, 180, 90], fill: "$accent", contains: [c_lab]}',
53
+ ' - {elementId: c_lab, elementType: text, bounds: [700, 150, 160, 30], content: {text: "输出", fontSize: 16, color: "$bg", align: center}}',
54
+ ' - elementId: e1', ' elementType: line',
55
+ ' points: [[270, 165], [330, 165], [330, 165], [390, 165]]', ' arrow: true',
56
+ ' attach: {from: {ref: a, side: right}, to: {ref: b, side: left}}',
57
+ ' - elementId: e2', ' elementType: line',
58
+ ' points: [[570, 165], [630, 165], [630, 165], [690, 165]]', ' arrow: true',
59
+ ' attach: {from: {ref: b, side: right}, to: {ref: c, side: left}}',
60
+ ' - elementId: fb', ' elementType: line',
61
+ ' points: [[240, 210], [240, 300], [750, 300], [750, 210]]', ' arrow: true', ' line: {color: "$text", width: 1, dash: dash}',
62
+ ' attach: {from: {ref: a, side: bottom}, to: {ref: c, side: bottom}}',
63
+ ' - {elementId: fb_lab, elementType: text, bounds: [455, 305, 90, 20], content: {text: "迭代反馈", fontSize: 13, color: "$text", align: center}}',
64
+ '',
65
+ ].join('\n'))
66
+
67
+ // ② 坏折线:斜段 + 箭头端过短
68
+ writeFileSync(join(dir, 'pages', '02.yaml'), [
69
+ 'pageType: content', 'elements:',
70
+ ' - {elementId: box, elementType: shape, kind: rect, bounds: [80, 120, 200, 90], fill: "$soft"}',
71
+ ' - elementId: bad_diag', ' elementType: line', ' points: [[600, 120], [800, 260]]', ' arrow: true',
72
+ ' - elementId: bad_short', ' elementType: line', ' points: [[300, 300], [515, 300], [520, 300]]', ' arrow: true',
73
+ '',
74
+ ].join('\n'))
75
+
76
+ // ③ 刻意斜线(环形语义)用 decoration 声明 ⇒ 应豁免
77
+ writeFileSync(join(dir, 'pages', '03.yaml'), [
78
+ 'pageType: content', 'elements:',
79
+ ' - elementId: ring_a', ' elementType: line', ' points: [[200, 200], [400, 350]]', ' arrow: false',
80
+ ' role: decoration', ' roleReason: 环形引导线的斜弦(设计意图)',
81
+ '',
82
+ ].join('\n'))
83
+
84
+ // ④ 新判据的正例与反例(用户裁定的区分:共线重叠/压框 = 缺陷;共用端点的扇出 = 标准画法)
85
+ writeFileSync(join(dir, 'pages', '04.yaml'), [
86
+ 'pageType: content', 'elements:',
87
+ // 正例①:互不相关的两条线走成共线重叠 ⇒ 必须报 line-collinear-overlap
88
+ ' - elementId: ov1', ' elementType: line', ' points: [[300, 380], [600, 380]]', ' arrow: false',
89
+ ' - elementId: ov2', ' elementType: line', ' points: [[400, 380], [700, 380]]', ' arrow: false',
90
+ // 正例②:线**压在盒子边框上** ⇒ 必须报 line-on-box-edge
91
+ ' - {elementId: edgebox, elementType: shape, kind: rect, bounds: [200, 150, 300, 120], fill: "$soft"}',
92
+ ' - elementId: onedge', ' elementType: line', ' points: [[200, 150], [500, 150]]', ' arrow: false',
93
+ // 反例:从**同一锚点**出发、共线且部分重合的一对线(扇出/树状干线)⇒ 不得报共线重叠
94
+ ' - elementId: fan_long', ' elementType: line', ' points: [[700, 430], [800, 430]]', ' arrow: false',
95
+ // R3 正反例:箭头插进图形(该报);同样几何但 roleReason 声明(不该报)
96
+ ' - elementId: ov_bad_in', ' elementType: line', ' points: [[340, 210], [390, 210]]', ' arrow: true',
97
+ ' - elementId: ov_declared', ' elementType: line', ' points: [[340, 230], [390, 230]]', ' arrow: true', ' roleReason: 强调贯穿(刻意插进图形)',
98
+ // R3 缺口正例:端点离图形边 ~6px(该报)
99
+ ' - elementId: ov_gap', ' elementType: line', ' points: [[560, 210], [572, 210]]', ' arrow: true',
100
+ ' - {elementId: gapbox, elementType: shape, kind: rect, bounds: [578, 150, 200, 120], fill: "$soft"}',
101
+ // X2 正反例:折返尖刺(该报)与其声明版(不该报)
102
+ ' - elementId: ov_spike', ' elementType: line', ' points: [[300, 470], [400, 470], [350, 470]]', ' arrow: false',
103
+ ' - elementId: ov_spike_declared', ' elementType: line', ' points: [[300, 495], [400, 495], [350, 495]]', ' arrow: false', ' roleReason: 刻意的往返重叠',
104
+ // X2 正例:**无箭头**连线端点离盒子 ~6px(该报 line-end-off-edge)
105
+ ' - elementId: ov_noarrow_gap', ' elementType: line', ' points: [[560, 230], [572, 230]]', ' arrow: false',
106
+ '',
107
+ ].join('\n'))
108
+
109
+ // ⑤ 组级裁剪取景(C2 的核心不变量):容器与"容器内部的组标题"必须被纳入,
110
+ // 离得远的旁观元素必须被排除——这条规则第一版写错过(8 vs 10),用断言锁死。
111
+ writeFileSync(join(dir, 'pages', '05.yaml'), [
112
+ 'pageType: content', 'groups:',
113
+ ' - {id: g_card, label: 卡片组, members: [c_shape, c_lab]}',
114
+ 'elements:',
115
+ ' - {elementId: c_shape, elementType: shape, kind: roundRect, bounds: [120, 300, 200, 90], fill: "$primary", contains: [c_lab]}',
116
+ ' - {elementId: c_lab, elementType: text, bounds: [130, 330, 180, 30], content: {text: "卡内文字", fontSize: 14, color: "$bg", align: center}}',
117
+ // 容器:contains 里有"几何上落在它内部"的标题(不在组 members 里)——必须靠 ③ 被纳入
118
+ ' - {elementId: c_box, elementType: shape, kind: roundRect, bounds: [100, 280, 240, 140], fill: "$soft", role: decoration, roleReason: 卡片底板, contains: [c_shape, c_lab, c_title]}',
119
+ ' - {elementId: c_title, elementType: text, bounds: [110, 288, 220, 22], content: {text: "卡片标题", fontSize: 13, color: "$text", align: center}}',
120
+ // 旁观者:离得远,不得被吞进取景
121
+ ' - {elementId: far_away, elementType: shape, kind: rect, bounds: [700, 470, 160, 40], fill: "$accent"}',
122
+ '',
123
+ ].join('\n'))
124
+
125
+ const ctx = await resolveDeck(dir)
126
+ await renderDeck(ctx, { out: 'preview' })
127
+ const layout = JSON.parse(readFileSync(join(dir, 'preview', 'layout.json'), 'utf8'))
128
+ const vOf = (i) => verifyDeck({ ...layout, pages: [layout.pages[i]] })
129
+
130
+ const v1 = vOf(0)
131
+ ok('手写复杂图(非任何族):**0 错误**(容器 contains + decoration 底板 + >2 点折线 + attach + 标签)',
132
+ v1.errors.length === 0, v1.errors.slice(0, 3).map((e) => `${e.code}:${e.id}`).join(' | ') || '无')
133
+ ok('手写复杂图:折线规则**不误报**(箭头端直段够长、全轴对齐)',
134
+ !v1.warns.some((w) => w.code === 'line-diagonal-segment' || w.code === 'arrow-end-short-run'),
135
+ v1.warns.map((w) => w.code).join(',') || '无相关警告')
136
+
137
+ const v2 = vOf(1)
138
+ ok('斜段连接线 ⇒ 报 `line-diagonal-segment`(带可操作的修法)',
139
+ v2.warns.some((w) => w.code === 'line-diagonal-segment' && /role: decoration/.test(w.message)),
140
+ v2.warns.map((w) => w.code).join(',') || '无')
141
+ ok('箭头端直段过短(20px 内的拐点)⇒ 报 `arrow-end-short-run`',
142
+ v2.warns.some((w) => w.code === 'arrow-end-short-run'), v2.warns.map((w) => w.code).join(',') || '无')
143
+
144
+ const v3 = vOf(2)
145
+ ok('刻意斜线用 `role: decoration` 声明 ⇒ **豁免**(不再提示,但仍是 warning 级、不阻断)',
146
+ !v3.warns.some((w) => w.code === 'line-diagonal-segment'), v3.warns.map((w) => w.code).join(',') || '无相关警告')
147
+
148
+ const v4 = vOf(3)
149
+ ok('互不相关的两条线**共线重叠** ⇒ 报 `line-collinear-overlap`(真缺陷判据)',
150
+ v4.warns.some((w) => w.code === 'line-collinear-overlap' && /ov1/.test(w.message) && /ov2/.test(w.message)),
151
+ v4.warns.map((w) => w.code).join(',') || '无')
152
+ ok('线**压在盒子边框上** ⇒ 报 `line-on-box-edge`(图 4 那类缺陷的机械判据)',
153
+ v4.warns.some((w) => w.code === 'line-on-box-edge'), v4.warns.map((w) => w.code).join(',') || '无')
154
+ ok('**共用端点**的扇出(标准树状干线)⇒ 不得报共线重叠(用户裁定:这样画更清晰)',
155
+ !v4.warns.some((w) => w.code === 'line-collinear-overlap' && /fan/.test(w.message)),
156
+ v4.warns.filter((w) => w.code === 'line-collinear-overlap').map((w) => w.message.slice(0, 46)).join(' | ') || '无')
157
+
158
+ // ⑥ 组级裁剪取景的不变量(C2)
159
+ const grp = await renderGroupIsolated(dir, { page: 5, group: 'g_card', out: join(dir, '.group-render') })
160
+ ok('组级取景:纳入容器与「容器内部的组标题」(C2 核心规则——第一版漏了这条,出 8 个元素而 CLI 出 10 个)',
161
+ ['c_shape', 'c_lab', 'c_box', 'c_title'].every((id) => grp.members.includes(id)), grp.members.join(','))
162
+ ok('组级取景:不吞入离得远的旁观元素("闭包把整页拉进来"是这条规则要防的事)',
163
+ !grp.members.includes('far_away'), `${grp.members.length} 个元素`)
164
+
165
+ // R3:两条**只警告、可声明**的接合判据(用户明确要求:可能是意料之内的设计 ⇒ 绝不 error)
166
+ ok('箭头插进图形 ⇒ 报 `arrow-tip-inside-shape`(warning,不是错误)',
167
+ v4.warns.some((w) => w.code === 'arrow-tip-inside-shape' && /ov_bad_in/.test(w.message)),
168
+ v4.warns.filter((w) => w.code === 'arrow-tip-inside-shape').map((w) => w.message.slice(0, 30)).join(' | ') || '无')
169
+ ok('端点离目标 ~6px ⇒ 报 `arrow-end-gap`',
170
+ v4.warns.some((w) => w.code === 'arrow-end-gap' && /ov_gap/.test(w.message)),
171
+ v4.warns.filter((w) => w.code === 'arrow-end-gap').map((w) => w.message.slice(0, 30)).join(' | ') || '无')
172
+ ok('**声明刻意如此**(roleReason)⇒ 同样的几何**不报**(用户要求:意料之内不提示)',
173
+ !v4.warns.some((w) => w.code === 'arrow-tip-inside-shape' && /ov_declared/.test(w.message)),
174
+ v4.warns.map((w) => w.code).join(',') || '无警告')
175
+
176
+ // X2:尖刺与"没接上"两条门禁(只警告、可声明)
177
+ // 注意:管线在 normalizePage 末尾已**兜底修掉**折返尖刺 ⇒ 正常路径上页里看不到它,
178
+ // 所以这里直接测**规则函数**(保证"护栏本身有效";页级想验证请临时关掉兜底规整)。
179
+ ok('折返尖刺 ⇒ 规则函数报 `line-spike`(护栏有效性;页级已被兜底规整修掉)',
180
+ checkLineRules([{ elementId: 'sp', elementType: 'line', points: [[300, 470], [400, 470], [350, 470]], arrow: false }])
181
+ .some((w) => w.code === 'line-spike'), '')
182
+ ok('规则函数对**声明版**尖刺不报(可声明豁免)',
183
+ !checkLineRules([{ elementId: 'sp2', elementType: 'line', points: [[300, 470], [400, 470], [350, 470]], arrow: false, roleReason: '刻意往返重叠' }])
184
+ .some((w) => w.code === 'line-spike'), '')
185
+ ok('**声明刻意往返重叠** ⇒ 不报 line-spike',
186
+ !v4.warns.some((w) => w.code === 'line-spike' && /ov_spike_declared/.test(w.message)), '')
187
+ ok('**无箭头**连线端点离盒子 ~6px ⇒ 报 `line-end-off-edge`(与 arrow-end-gap 分工)',
188
+ v4.warns.some((w) => w.code === 'line-end-off-edge' && /ov_noarrow_gap/.test(w.message)),
189
+ v4.warns.filter((w) => w.code === 'line-end-off-edge').map((w) => w.message.slice(0, 30)).join(' | ') || '无')
190
+
191
+ // ⑥ 手写路径能出成品:导出 parity 自证
192
+ const r = await exportPptx(ctx, { out: join(dir, 'hand.pptx') })
193
+ ok('手写路径能正常出成品(导出 parity.ok=true,含虚线折线与附着线)',
194
+ r.parity?.ok === true, `attached ${r.parity?.attachedLinesOut}/${r.parity?.attachedLinesExp}|poly ${r.parity?.polyLinesOut}/${r.parity?.polyLinesExp}`)
195
+
196
+ // ⑤ 单元级:规则函数本身(DSL 原文与 preview 快照两种字段名都要认)
197
+ ok('规则函数:DSL 原文(elementType)与 preview 快照(kind)两条路径都认得出折线',
198
+ checkLineRules([{ elementId: 'x', elementType: 'line', points: [[0, 0], [100, 0], [105, 0]], arrow: true }]).some((w) => w.code === 'arrow-end-short-run')
199
+ && checkLineRules([{ id: 'y', kind: 'line', points: [[0, 0], [100, 0], [105, 0]], arrow: true }]).some((w) => w.code === 'arrow-end-short-run'))
200
+
201
+ rmSync(dir, { recursive: true, force: true })
202
+ console.log(`\n==== verify-handwritten 结果:${pass} 通过 / ${fail} 失败 ====`)
203
+ if (fail) { console.log(`失败项:${failures.join(';')}`); process.exit(1) }
@@ -461,22 +461,37 @@ check('负对照(c) 图表部件缺失 ⇒ 计数判据必须红(chartsOut < c
461
461
 
462
462
  // ── 8. 回归:不含图表的工程产物**零变化** ────────────────────────────────────
463
463
  // 用户拍板的取舍里有一条:**其他绘图(text/shape/line/image/table/custGeom)保持不变**。
464
- // 判据最硬的一档 = 拿 HEAD 版本的导出器跑同一个(无图表)工程,产物**逐部件**比对(只有 core.xml 的时间戳除外)。
464
+ // 判据最硬的一档 = 拿**原生图表之前**的导出器跑同一个(无图表)工程,产物**逐部件**比对(只有 core.xml 的时间戳除外)。
465
465
  // 关系编号(媒体/讲稿/图表)是我这轮动过的地方,正需要这条挡住"加图表把 rId 排错"的回归。
466
- h('8. 回归:不含图表的工程产物逐部件零变化(对照 HEAD 版本导出器)')
466
+ // ⚠ 2026-09-26 Lead 修复:原实现取 `HEAD:src/pptd/*` 只复制 4 个文件——一旦 HEAD 变成"已含本轮改动"的提交,
467
+ // ① 对照失去意义(新 vs 新),② 复制出来的 svgCharts.js 会 import 未被复制的 ./schema.js ⇒ ERR_MODULE_NOT_FOUND 崩溃。
468
+ // 现在改成:从"原生图表之前的参照提交"取**整棵 src/**(相对 import 全部可解析),并有明确的回落链与跳过语义。
469
+ h('8. 回归:不含图表的工程产物逐部件零变化(对照"原生图表之前"的导出器)')
467
470
  {
468
471
  const oldDir = join(WORK, 'old-src')
469
- const need = ['pptd/export-pptx.js', 'pptd/layout.js', 'pptd/svgCharts.js', 'zips.js']
470
- let extracted = true
471
- for (const f of need) {
472
- const r = spawnSync('git', ['-C', root, 'show', `HEAD:src/${f}`], { maxBuffer: 20 * 1024 * 1024 })
473
- if (r.status !== 0) { extracted = false; break }
474
- mkdirSync(join(oldDir, dirname(f)), { recursive: true })
475
- writeFileSync(join(oldDir, f), r.stdout)
472
+ const refs = [process.env.PPT_OLD_EXPORT_REF, 'eb1e6f8', 'v1.1.2', 'HEAD~1'].filter(Boolean)
473
+ let ref = null
474
+ for (const cand of refs) {
475
+ const r = spawnSync('git', ['-C', root, 'cat-file', '-e', `${cand}^{commit}`], { encoding: 'utf8' })
476
+ if (r.status === 0) { ref = cand; break }
477
+ }
478
+ let extracted = false
479
+ if (ref !== null) {
480
+ const list = spawnSync('git', ['-C', root, 'ls-tree', '-r', '--name-only', ref, 'src'], { encoding: 'utf8', maxBuffer: 20 * 1024 * 1024 })
481
+ const files = String(list.stdout ?? '').trim().split('\n').filter(Boolean)
482
+ extracted = list.status === 0 && files.length > 0
483
+ for (const f of files) {
484
+ const r = spawnSync('git', ['-C', root, 'show', `${ref}:${f}`], { maxBuffer: 32 * 1024 * 1024 })
485
+ if (r.status !== 0) { extracted = false; break }
486
+ const rel = f.replace(/^src\//, '')
487
+ mkdirSync(join(oldDir, dirname(rel)), { recursive: true })
488
+ writeFileSync(join(oldDir, rel), r.stdout)
489
+ }
476
490
  }
477
491
  if (!extracted) {
478
- check('回归对照:HEAD 版本的 pptd 导出器可取得(无 git/无 HEAD ⇒ 跳过,不计 FAIL)', true, 'git show HEAD:src/pptd/export-pptx.js 失败')
492
+ check(`回归对照:参照提交的 src/ 可取得(尝试 ${refs.join(' → ')};无 git/无该提交 ⇒ 跳过,不计 FAIL)`, true, '未取得参照,跳过逐部件对照')
479
493
  } else {
494
+ check(`回归对照:参照版本 = ${ref}(整棵 src/ 已取出,相对 import 可解析)`, true, `文件数 = 已复制`)
480
495
  // 无图表的工程:文本 / 表格 / 形状 / 连线 / 图片 / 讲稿(覆盖媒体 rId + 讲稿 rId 两条编号路径)
481
496
  const noChart = join(WORK, 'no-chart')
482
497
  mkdirSync(join(noChart, 'pages'), { recursive: true })