dsh-ppt-studio 1.1.5 → 1.2.1

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 (94) 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 +159 -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 +57 -0
  15. package/docs/15-1.2.0 /345/217/221/345/270/203/346/212/245/345/221/212.md" +46 -0
  16. 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
  17. package/examples/fx/fx-pro/preview/layout.json +12 -0
  18. package/examples/fx/fx-pro/preview/measured.json +1 -1
  19. package/examples/fx/fx-pro.pptx +0 -0
  20. package/examples/fx/seed.pptx +0 -0
  21. package/examples/fx/single.pptx +0 -0
  22. package/examples/fx/spliced.pptx +0 -0
  23. package/examples/smoke/imported/preview-rt/layout.json +3 -0
  24. package/examples/smoke/imported/source.pptx +0 -0
  25. package/examples/smoke/out-smoke.pptx +0 -0
  26. package/examples/smoke/preview/layout.json +3 -0
  27. package/lib/group-render.js +177 -0
  28. package/lib/msrender.js +27 -12
  29. package/lib/pptd/blocks.js +52 -0
  30. package/lib/pptd/export-pptx.js +266 -11
  31. package/lib/pptd/layout.js +54 -3
  32. package/lib/pptd/line-rules.js +259 -0
  33. package/lib/pptd/line-text-rule.js +90 -0
  34. package/lib/pptd/relations.js +615 -0
  35. package/lib/pptd/render-html.js +19 -1
  36. package/lib/pptd/route-geometry.js +120 -0
  37. package/lib/pptd/schema.js +146 -11
  38. package/lib/pptd/style-profile.js +133 -0
  39. package/lib/scaffold.js +54 -1
  40. package/lib/tools.js +20 -2
  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/handdrawn-option-probe.mjs +78 -0
  55. package/scripts/probe-prompt-cost.mjs +127 -0
  56. package/scripts/red-probe.mjs +75 -0
  57. package/scripts/render-group.mjs +72 -0
  58. package/scripts/smoke.mjs +191 -20
  59. package/scripts/test-fast.mjs +108 -0
  60. package/scripts/test-map.mjs +162 -0
  61. package/scripts/test-release.mjs +64 -0
  62. package/scripts/verify-diagram-relations.mjs +405 -0
  63. package/scripts/verify-handdrawn.mjs +115 -0
  64. package/scripts/verify-handwritten.mjs +203 -0
  65. package/scripts/verify-preset-scope.mjs +1 -1
  66. package/scripts/verify-primitives.mjs +714 -0
  67. package/scripts/verify-products.mjs +37 -0
  68. package/scripts/verify-shape.mjs +84 -0
  69. package/scripts/visual-pair-batch.mjs +77 -0
  70. package/skills/ppt-studio-diagram/SKILL.md +30 -0
  71. package/skills/ppt-studio-diagram/capability-matrix.md +22 -0
  72. package/skills/ppt-studio-diagram/composition.md +22 -0
  73. package/skills/ppt-studio-diagram/mechanisms.md +141 -0
  74. package/skills/ppt-studio-diagram/pitfalls.md +97 -0
  75. package/skills/ppt-studio-diagram/routing.md +17 -0
  76. package/skills/ppt-studio-diagram/text-and-spacing.md +16 -0
  77. package/skills/ppt-studio-diagram/verify.md +50 -0
  78. package/skills/ppt-studio-diagram-compare/SKILL.md +38 -0
  79. package/skills/ppt-studio-diagram-flow/SKILL.md +38 -0
  80. package/skills/ppt-studio-diagram-funnel/SKILL.md +39 -0
  81. package/skills/ppt-studio-diagram-layers/SKILL.md +32 -0
  82. package/skills/ppt-studio-diagram-loop/SKILL.md +39 -0
  83. package/skills/ppt-studio-diagram-matrix/SKILL.md +37 -0
  84. package/skills/ppt-studio-diagram-sequence/SKILL.md +31 -0
  85. package/skills/ppt-studio-diagram-state/SKILL.md +48 -0
  86. package/skills/ppt-studio-diagram-steps-ring/SKILL.md +38 -0
  87. package/skills/ppt-studio-diagram-swimlane/SKILL.md +37 -0
  88. package/skills/ppt-studio-diagram-timeline/SKILL.md +38 -0
  89. package/skills/ppt-studio-diagram-tree/SKILL.md +38 -0
  90. package/skills/ppt-studio-manual/SKILL.md +2 -2
  91. package/templates/academic-white/preview.png +0 -0
  92. package/templates/business-blue/preview.png +0 -0
  93. package/templates/pitch-bold/preview.png +0 -0
  94. package/templates/tech-dark/preview.png +0 -0
