dsh-ppt-studio 1.1.5 → 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 (92) hide show
  1. package/README.md +4 -1
  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 +149 -0
  5. package/docs/05-/350/277/255/344/273/243/346/265/201/347/250/213.md +23 -0
  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/scaffold.js +54 -1
  39. package/lib/tools.js +20 -2
  40. package/lib/verify.js +69 -1
  41. package/package.json +20 -3
  42. package/scripts/audit-manual-facts.mjs +15 -3
  43. package/scripts/bench-diagram-scale.mjs +165 -0
  44. package/scripts/check-generators.mjs +22 -0
  45. package/scripts/check-lib-sync.mjs +33 -0
  46. package/scripts/check-line-text.mjs +30 -0
  47. package/scripts/check-package.mjs +60 -0
  48. package/scripts/check-removed-traces.mjs +37 -0
  49. package/scripts/check-syntax.mjs +44 -0
  50. package/scripts/check-unknown-keys.mjs +114 -0
  51. package/scripts/fixtures/handdrawn-baseline.json +177 -0
  52. package/scripts/fixtures/prompt-cost-baseline.json +178 -0
  53. package/scripts/handdrawn-option-probe.mjs +78 -0
  54. package/scripts/probe-prompt-cost.mjs +127 -0
  55. package/scripts/red-probe.mjs +75 -0
  56. package/scripts/render-group.mjs +72 -0
  57. package/scripts/smoke.mjs +183 -12
  58. package/scripts/test-fast.mjs +108 -0
  59. package/scripts/test-map.mjs +162 -0
  60. package/scripts/test-release.mjs +64 -0
  61. package/scripts/verify-diagram-relations.mjs +405 -0
  62. package/scripts/verify-handdrawn.mjs +115 -0
  63. package/scripts/verify-handwritten.mjs +203 -0
  64. package/scripts/verify-primitives.mjs +714 -0
  65. package/scripts/verify-products.mjs +37 -0
  66. package/scripts/verify-shape.mjs +84 -0
  67. package/scripts/visual-pair-batch.mjs +77 -0
  68. package/skills/diagram-drawing/SKILL.md +23 -0
  69. package/skills/diagram-drawing/capability-matrix.md +22 -0
  70. package/skills/diagram-drawing/composition.md +22 -0
  71. package/skills/diagram-drawing/mechanisms.md +141 -0
  72. package/skills/diagram-drawing/pitfalls.md +89 -0
  73. package/skills/diagram-drawing/routing.md +17 -0
  74. package/skills/diagram-drawing/text-and-spacing.md +16 -0
  75. package/skills/diagram-drawing/verify.md +50 -0
  76. package/skills/diagrams/compare.md +24 -0
  77. package/skills/diagrams/flow.md +24 -0
  78. package/skills/diagrams/funnel.md +25 -0
  79. package/skills/diagrams/layers.md +18 -0
  80. package/skills/diagrams/loop.md +25 -0
  81. package/skills/diagrams/matrix.md +23 -0
  82. package/skills/diagrams/sequence.md +17 -0
  83. package/skills/diagrams/state.md +34 -0
  84. package/skills/diagrams/steps-ring.md +24 -0
  85. package/skills/diagrams/swimlane.md +23 -0
  86. package/skills/diagrams/timeline.md +24 -0
  87. package/skills/diagrams/tree.md +24 -0
  88. package/skills/ppt-studio-manual/SKILL.md +2 -2
  89. package/templates/academic-white/preview.png +0 -0
  90. package/templates/business-blue/preview.png +0 -0
  91. package/templates/pitch-bold/preview.png +0 -0
  92. 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) }