@@ -0,0 +1,37 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * 产物一致性(**本机专用**:需要 PowerPoint COM 渲染,故不进 npm test)。
4
+ * 流程:手画生成器 → 导出到**非锁定名** pptx → COM 渲染 13 张命名 PNG → 与基线比对
5
+ * · 硬不变量:layout 摘要(走 verify-handdrawn ✓)
6
+ * · 次要不变量:13 张 PNG 的 sha256(差异只报不改 ✓)
7
+ * 用法:npm run verify:products
8
+ */
9
+ import { execFileSync } from 'node:child_process'
10
+ import { createHash } from 'node:crypto'
11
+ import { readFileSync, existsSync } from 'node:fs'
12
+ import { join } from 'node:path'
13
+
14
+ const ROOT = new URL('..', import.meta.url).pathname.replace(/^\/([A-Za-z]:)/, '$1')
15
+ const OUT = join(ROOT, '..', 'ppt-deliverable', '手画12+1')
16
+ const sha = (p) => createHash('sha256').update(readFileSync(p)).digest('hex')
17
+ const run = (cmd) => execFileSync('node', cmd, { cwd: ROOT, stdio: 'inherit' })
18
+
19
+ console.log('… 1/3 重出手画页面与产物(导出到非锁定名,避免 PowerPoint 占用)')
20
+ run(['examples/hand-drawn/gen/gen13-attach.cjs'])
21
+ run(['examples/hand-drawn/gen/gen-1113.cjs'])
22
+
23
+ console.log('… 2/3 硬不变量:layout 摘要与基线')
24
+ const vh = execFileSync('node', ['scripts/verify-handdrawn.mjs'], { cwd: ROOT, encoding: 'utf8' })
25
+ process.stdout.write(vh.split('\n').slice(-3).join('\n') + '\n')
26
+ if (!/与基线一致/.test(vh)) { console.log('✗ 摘要与基线不一致'); process.exit(1) }
27
+
28
+ console.log('… 3/3 次要不变量:13 张 PNG sha')
29
+ const base = JSON.parse(readFileSync(join(ROOT, 'scripts', 'fixtures', 'handdrawn-baseline.json'), 'utf8'))
30
+ let same = 0, diff = 0, miss = 0
31
+ for (const [name, h] of Object.entries(base.pngs ?? {})) {
32
+ const f = join(OUT, name)
33
+ if (!existsSync(f)) { miss++; continue }
34
+ if (sha(f) === h) same++; else { diff++; console.log(' ⚠ 像素差异:' + name) }
35
+ }
36
+ console.log(' 相同 ' + same + '|差异 ' + diff + '|缺失 ' + miss + (miss ? '(交付目录不在本机 ⇒ 跳过像素比对)' : diff ? '(像素差异属环境/渲染差异,硬不变量已过 ✓)' : ' ✓'))
37
+ console.log('==== 产物一致性:通过 ====')
@@ -0,0 +1,84 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * 折线形态自证(X3):`node scripts/verify-shape.mjs`
4
+ *
5
+ * 为什么单独成脚本:`verify:route` 的断言针对"密集夹具的交叉",而这里要守的是**形态底线**
6
+ * (折返尖刺、端点悬在图形内部)——跨**所有**交付面跑,任何一批新内容都能被它兜住。
7
+ *
8
+ * 三条断言:
9
+ * ① **无折返尖刺**:连续三点共线且中间点折返(用户实测图 10 顶端那根多余线就是这个);
10
+ * ② **无端点悬空于图形内部**:端点落在某个图形**内部**(离轮廓 >2px)⇒ 看起来"戳进盒子";
11
+ * ③ 检查覆盖 12 族夹具 + 两个从零批次(缺目录时跳过并说明,不假装通过)。
12
+ */
13
+ import { readFileSync, readdirSync, existsSync } from 'node:fs'
14
+ import { join } from 'node:path'
15
+ import { tmpdir } from 'node:os'
16
+
17
+ let pass = 0
18
+ let fail = 0
19
+ const failures = []
20
+ const ok = (name, cond, detail = '') => {
21
+ if (cond) { pass++; console.log(`✓ ${name}${detail ? ` — ${detail}` : ''}`) }
22
+ else { fail++; failures.push(name); console.log(`✗ ${name}${detail ? ` — ${detail}` : ''}`) }
23
+ }
24
+
25
+ const spikesOf = (pts) => {
26
+ let n = 0
27
+ for (let i = 1; i + 1 < pts.length; i++) {
28
+ const a = pts[i - 1]; const b = pts[i]; const c = pts[i + 1]
29
+ const sameX = Math.abs(a[0] - b[0]) < 0.5 && Math.abs(b[0] - c[0]) < 0.5
30
+ const sameY = Math.abs(a[1] - b[1]) < 0.5 && Math.abs(b[1] - c[1]) < 0.5
31
+ if ((sameX && (b[1] - a[1]) * (c[1] - b[1]) < 0) || (sameY && (b[0] - a[0]) * (c[0] - b[0]) < 0)) n++
32
+ }
33
+ return n
34
+ }
35
+ const rectOf = (b) => Array.isArray(b) ? { x: b[0], y: b[1], w: b[2], h: b[3] } : b
36
+ /** 端点是否"悬在某个图形内部"(离轮廓 >2px) */
37
+ const insideAny = (pt, boxes) => {
38
+ for (const b of boxes) {
39
+ if (b.role === 'decoration') continue
40
+ if (pt[0] > b.r.x + 2 && pt[0] < b.r.x + b.r.w - 2 && pt[1] > b.r.y + 2 && pt[1] < b.r.y + b.r.h - 2) return b.id
41
+ }
42
+ return null
43
+ }
44
+
45
+ /** 待检面:directory → 名称(缺目录则跳过) */
46
+ const latest = readdirSync(tmpdir()).filter((d) => d.startsWith('pptd-families-')).sort().pop()
47
+ const targets = [
48
+ ['既有 12 族夹具', latest ? join(tmpdir(), latest) : null],
49
+ ['从零批次 fresh-sea(当前交付批次)', 'examples/archive/fresh-sea-family'],
50
+ ]
51
+
52
+ for (const [label, dir] of targets) {
53
+ if (!dir || !existsSync(join(dir, 'preview', 'layout.json'))) {
54
+ console.log(`… 跳过 ${label}(尚无 preview/layout.json;先跑对应渲染)`)
55
+ continue
56
+ }
57
+ const layout = JSON.parse(readFileSync(join(dir, 'preview', 'layout.json'), 'utf8'))
58
+ let spike = 0
59
+ let floating = 0
60
+ const spikeAt = []
61
+ const floatAt = []
62
+ layout.pages.forEach((page, pi) => {
63
+ // 只把**内容图形**当"盒子":分组容器(声明了 contains 的板)与 role:decoration 都是结构件,
64
+ // 连线端点落在里面是正常画法(第一版没排除容器 ⇒ 误报 5~7 处,实测修正)。
65
+ const boxes = (page.elements ?? [])
66
+ .filter((e) => e.kind === 'shape' && e.bounds && e.role !== 'decoration' && !(e.contains ?? []).length)
67
+ .map((e) => ({ id: e.id, r: rectOf(e.bounds), role: e.role }))
68
+ for (const e of (page.elements ?? [])) {
69
+ if (e.kind !== 'line' || !Array.isArray(e.points)) continue
70
+ const sp = spikesOf(e.points)
71
+ if (sp) { spike += sp; spikeAt.push(`p${pi + 1}:${e.id}`) }
72
+ if (e.role === 'decoration' || e.roleReason) continue
73
+ for (const pt of [e.points[0], e.points[e.points.length - 1]]) {
74
+ const hit = insideAny(pt, boxes)
75
+ if (hit) { floating++; floatAt.push(`p${pi + 1}:${e.id}→${hit}`) }
76
+ }
77
+ }
78
+ })
79
+ ok(`${label}:**无折返尖刺**`, spike === 0, spike ? `${spike} 处(${spikeAt.slice(0, 4).join(', ')})` : `${layout.pages.length} 页`)
80
+ ok(`${label}:**无端点悬在图形内部**`, floating === 0, floating ? `${floating} 处(${floatAt.slice(0, 4).join(', ')})` : '全部端点贴边或落在图形外')
81
+ }
82
+
83
+ console.log(`\n==== verify-shape 结果:${pass} 通过 / ${fail} 失败 ====`)
84
+ if (fail) { console.log(`失败项:${failures.join(';')}`); process.exit(1) }
@@ -0,0 +1,77 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * 并排对照工具(批量版,等价于被回滚删除的 visual-pair.mjs)—— S4 交付用
4
+ *
5
+ * 用法:
6
+ * node scripts/visual-pair-batch.mjs --old <旧图目录> --new <新图目录> --out <输出目录> [--width 1400]
7
+ *
8
+ * 配对规则:按文件名开头的两位页号(01..13)配对。
9
+ * 输出:<输出目录>/对比-NN.png —— 上=旧(红字),下=新(绿字),同宽同缩放。
10
+ *
11
+ * 纪律(写进工具,而非只在口头):
12
+ * · 先图后数:结论前必须先看渲染图;坐标与门禁数字只作辅助。
13
+ * · 改了引擎必须重出产物:对照图里的"新"必须来自重出后的 PNG。
14
+ * · 对照图要能一眼判对:同内容、同缩放、上下并排、标注来源。
15
+ */
16
+ import { mkdirSync, readdirSync, writeFileSync, existsSync } from 'node:fs'
17
+ import { join, resolve } from 'node:path'
18
+ import { spawnSync } from 'node:child_process'
19
+ import { tmpdir } from 'node:os'
20
+
21
+ const argv = process.argv.slice(2)
22
+ const arg = (n, d = null) => { const i = argv.indexOf(`--${n}`); return i >= 0 && argv[i + 1] ? argv[i + 1] : d }
23
+ const oldDir = arg('old')
24
+ const newDir = arg('new')
25
+ const outDir = arg('out')
26
+ const width = Number(arg('width', '1400'))
27
+ if (!oldDir || !newDir || !outDir) {
28
+ console.log('用法:node scripts/visual-pair-batch.mjs --old <旧图目录> --new <新图目录> --out <输出目录>')
29
+ process.exit(2)
30
+ }
31
+ const py = process.env.PPTD_PYTHON ?? 'C:\\Users\\11867\\.dsh\\dsh-runtimes\\dsh-primary-runtime\\dependencies\\python\\python.exe'
32
+ if (!existsSync(py)) { console.log('✗ 找不到 python:' + py); process.exit(1) }
33
+
34
+ const index = (dir) => {
35
+ const m = new Map()
36
+ for (const f of readdirSync(dir)) {
37
+ const mm = /^(\d{2})/.exec(f)
38
+ if (mm && /\.png$/i.test(f) && !m.has(mm[1])) m.set(mm[1], join(dir, f))
39
+ }
40
+ return m
41
+ }
42
+ const A = index(oldDir)
43
+ const B = index(newDir)
44
+ const pairs = [...A.keys()].filter((k) => B.has(k)).sort()
45
+ if (!pairs.length) { console.log('✗ 没有可配对的页'); process.exit(1) }
46
+ mkdirSync(outDir, { recursive: true })
47
+ const listFile = join(tmpdir(), 'visual-pair-list.txt')
48
+ writeFileSync(listFile, pairs.map((k) => `${k}\t${A.get(k)}\t${B.get(k)}`).join('\n'), 'utf8')
49
+
50
+ const pySrc = [
51
+ 'import sys',
52
+ 'from PIL import Image, ImageDraw',
53
+ "rows = [l.rstrip().split(chr(9)) for l in open(sys.argv[1], encoding='utf-8') if l.strip()]",
54
+ 'out_dir = sys.argv[2]',
55
+ 'W = int(sys.argv[3])',
56
+ 'for code, oldf, newf in rows:',
57
+ ' a = Image.open(oldf).convert("RGB")',
58
+ ' b = Image.open(newf).convert("RGB")',
59
+ ' a = a.resize((W, max(1, int(a.height * W / a.width))))',
60
+ ' b = b.resize((W, max(1, int(b.height * W / b.width))))',
61
+ ' bar = 26',
62
+ ' im = Image.new("RGB", (W, a.height + b.height + bar * 2), "white")',
63
+ ' d = ImageDraw.Draw(im)',
64
+ ' d.text((8, 7), "OLD " + oldf.replace(chr(92), "/").split("/")[-1], fill=(180, 0, 0))',
65
+ ' im.paste(a, (0, bar))',
66
+ ' d.text((8, bar + a.height + 7), "NEW " + newf.replace(chr(92), "/").split("/")[-1], fill=(0, 110, 0))',
67
+ ' im.paste(b, (0, bar * 2 + a.height))',
68
+ ' im.save(out_dir + "/对比-" + code + ".png")',
69
+ ' print("对比-" + code + ".png")',
70
+ ].join('\n')
71
+ const pyFile = join(tmpdir(), 'visual-pair-batch.py')
72
+ writeFileSync(pyFile, pySrc, 'utf8')
73
+ const r = spawnSync(py, [pyFile, listFile, resolve(outDir), String(width)], { encoding: 'utf8' })
74
+ process.stdout.write(r.stdout ?? '')
75
+ if (r.stderr) process.stderr.write(r.stderr)
76
+ console.log(`共 ${pairs.length} 张对照图 → ${outDir}`)
77
+ process.exit(r.status ?? 1)
@@ -0,0 +1,30 @@
1
+ ---
2
+ name: ppt-studio-diagram
3
+ description: 复杂图绘制总纲:何时调用哪个机制(attach/正交化/结构声明/门禁 code 对照)、12 类图的配方索引、验证纪律与坑表。
4
+ whenToUse: 要画任何非模板化的复杂图(流程/泳道/层级/时序/状态机/漏斗/环/矩阵/时间轴/对比/分层)或手画遇阻、门禁报错时加载。
5
+ ---
6
+ # 复杂图绘制总纲(手画 + 按需查阅子 skill)
7
+
8
+ > **优先级**:用户指令 > 本手册经验值 > 工具默认。本手册给的是启发式与反例,**不定义任何门禁数值**;
9
+
10
+ ## 铁律(每条都有代价换来的)
11
+ 1. **先图后数**:任何"画好了/修好了"的结论,必须**先渲染并亲眼看图**才能下;坐标与门禁数字只作辅助。
12
+ 2. **改了引擎/改了页 ⇒ 必须重出产物**(PNG/pptx)并替换到位;只改代码不重出 = 未完成。
13
+ 3. **出图必过门禁**:`node scripts/verify-*.mjs` 与 `npm test`;有错先定位再改,绝不改门禁迁就图。
14
+ 4. **占位必预留**:标题带 / 标签带 / 徽标栏 / 导轨栏,**先算好空间再摆元素**;后摆的东西不许压已有的。
15
+ 5. **连线端点必须落在元素边上**:线的端点若不与任何元素边重合,引擎会**吸附**到附近元素 ⇒ 出现莫名长线。
16
+ 6. **能力边界如实标注**:引擎不支持的东西(阴影/高光/手绘笔触)不要假装;要么不用,要么标"近似/不支持"。
17
+ 7. **每条边都必须有标签**:图上任何转移/连线都要能读出"从哪到哪、什么条件";缺标签 = **内容缺失**。门禁抓不到(实测主链 5 条标签全丢,门禁仍 0 错)——只能靠看图与旧版并排发现。
18
+ 8. **有意重叠要声明**:`expectedOverlaps` 只用于**真有意**的穿入(如回路进分组容器),并写明理由;绝不用来掩盖缺陷。
19
+
20
+ ## 固定流程
21
+ 1. 读子 skill 拿**配方数值**(一族一份:骨架 → 流向 → 尺寸/间距 → 该族坑 → 检查清单)。
22
+ 2. 生成 `elements:`(显式坐标)→ `resolveDeck` → `renderDeck`(出 HTML+layout.json)。
23
+ 3. `verifyDeck` 逐页过门禁 → 有错就改(对照 pitfalls)。
24
+ 4. `exportPptx` → **真渲染 PNG**(COM;不可用则 Edge 无头截预览 HTML)。
25
+ 5. **自己读图**:看层级/留白/避让/连线语义(谁指向谁)→ 不满意就地改 → 回到 2。
26
+ 6. 交付:PNG + pptx + 一句"这页看什么"的说明。
27
+
28
+ ## 两条看图通路
29
+ - COM:`lib/msrender.js` 的 `renderPptxToPng(pptx, outDir, { pages: [...] })`(导出物必须能被 PowerPoint 打开)。
30
+ - 备用:Edge 无头截预览 HTML —— `msedge --headless=new --window-size=W,H --screenshot=out.png file:///...html`(视口要按画布比例设,否则画面会挤在角落)。
@@ -0,0 +1,22 @@
1
+ # 能力边界(capability-matrix)
2
+
3
+ > 交付时必须如实标注:**不支持的要么不用,要么标"近似"**;能让它在写入时就**报错**最好。
4
+
5
+ ## 支持(预览与 OOXML 两端同源)
6
+ | 能力 | 说明 |
7
+ |---|---|
8
+ | 纯色 / **线性渐变**填充 | fill 对象形如 {type:'gradient', stops:[{pos,color}], angle};预览转 CSS linear-gradient、导出转 a:gradFill |
9
+ | 形状图元 | rect / roundRect / ellipse / triangle / parallelogram / diamond / pentagon / hexagon / chevron / octagon / star5 |
10
+ | **custGeom 任意路径** | path.w/h + moveTo/lnTo/quadBezTo/cubicBezTo/arcTo/close ⇒ 梯形等自定义几何 |
11
+ | 折线 / 虚线 / 两端箭头 | line.points 支持 N 点;dash: dash;arrow: true|false|'end'|'both' |
12
+ | 分组与包含 | contains 声明"我包住谁";跨容器穿入用 expectedOverlaps 声明 |
13
+
14
+ ## 不支持(如实标注,勿假装)
15
+ | 能力 | 现状 |
16
+ |---|---|
17
+ | **逐形状阴影 / 卡片高光** | 导出层只有主题级 effectStyleLst,无逐形状 a:outerShdw;预览端无 box-shadow ⇒ 只能近似为"纯色/渐变 + 描边" |
18
+ | 手绘笔触质感 | 无 ⇒ 明确标"不支持真笔触" |
19
+ | 3 段以上复杂渐变 | 能写 stops 但视觉收益有限,建议 2 段 |
20
+
21
+ ## 规矩
22
+ 写"引擎画不出来的值"应当**直接报错**(例如 elevation 只允许 none),绝不静默无效。
@@ -0,0 +1,22 @@
1
+ # 构图技法(composition)
2
+
3
+ ## 标题体系
4
+ - 大标题 25pt bold(深色)@ (60,34),宽 ≤720。
5
+ - 分栏/分层标签用 ①②③ + 14pt bold 主色,放在各栏上方一行。
6
+
7
+ ## 容器与分组
8
+ - 需要"这几位是一组"时,用**软填充容器**(浅紫 #EDE9FE 一类)包住整组,而不是只画个线框。
9
+ - 容器**必须把内部所有元素写进 contains**;连线穿容器要按纪律精确声明。
10
+
11
+ ## 形状对比与色调层级
12
+ - 端点/决策用 **parallelogram / diamond**,过程用 rect/roundRect,数量递减用 **custGeom 梯形**。
13
+ - 层级靠三件事:**字号级差**(25/15/13/11)、**色彩重量**(主色 vs accent vs 浅底)、**形状差异**。
14
+
15
+ ## 留白
16
+ - 内容占中间带(约 y 130..470),四边留 ≥40px;宁少勿挤。
17
+ - 一页只允许**一处** accent 高亮(多了等于没有重点)。
18
+
19
+ ## 参考稿驱动
20
+ 拿到参考图时:先量它的结构(标题位置与字号、分几栏/几层、容器与形状、回路走哪、留白比例),再用同一结构画我们的内容 —— **不要凭感觉仿"风格"**。
21
+
22
+
@@ -0,0 +1,141 @@
1
+ # 机制调度手册(何时调用哪个机制)
2
+
3
+ > **动笔前先读权威规则**:`ppt_schema` 工具(源码 `src/scaffold.js` 的 `SCHEMA_REF`)里写着引擎的全部词汇与硬规则。
4
+ > 本文件是"**什么时候该用哪一条**"的索引;两者冲突时**以 SCHEMA_REF 为准**。
5
+
6
+ ## 0. 三条总纲(血泪换来的)
7
+
8
+ 1. **连边一律优先 `attach`,绝不手算像素。**
9
+ 实测教训:某页我手写"卡片右沿 x=628",引擎 `attach` 解析后是 **608** —— 我错了 20px 而不自知。
10
+ `attach: { from: { ref: <id>, side: top|right|bottom|left }, to: { … } }` ⇒ 引擎把两端锚到**真实边缘**,
11
+ 并把"你写的坐标被改了多少"记成 `attach-override` 警告(**不是报错,是透明提示**)。
12
+ 2. **错与警都要逐条读;警告不解释不许收工。**
13
+ 实测教训:我连续几轮"门禁 0 错"就收工,结果用户反馈的问题(竖线穿字、折点贴箭头、箭头没接上)
14
+ **全都在警告里早就报了**:`line-crossing` / `arrow-end-short-run` / `arrow-end-gap`。
15
+ 0 错 ≠ 没问题;**0 错只说明"没有不可声明级缺陷"**。
16
+ 3. **刻意设计要"声明",不要靠"绕过"。**
17
+ 环/放射/漏斗的斜段、汇流交叉、装饰底板,都该 `role: decoration` 或 `roleReason: "…"` 明说;
18
+ **不要**用 `expectedOverlaps` 去盖"压字/压盒"(那是掩盖真实缺陷)。
19
+
20
+ ## 1. 连接线与箭头
21
+
22
+ | 想要的效果 | 用什么 | 备注 |
23
+ |---|---|---|
24
+ | 端点贴到某个元素的边 | **`attach`** | 首选;斜边形状(parallelogram/triangle/diamond)也接到**真实斜边** |
25
+ | 折线只用横/竖段 | 交给引擎(默认自动正交化) | 显式 `elements` 的折线**已自动**过 `orthogonalize` |
26
+ | 箭头前留足直段(≥18px) | 交给引擎(默认自动修) | **已自动生效**;修不了(会造折返)就原样保留并由 `arrow-end-short-run` 报出 |
27
+ | 刻意保留斜段(环形/放射/漏斗) | `role: decoration` 或 `roleReason` | 引擎的自动正交化对"斜段折线"自动跳过,但**警告仍会报** ⇒ 要声明 |
28
+ | 端点刻意留空(2–10px 缝) | `roleReason: "…"` | 否则报 `line-end-off-edge`/`arrow-end-gap` |
29
+
30
+ ## 2. 门禁 code 对照表(看到 → 怎么做)
31
+
32
+ | code | 级别 | 含义 | 处置 |
33
+ |---|---|---|---|
34
+ | `line-crossing` | 警示 | 两条线交叉(+型) | 换车道/换进出边;刻意汇流 ⇒ `roleReason` |
35
+ | `line-end-off-edge` | 警示 | 端点离元素 2–10px 没接上 | **改 `attach`**(首选)或声明刻意留空 |
36
+ | `arrow-end-gap` | 警示 | 带箭头端点没接到目标 | 同上 |
37
+ | `arrow-end-short-run` | 警示 | 箭头前直段 <18px | 挪拐点;**`attach` 通常直接消灭它** |
38
+ | `arrow-tip-inside-shape` | 警示 | 箭头插进图形内部 | 改 `attach` 到边缘;刻意插入 ⇒ `roleReason` |
39
+ | `line-diagonal-segment` | 警示 | 折线有斜段 | 连接线改轴对齐;刻意(环/放射)⇒ `role: decoration` |
40
+ | `attach-override` | 警示 | 你的手写坐标与 attach 解析不一致 | **正常**;想清零就把 points 改成解析后的真实值 |
41
+ | `hotspot` / `density` | 建议 | 密集区/信息密度提示 | 信息性,可保留;但要**明说是建议级** |
42
+ | `content-collision` | **错** | 文字互压 | **不可声明豁免**,必须改布局 |
43
+ | `unexpected-overlap` | **错** | 设计外重叠 | 改布局,或按层级正确声明 |
44
+
45
+ ## 3. 结构声明的正确姿势
46
+
47
+ - 容器(象限框、泳道带、软底板)**必须把内部元素写进 `contains`**;无内容的形状**省略** `contains`(不写空数组)。
48
+ - **声明闭包**:嵌套承载只需声明**相邻层**(card×inBox、inBox×inText),隔层组合由传递自动通过。
49
+ - 语义性底板/背景带优先 `role: decoration`(豁免重叠),不要用 `expectedOverlaps` 盖。
50
+ - 跨容器连线(泳道跨道)**必然穿带** ⇒ 只把**真正相交**的元素对写进 `expectedOverlaps`;过度声明会产生 `declared-stale` 噪音。
51
+
52
+ ## 4. 渲染与交付通路(别在这栽跟头)
53
+
54
+ 1. **渲染前确认没有进程占用目标 pptx**(尤其 PowerPoint 开着)——否则 COM 会**静默渲染内存里的旧副本**,
55
+ 导出还会 EBUSY 失败。对策:导出到**带时间戳的新文件名**。
56
+ 2. **重出前先清空输出目录**,否则旧 PNG 残留会被当成新图复制。
57
+ 3. **读图通路按路径缓存** ⇒ 大改后的页面要**复制到新文件名再读**。
58
+ 4. 复跑 `smoke.mjs` 前先 `Remove-Item examples\smoke\.tmp-home -Recurse -Force`(它非幂等,残留即崩)。
59
+ 5. **"崩"与"断言红"要分清**:崩多为环境/残留,红才是真回归。
60
+ 6. **改了引擎必须重出产物**;只改源码不重出 = 用户看到的还是旧的。
61
+
62
+ ## 5. 一页图的固定动作单
63
+
64
+ 1. 读 `SCHEMA_REF`(机制词汇 + 本节对照表);
65
+ 2. 写 `elements`:**连边用 `attach`**、容器写 `contains`、刻意设计写 `role/roleReason`;
66
+ 3. `resolveDeck` → `renderDeck` → `verifyDeck`:**错清零,且逐条解释每一条警**;
67
+ 4. 导出 → **真渲染 PNG** → **自己读图**(层级/留白/避让/语义)→ 不满意回第 2 步;
68
+ 5. 与旧版/参考稿**并排对照**,记录"更好/持平/变差"及依据。
69
+
70
+ ## 6. 各子 skill 的专属机制速查
71
+
72
+ | 子 skill | 必用机制 / 专属要点 |
73
+ |---|---|
74
+ | tree | 父→子折线用 `attach(bottom→top)`;同组子树加**分组容器**(contains 相邻层) |
75
+ | matrix | 象限框为容器,**格内卡片写进其 contains**;高亮**唯一** |
76
+ | timeline | 刻度线 `attach` 到标签/主轴端点(避免 4px 缝);里程碑**只高亮一个** |
77
+ | swimlane | 跨道箭头用 `attach` 两端 + 只声明**真正相交**的泳道带;带 contains 本道任务 |
78
+ | compare | 面板 contains 要点卡;中间标记**宽度 ≤ 间隙**(36px 落进 40px) |
79
+ | loop | 环形弦 = 刻意斜段 ⇒ `role: decoration` + `roleReason: 环形语义`;端点用 `attach` |
80
+ | funnel | 用 **custGeom 梯形**(矩形堆叠是台阶不是漏斗);段间边界相接不算重叠 |
81
+ | steps-ring | 真圆用 `ellipse`(浅填充+主色描边)+ 编号小圆点;点与标签留 ≥24px |
82
+ | sequence | 生命线 `attach` 到参与者卡底边;消息标签加**白底芯片**;汇流交叉写 `roleReason` |
83
+ | state | 转移标签贴箭头上方 **4px**,卡距≥标签宽(70px);返工回边走**外圈车道** |
84
+ | flow | 起止 `roundRect`、末步 accent;异常回边标签放**车道上方**独立一行 |
85
+ | layers | 层带 contains 层内卡;层间箭头用 `attach`(left/right 边) |
86
+
87
+ ## 7. 契约:2 点直连线 ≠ 折线(2026-09 实测确认,**别再踩**)
88
+
89
+ | 形态 | 导出 | 规则 |
90
+ |---|---|---|
91
+ | **2 点直连线**(含斜向!) | `<p:cxnSp>`(带 `flipH`/`flipV`) | **受支持的连接符** ✓ —— **绝对不要正交化它** ✗ |
92
+ | **n≥3 折线** | 一帧 `p:sp` + `custGeom` 开放路径 | **保证轴对齐** ✓(斜段会被自动拆成横/竖两段) |
93
+
94
+ **实测事故**:我曾把"折线永远轴对齐"套到 2 点斜连线上 ⇒ 它们被拆成 custGeom ✗ ⇒
95
+ `cxnSp` 数归零 ⇒ smoke 的 **6 条连线方向专项断言全崩**(`✗ 连线:全部 6 条 cxnSp 已导出` +
96
+ `TypeError … reading 'flipH'`)✓。**这是代码真错,不是 fixture** ✗。
97
+
98
+ **实现现状**(`src/pptd/route-geometry.js` 的 `routePolyline`):
99
+ ```js
100
+ if (raw.length < 3) return raw // 2 点直连线:原样(cxnSp 契约)
101
+ const fixed = orthogonalize(arrowEndRuns(orthogonalize(raw), arrow))
102
+ return hasFoldback(fixed) ? raw : fixed // 折返守卫:不造尖刺,交门禁报
103
+ ```
104
+ **豁免声明**(`layout.js`,只看显式声明):`orthogonal: false` / `role: decoration` / `roleReason: '…'`。
105
+
106
+ **推论**:环形/放射图若用 **2 点弦**(如 06/08)⇒ **本来就不会被正交化** ✓,
107
+ 此时写 `role: decoration` 只为**消掉 `line-diagonal-segment` 警告** ✓(属警告清理,不是功能必需)。
108
+
109
+ ## 8. 泛用性不降级(G3):手画是**一等公民**,不是退路
110
+
111
+ **引擎的既定教义**(`SCHEMA_REF` 第 6 条原文):"一页可以**既用 diagram 又手写元素**(缝合页);
112
+ assistant 想要家族库没有的图,就按上面几条手写 elements 拼出来——**这是被支持的路径,不是退路**。"
113
+
114
+
115
+ **机制层面手画与族已等价**(G1 完成后):
116
+ | 能力 | 族 | 手画 |
117
+ |---|---|---|
118
+ | 正交化 + 箭头端 ≥18px + 折返守卫 | ✅ | ✅ **同一份实现**(`route-geometry.js`) |
119
+ | `attach` 锚边(含**斜边真实轮廓**) | ✅ | ✅ 同一实现 |
120
+ | 线×文字 / 线×线 / 贴边 / 尖端入形 / 斜段 等检查 | ✅ | ✅ 同一门禁 |
121
+ | 结构声明(`contains`/`expectedOverlaps`/`role`) | ✅ | ✅ 同一套 |
122
+ | **自由构图**(族没有的图) | ❌ 受族模板限 | ✅ **不受限**(图 13 即样例) |
123
+
124
+ ⇒ 泛用性**严格更强** ✓:族覆盖范围内按 skill 配方调用机制 ✓;范围外退化为手画但**机制不减** ✓。
125
+
126
+
127
+
128
+ ### 补充 code(§2 对照表增补)
129
+
130
+ | code | 严重级 | 含义 | 怎么修 |
131
+ |---|---|---|---|
132
+ | `unknown-key` | **warning(不阻断)** | 页面级/元素级出现**未知键**(拼写错误或残留字段)——过去会被静默忽略,可能悄悄出空页/漏项 | 改对键名;若是合法新字段,说明白名单漏项(`scripts/check-unknown-keys.mjs` 会在本仓示例上**直接报失败**逼你补) |
133
+
134
+
135
+ ## 门禁与 skill 的分工(发版前确立的原则)
136
+
137
+ - **skill 管「做法选择」**(调不调、调哪个、用哪种画法)—— **由模型自由决定**;
138
+ - **门禁只锁两类东西**:① **事实**(几何/文本的客观缺陷,如箭头前直段 <18px、线穿文字);② **声明自洽**(你声明了 A 与 B 的关系就必须自洽,如 `contains` / `attach` / `badgeOf` / `roleReason`);
139
+ - **风格、审美、表达取向一律不进 error**(要不要分组容器、环用卡片还是真圆 ⇒ 属 skill 建议与参考对照);
140
+ - **error 只能来自事实或声明自相矛盾;启发式只能出 warning**;
141
+ - 每条警告都必须有**处置结论**(见 `docs/17`);**不许用声明掩盖真缺陷**。
@@ -0,0 +1,97 @@
1
+ # 踩坑索引(症状 → 根因 → 修法)
2
+
3
+
4
+ ## A. 校验与结构声明
5
+
6
+ | # | 症状(门禁/报错原话) | 根因 | 修法 |
7
+ |---|---|---|---|
8
+ | A1 | `contains: non-empty array of element ids` | 给**没有内容**的形状写了 `contains: []` | 无内容就**整个省略** `contains` |
9
+ | A2 | `contains 声明不成立:父盒未真正包含子元素(pad=0)` | 子元素实际**不在**父盒矩形内(常见于"父盒加高/挪动,子元素没跟着动") | 改父盒尺寸时**同步重排内部**(三处一起改:盒 / 标题 / 副标题) |
10
+ | A3 | `"xxx" 不是本页元素 id(防呆)` | `contains` 里引用了**不存在**的元素(或漏发该元素) | 先发元素再引用;或删掉该 id |
11
+ | A4 | `unexpected-overlap:容器 × 内部卡片` | 容器**没登记**住在里面的元素(象限容器→风险卡、泳道带→本道任务) | 容器的 `contains` 列出**全部**内部元素 id |
12
+ | A5 | `relation-invalid:容器 × 芯片内文字` | 把**芯片里的文字**当成了外层带的子元素(它属于芯片) | 文字只登记在**直接父级**的 `contains` 里 |
13
+ | A6 | `declared-stale:预期重叠声明已失效` | `expectedOverlaps` **过度声明**(声明了一对实际不相交的元素) | 只声明**真正相交**的对;删掉多余的 |
14
+
15
+ ## B. 连线与几何
16
+
17
+ | # | 症状 | 根因 | 修法 |
18
+ |---|---|---|---|
19
+ | B1 | 页面上出现**莫名长横线**,或线端点"吸附"到别的元素上 | 线的端点**不在任何元素边上** ⇒ 引擎自动吸附 | 端点**精确落在元素边**(x = 卡左边/右边,y = 卡心) |
20
+ | B2 | `unexpected-overlap:卡 × 线`(交叠十几像素) | 线的起点画在卡**内部**(如 `A.x + 6`) | 端点改到**卡边**(`A.x`)并声明 `side: left` 等 |
21
+ | B3 | `unexpected-overlap:泳道带 × 跨道连线` | 跨道连线**必然**穿带(泳道图的设计本质) | 按纪律**显式声明** `expectedOverlaps` 并写明"属设计意图",**不得改门禁** |
22
+ | B4 | 回边竖管**压住卡下标签** | 车道基准没让开标签带 | 车道整体下移(≥ 标签带高 + 8px);标签放在车道**上方** |
23
+ | B5 | `style-connector-off-target:还有 4.0px` | 连接符尖端离目标轮廓太远 | 尖端**贴住**目标边(±2.5px 内) |
24
+ | B6 | 环形图连线从卡心出发、压住卡片 | 同 B2 | 用**卡边 + 沿环方向**裁剪端点(`off ≈ 半卡宽`) |
25
+
26
+ ## C. 文字与间距
27
+
28
+ | # | 症状 | 根因 | 修法 |
29
+ |---|---|---|---|
30
+ | C1 | `text-overflow:估算宽 65px / 容器 44px` | 节点多于列宽、或左对齐槽位没重算 | 槽宽从卡片重算;**放不下按度量自动缩字号**(下限 9pt) |
31
+ | C2 | `content-collision:标题 × 副标题`(交叠 2px) | 两行文字框挨太紧 | 拉开 ≥4px;或加高父盒并两行一起重排 |
32
+ | C3 | `style-badge-too-tight`(徽标压字) | 徽标竖栏居中,压到第二行文字 | **有副标题时徽标与标题同行** |
33
+ | C4 | `style-contrast-low:1.22:1`(白字压浅蓝渐变) | 浅色/渐变底上硬用白字 | 文字色按**对比度自动选**;渐变要按 **stops 两端取最差**判 |
34
+ | C5 | 标签被线穿过 | 标签落在车道上 | 标签移到车道**上方**一行 |
35
+
36
+ ## D. 工程与流程(我踩过、代价最大的)
37
+
38
+ | # | 症状 | 根因 | 修法 |
39
+ |---|---|---|---|
40
+ | D1 | `ReferenceError: require is not defined in ES module scope` | 脚本存成 `.mjs` 却用 `require` | CJS 用 `.cjs`;ESM 用 `import` |
41
+ | D2 | 脚本语法错、输出乱码/`ParserError`,反复改不对 | 把带 `\n`、引号的复杂 JS **塞进 PowerShell here-string** ⇒ 转义层层叠加 | **用文件工具直接写脚本文件**(字节可控)或**直接写 YAML** |
42
+ | D3 | `Slides.Item : Integer out of range. 2 is not in…` | 渲染请求的页数 > pptx 实际页数 | 按 `ctx.pages.length` **动态**传页数 |
43
+ | D4 | 改了引擎"看起来修好了",用户打开的还是旧文件 | **只改代码没重出产物** | 铁律:改了引擎/页面 ⇒ **重出 pptx/PNG 并替换到位** |
44
+ | D5 | 只看坐标就宣布"修好了",实际图上仍有问题 | 数值 ≠ 观感 | 铁律:**先读图再下结论**;图与门禁互补,二者都给 |
45
+ | D6 | 补丁"语法正确、构建通过",目标却没变化 | 锚点落在**别的族/别的分支**(8 个族共用相似代码) | **插桩定位**(在可疑函数里放唯一标记,渲染后看 notes 是否出现);或先算**函数区间**只改区间内 |
46
+ | D7 | 门禁/合规抓不到"内容缺失"(如标签全丢) | 门禁只查几何与可读性,不查语义完整性 | 与**旧版并排读图**对照内容;把"每条边都要有标签"写进清单 |
47
+ | D8 | 复杂度量爆炸:改了盒高,子元素掉出盒外 | 尺寸改动**没有联动** | 改尺寸时**同时**检查:父盒、内部元素、连线端点 |
48
+
49
+ ## E. 渲染与交付通路(2026-09 实测补充)
50
+
51
+ | # | 症状 | 根因 | 修法 |
52
+ |---|---|---|---|
53
+ | E1 | 交付的 PNG 与源码/门禁不一致(改了页面但图没变),且 sha 与旧版一字不差 | **有进程打开着目标 pptx(尤其 PowerPoint)**⇒ COM `Presentations.Open` 拿到的是**内存里的旧副本**,静默渲染旧内容 | 渲染前确认没有进程占用该 pptx;**重出前先清空输出目录**,避免旧 PNG 残留被当成新图复制 |
54
+ | E2 | `exportPptx` 抛 `open … .pptx`(EBUSY/EPERM)、管线中断 | 同上,文件被 PowerPoint 锁住 | 请用户关闭后重出;或导出到新文件名 |
55
+ | E3 | 复制出的 PNG 是别的/更早的页面 | 复制步骤依赖渲染器输出名(`01.png…`),若渲染失败/被锁则**复制的是上次残留** | 渲染后校验输出文件的时间戳/sha 再复制;失败就中断,别静默复制 |
56
+ | E4 | 读图复核时看到的是旧图(同路径读两次结果完全相同) | 看图通路**按路径缓存**了归一化副本 | 复核大改后的页面时,**复制到新文件名再读**(或改路径) |
57
+
58
+ ## B7. 斜边 prst 的端点:轮廓 ≠ bbox
59
+
60
+ | 症状 | 根因 | 修法 |
61
+ |---|---|---|
62
+ | 平行四边形/chevron 等斜边形状旁边,箭头看着"没接上"(留 ~15px 缝) | `parallelogram` 的路径是 `(0,h)→(x1,0)→(w,0)→(w−x1,h)`,`x1 = adj/100000 × min(w,h)`(默认 adj=25000)⇒ **腰高处可见边比 bbox 内缩 x1/2** | 端点按**轮廓**算:左/右侧各内缩 `x1/2`(118 高时约 15px) |
63
+ | 按轮廓修完反而报 `unexpected-overlap 形状×线` | **门禁按 bbox 判形状** ⇒ 端点进入 bbox 必报重叠 | 这属 bbox×斜边轮廓的**固有偏差**,按纪律 `expectedOverlaps` **显式声明并写明理由**(不是掩盖缺陷);或改从**上下边**接入(水平边无内缩,最干净) |
64
+
65
+ | E5 | `node scripts/smoke.mjs` 第二次跑**必然崩溃**(`templates.js:334 目标目录已存在 deck.yaml,拒绝覆盖`) | smoke **非幂等**:它固定用 `examples/smoke/.tmp-home/_ws` 做模板物化目标,跑完不清理 ⇒ 残留 | 复跑前先 `Remove-Item examples\smoke\.tmp-home -Recurse -Force`。另:**"崩"与"断言红"要分清**——崩多为环境/残留,红才是真回归;盲目把崩当成自己的代码问题会浪费整轮 |
66
+
67
+ **E5 补充(2026-09 实测第二次踩到)**:只清 `.tmp-home` 不够 —— smoke 还会改写多种 fixture
68
+ (`band-smoke / media-smoke / overlap-smoke / scaffold-smoke / v3-smoke / smoke`)。
69
+ 实测现象:`✗ 连线:全部 6 条 cxnSp 已导出` + `TypeError: Cannot read properties of undefined (reading 'flipH')`
70
+ (看似"导入导出坏了",实为**残留副本**)。稳妥配方:
71
+
72
+ ```powershell
73
+ Remove-Item examples\smoke\.tmp-home -Recurse -Force -ErrorAction SilentlyContinue
74
+ git checkout -- examples # 把被 smoke 改写的跟踪 fixture 恢复原样
75
+ node scripts/smoke.mjs
76
+ ```
77
+
78
+ 判据:**同一 HEAD 下上一轮 322/0、这一轮崩溃 ⇒ 先怀疑 fixture,再怀疑代码**(本阶段两次都是 fixture)。
79
+
80
+
81
+
82
+ ## F. 流程坑(发版准备期实测,血泪级)
83
+
84
+ - **F1 清 fixture 只能用 `git checkout -- examples/smoke`** —— 用 `git checkout -- examples` 会连 `examples/hand-drawn/gen/*.cjs` 一起回滚,把刚做的手画/生成器修改一并抹掉(实测损失过一次修复)。
85
+ - **F2 参数化功能必须同时测「缺省路径」** —— `const X = argv[2] || X` 这类自引用(TDZ)会被「只测带参数路径」短路掩盖 ⇒ 缺省路径必崩且躲过全部测试;生成器现由 `check-generators.mjs`(语法级)兜底,运行期覆盖仍未做(见已知限制)。
86
+ - **F3 改基线必须留痕** —— `--capture` 曾整文件覆写导致上次原因丢失;现保留 `history` + 支持 `--reason`,且 `verify-handdrawn` 会**断言** history 与 reason 存在。
87
+ - **F4 破坏性脚本先只读定位、全量断言,再动手** —— 边定位边删 ⇒「定位失败时已删一半」;范式:Phase A 打印现场 + 断言 ⇒ Phase B 执行(异常即 `git reset --hard`)。
88
+ - **F5 多步脚本里每个判据都要单独取退出码** —— 曾在 `check-*` 子检查失败时因 `$LASTEXITCODE` 取到的是别的命令而**误提交**(后经 amend 更正)⇒ 判据不过不提交。
89
+ - **F6 Windows 工具链三坑** —— `npm`/`tar` 是垫片 ⇒ 必须 `execSync`(`execFileSync` 直接 ENOENT);命令输出带 `\r` ⇒ 精确匹配前先去掉;检查器扫描要排除自身(词表自指)。
90
+
91
+ ## G. 发布通道坑(2026-09-30 实测)
92
+
93
+ - **G1 推 tag ≠ 发布** ✗ —— `publish.yml` 由 **`release` 事件**触发(OIDC 受信发布)⇒ 只推 `v1.2.1` 不会发;必须创建 GitHub Release。1.2.1 首次就因此静默没发(registry 停在 1.2.0)。
94
+ - **G2 别把本地 `npm publish` 的失败当发布失败** ✗ —— 本机无 npm 凭据(E401)是**设计如此**;发布判据只能是 **registry 现状**(`npm view … version`)。
95
+ - **G3 创建 Release 就等于发布** ✗ —— 未获用户明确指令不得创建 Release;撤回需**删 Release + `gh run cancel`**(本地 pack 的 tag 不会自动发)。
96
+ - **G4 skill 的 frontmatter 是 YAML** ✗ —— 描述里写 `role: decoration` 这类含 **`: `** 的纯量会导致该 skill **静默解析失败**(症状:14 个对、1 个不见)⇒ 描述里用全角「:」或加引号。
97
+ - **G5 用自己的注册器自证** ✓ —— 改完 skill 立刻跑 `bundledSkillNames()`(插件自带扫描器)确认数量与名字,**别等装到用户机器上才发现**。
@@ -0,0 +1,17 @@
1
+ # 连线规则(routing)
2
+
3
+ ## 端点
4
+ - **端点必须落在元素边上**(x = 卡左边/右边,y = 卡心)。落在卡内 ⇒ 门禁报"卡 × 线";落在空中 ⇒ 引擎会**自动吸附**到附近元素,画出莫名长线。
5
+ - 有 `attach` 能力时优先用锚定;否则手工把坐标算到边上。
6
+
7
+ ## 折线与车道
8
+ - 正交折线:先竖后横或先横后竖,转角 90°;拐点尽量落在元素间距中线。
9
+ - **回边走外圈车道**(图上/图下),车道基准要让开所有预留带(标题带、标签带)。
10
+ - 回边标签放在**车道上方独立一行**,不允许压在车道上。
11
+
12
+ ## 标签遮罩
13
+ - 标签必须落在线上时,用**白底小圆角芯片**承载文字(芯片写进 contains),模拟"遮罩"效果。
14
+ - 连线贴边判据:尖端距目标轮廓 **≤2.5px**。
15
+
16
+ ## 跨容器
17
+ - 泳道/分组容器内的连线必然穿容器 ⇒ 把**真正相交**的元素对写进 `expectedOverlaps` 并写明"设计意图";过度声明会产生 declared-stale 噪音。
@@ -0,0 +1,16 @@
1
+ # 文字与间距(text-and-spacing)
2
+
3
+ ## 字号级差(一页内固定用这几档)
4
+ 25(页标题)/ 16(面板标题·卡片标题)/ 13–15(卡内主文)/ 11–12(副标题·注解·标签)。
5
+
6
+ ## 槽位与自动缩字
7
+ - 形状内文字**必须写进该形状的 contains**;无内容的形状**省略 contains**(不能写空数组)。
8
+ - 文字放不下时:先**从卡片重算槽宽**,再按度量**自动缩字号**(下限 9pt);宁可缩字,不要溢出。
9
+ - 左对齐时槽宽 = 卡宽 − 左侧让位(徽标栏)− 右侧留白;**徽标与标题同行**,不要把徽标竖着居中(会压第二行)。
10
+
11
+ ## 行距与互压
12
+ - 同一盒内两行文字框间隔 ≥4px;改了盒高要**同时重排内部两行**(否则 contains 不成立)。
13
+
14
+ ## 对比度
15
+ - 文字与底色对比度 **≥3:1**;渐变底要按 **stops 的最亮端与最暗端分别判、取最差**(不许用平均值)。
16
+ - 浅色/渐变底上不要默认白字:按对比度**自动选**字色。
@@ -0,0 +1,50 @@
1
+ # 验收流程(verify)
2
+
3
+ ## 固定五步(每页都要走完)
4
+ 1. 按子 skill 配方生成显式 `elements`(不手写"裸坐标"乱撞)。
5
+ 2. `resolveDeck` → `renderDeck`(产出预览 HTML 与 layout.json)。
6
+ 3. **过门禁**:`verifyDeck` 逐页 0 错(关系/重叠/溢出/对比度/贴边);有错按 pitfalls 定位后再改。
7
+ 4. `exportPptx` → **真渲染 PNG**。
8
+ 5. **自己读图**:看层级、留白、避让、连线语义(谁指向谁);不满意就地改,回到第 2 步。
9
+
10
+ ## 两条看图通路
11
+ - **COM(首选)**:lib/msrender.js 的 renderPptxToPng(pptx, outDir, { pages });页数要按实际页数传(传大了报 Slides.Item 越界)。
12
+ - **Edge 无头(备用)**:msedge --headless=new --window-size=W,H --screenshot=out.png file:///...preview.html —— 视口要按画布比例设,否则画面挤在角落。
13
+
14
+ ## 判定口径(同内容对照时)
15
+ ① **内容一致**(语义/数据/标签不丢 —— 门禁抓不到,只能靠与旧版并排读图);② **可读性不降**(字号/对比度/避让,门禁可量化);③ **观感不降**(构图/层级/留白,人眼判定)。
16
+
17
+ ## 铁律
18
+ 先图后数;改了引擎/页面必须**重出产物并替换**;边界能力如实标注;不用 expectedOverlaps 掩盖真实缺陷。
19
+
20
+ ## 铁律之二:**警告也要逐条读**(2026-09 血的教训)
21
+
22
+ > 我连续多轮"门禁 0 错"就收工 ⇒ 用户连续反馈:竖线穿字、折点贴着箭头、箭头没接上平行四边形。
23
+ > 回查发现**这些全都在警告里早就报了**:`line-crossing`、`arrow-end-short-run`、`line-end-off-edge`、`arrow-tip-inside-shape`。
24
+ > ⇒ **0 错 ≠ 没问题**;0 错只等于"没有不可声明级缺陷"。
25
+
26
+ 收工前必须做到:
27
+ 1. 打印本页**全部警告**(按 code 分组计数);
28
+ 2. 每条警告给一句**处置结论**:改布局 / 改 `attach` / 写 `roleReason` 声明 / 判定为**信息性建议**(`hotspot`/`density`);
29
+ 3. **一条不解释就不许收工**。
30
+
31
+ 配套索引见 `mechanisms.md`(何时用哪个机制 + 门禁 code 对照表)。
32
+
33
+ ## 铁律之三:验证输出的"看法"(2026-09,三次踩坑)
34
+
35
+ 1. **验证输出一律原文落屏,不许过滤成摘要** ✗
36
+ 我连续三次把 smoke/probe 输出 `Select-String` 成几行摘要 ⇒ 失败断言**没留下** ⇒ 无法归因,
37
+ 只能猜(其中两次猜错)✓。至少保留**失败行 + 其上下文**;宁可刷屏。
38
+ 2. **判据只看汇总行与退出码** ✓
39
+ `==== 结果:N 通过 / M 失败 ====` / `node --check` 的 exit code / `抓取断言:全部命中`。
40
+ **绝不要用 `grep ✗` 当失败探测器** ✗ —— smoke 正文里**故意造错的 fixture** 自身就带 `[✗]`,
41
+ 会把"通过用例的内部内容"当成失败 ✓(我被骗过一次 ✓)。
42
+ 3. **复跑 smoke 前先清 fixture** ✓
43
+ ```powershell
44
+ Remove-Item examples\smoke\.tmp-home -Recurse -Force -ErrorAction SilentlyContinue
45
+ git checkout -- examples
46
+ ```
47
+ 4. **归因顺序:先 fixture,后代码 —— 但别停在 fixture** ✓
48
+ 同一 HEAD 上轮绿、本轮崩 ⇒ **先怀疑 fixture 残留** ✓(本阶段三次如此);
49
+ 但本轮实测反例:`cxnSp` 断言崩**是代码真错** ✗(我把 2 点斜连线正交化了 ✓)⇒
50
+ 怀疑过 fixture 之后,**必须再核对一次"我的改动是否真的违反契约"** ✓。