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,114 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * `unknown-key` 检查:页面级/元素级**未知键** ⇒ 报 warning(**不阻断** ✓)。
4
+ *
5
+ * 为什么需要:写错/残留的键过去会被静默忽略 ⇒ 表面"出图了",其实是空页或漏项。
6
+ *
7
+ * 白名单来源(**不手写猜测** ✓):
8
+ * · 页面级:从 `src/pptd/schema.js` 的 `validatePage` 里**真正被访问的 `page.<键>`** 推导;
9
+ * · 元素级:`ELEMENT_KEYS`(类型专属)+ 通用键(`elementId/elementType/bounds/...`)+ `path`(custGeom 路径)。
10
+ * 防漂移:默认模式扫**本仓自带示例**,一旦发现未知键就**判失败** ⇒ 白名单漏项会立刻在 `npm test` 暴露 ✓
11
+ * (宁可在 CI 里发现自己漏了键,也不能对用户误报)。
12
+ *
13
+ * 用法:
14
+ * node scripts/check-unknown-keys.mjs # 扫 examples/hand-drawn + examples/smoke(必须 0 命中)
15
+ * node scripts/check-unknown-keys.mjs --fixture <dir> # 扫指定夹具(至少 1 命中才算红样本通过)
16
+ */
17
+ import { readdirSync, readFileSync, existsSync } from 'node:fs'
18
+ import { join } from 'node:path'
19
+
20
+ const ROOT = new URL('..', import.meta.url).pathname.replace(/^\/([A-Za-z]:)/, '$1')
21
+
22
+ // ── 白名单(来源见文件头注释;改 schema 消费点时同步这里)──
23
+ const PAGE_KEYS = new Set([
24
+ 'version', 'pageType', 'title', 'elements', 'notes', 'groups', 'expectedOverlaps',
25
+ 'background', 'blocks', 'contrastExempt', 'expectedOutOfSafeArea', 'overlapMode',
26
+ 'rowBounds', 'safeArea', 'source',
27
+ ])
28
+ const EL_COMMON = new Set([
29
+ 'elementId', 'elementType', 'bounds', 'role', 'roleReason', 'contains', 'badgeOf',
30
+ 'expectedOverlaps', 'rotation',
31
+ ])
32
+ const EL_BY_TYPE = {
33
+ text: ['content'],
34
+ shape: ['kind', 'fill', 'line', 'path'],
35
+ line: ['points', 'arrow', 'line', 'attach', 'dash'],
36
+ image: ['src', 'fit'],
37
+ table: ['cols', 'rows', 'header'],
38
+ chart: ['chart'],
39
+ }
40
+ const elAllowed = (type) => new Set([...EL_COMMON, ...(EL_BY_TYPE[type] ?? [])])
41
+
42
+ // ── 收集待检 YAML ──
43
+ const collect = (dir) => {
44
+ const out = []
45
+ const rec = (d) => {
46
+ for (const e of readdirSync(d, { withFileTypes: true })) {
47
+ const p = join(d, e.name)
48
+ if (/preview$|\.tmp/.test(p)) continue
49
+ if (e.isDirectory()) rec(p)
50
+ else if (/^pages?\/.*\.ya?ml$|pages\/.*\.ya?ml$/.test(p.replace(/\\/g, '/')) || /\.ya?ml$/.test(e.name)) out.push(p)
51
+ }
52
+ }
53
+ rec(dir)
54
+ return out.filter((p) => !/deck\.ya?ml$/.test(p)) // deck.yaml 是 deck 级(另有 schema 校验)
55
+ }
56
+
57
+ // ── 极简 YAML 顶层/元素键扫描(只认结构,不做完整解析)──
58
+ const scanPage = (file) => {
59
+ const text = readFileSync(file, 'utf8')
60
+ const lines = text.split('\n')
61
+ const found = []
62
+ let inElements = false
63
+ let curEl = null
64
+ const flushEl = () => {
65
+ if (!curEl) return
66
+ const allowed = elAllowed(curEl.type)
67
+ for (const k of Object.keys(curEl.keys)) if (!allowed.has(k)) found.push({ scope: '元素 ' + (curEl.id ?? '?'), key: k })
68
+ curEl = null
69
+ }
70
+ for (let i = 0; i < lines.length; i++) {
71
+ const raw = lines[i]
72
+ const line = raw.replace(/\s+#.*$/, '')
73
+ if (!line.trim() || /^\s*#/.test(line)) continue
74
+ const top = line.match(/^([A-Za-z_][\w]*):/)
75
+ if (top) {
76
+ if (top[1] === 'elements') { inElements = true; continue }
77
+ if (!PAGE_KEYS.has(top[1])) found.push({ scope: '页面', key: top[1] })
78
+ continue
79
+ }
80
+ if (!inElements) continue
81
+ const elStart = line.match(/^ {2}- elementId:\s*(\S+)/)
82
+ if (elStart) { flushEl(); curEl = { id: elStart[1].replace(/['"]/g, ''), type: null, keys: {} }; continue }
83
+ if (!curEl) continue
84
+ const kv = line.match(/^ {4}([A-Za-z_][\w]*):\s*(.*)$/)
85
+ if (!kv) continue
86
+ const [, k, v] = kv
87
+ if (k === 'elementType') curEl.type = v.trim().replace(/['"]/g, '')
88
+ else curEl.keys[k] = true
89
+ }
90
+ flushEl()
91
+ return found
92
+ }
93
+
94
+ // ── 主流程 ──
95
+ const args = process.argv.slice(2)
96
+ const fixtureIdx = args.indexOf('--fixture')
97
+ const targets = fixtureIdx >= 0 ? [join(ROOT, args[fixtureIdx + 1])] : [join(ROOT, 'examples', 'hand-drawn'), join(ROOT, 'examples', 'smoke')]
98
+ const hits = []
99
+ for (const t of targets) {
100
+ if (!existsSync(t)) continue
101
+ for (const f of collect(t)) for (const h of scanPage(f)) hits.push({ file: f.replace(ROOT + '/', ''), ...h })
102
+ }
103
+ if (fixtureIdx >= 0) {
104
+ console.log('unknown-key 红样本:[fixture] 命中 ' + hits.length + ' 处' + (hits.length > 0 ? ' ✓' : ' ✗'))
105
+ for (const h of hits) console.log(' [警告][unknown-key] ' + h.file + ' — ' + h.scope + ' 出现未知键 "' + h.key + '"')
106
+ process.exit(hits.length > 0 ? 0 : 1)
107
+ }
108
+ console.log('unknown-key 零误报自证:扫 ' + targets.length + ' 个目录|未知键 ' + hits.length + ' 处')
109
+ for (const h of hits.slice(0, 12)) console.log(' · ' + h.file + ' ' + h.scope + ' → ' + h.key)
110
+ if (hits.length) {
111
+ console.log('✗ 本仓示例出现未知键 ⇒ 要么页面写错(应修页面),要么白名单漏了合法键(应补白名单)——两者都必须处理')
112
+ process.exit(1)
113
+ }
114
+ console.log('✓ 本仓示例 0 未知键(白名单与真实用法一致)')
@@ -0,0 +1,177 @@
1
+ {
2
+ "savedAt": "2026-09-30T04:24:38.618Z",
3
+ "head": "55f17ad08101da726b73a9001134a6746ae86299",
4
+ "digest": "f675371b5e56d694d80e4915cc443ccd0aa3e88b7c97d367aceaf5539ae35fbd",
5
+ "perPage": [
6
+ {
7
+ "page": "01",
8
+ "els": 23,
9
+ "errors": 0,
10
+ "errorCodes": [],
11
+ "warns": 3,
12
+ "warnCodes": [
13
+ "hotspot"
14
+ ],
15
+ "digest": "cc0400aa6eb74da595cc69ee555418d2123c408eaebc739b389ba4ee142c1d17"
16
+ },
17
+ {
18
+ "page": "02",
19
+ "els": 19,
20
+ "errors": 0,
21
+ "errorCodes": [],
22
+ "warns": 1,
23
+ "warnCodes": [
24
+ "density"
25
+ ],
26
+ "digest": "fb901a860b2812aa8dc00b34e8d0f48b87b25fecc0eec6471cea92fa637d272c"
27
+ },
28
+ {
29
+ "page": "03",
30
+ "els": 27,
31
+ "errors": 0,
32
+ "errorCodes": [],
33
+ "warns": 5,
34
+ "warnCodes": [
35
+ "density",
36
+ "hotspot",
37
+ "line-end-off-edge"
38
+ ],
39
+ "digest": "82603d08ddeed43514ceca9dca6a26a72cff87bdd5009651c3444d6bd3d5b700"
40
+ },
41
+ {
42
+ "page": "04",
43
+ "els": 31,
44
+ "errors": 0,
45
+ "errorCodes": [],
46
+ "warns": 7,
47
+ "warnCodes": [
48
+ "arrow-tip-inside-shape",
49
+ "density",
50
+ "hotspot"
51
+ ],
52
+ "digest": "4ba29ae26b06e837e647c6c9ffe88139c8e853c637b78b1808c8a2c60512aad1"
53
+ },
54
+ {
55
+ "page": "05",
56
+ "els": 23,
57
+ "errors": 0,
58
+ "errorCodes": [],
59
+ "warns": 3,
60
+ "warnCodes": [
61
+ "density",
62
+ "hotspot"
63
+ ],
64
+ "digest": "08c7d4dc3efce9f743a96cd25489b94dbfd0d8ea03efb8650432769860fa5dd3"
65
+ },
66
+ {
67
+ "page": "06",
68
+ "els": 17,
69
+ "errors": 0,
70
+ "errorCodes": [],
71
+ "warns": 4,
72
+ "warnCodes": [
73
+ "line-diagonal-segment"
74
+ ],
75
+ "digest": "dc9bafef54b12d21bed5e158541f1338189ece093592d89b3534a024bb60956a"
76
+ },
77
+ {
78
+ "page": "07",
79
+ "els": 10,
80
+ "errors": 0,
81
+ "errorCodes": [],
82
+ "warns": 0,
83
+ "warnCodes": [],
84
+ "digest": "7dd49bca1f4d720972e5540ab00c08d11f1e543d0b50151126c0fee5944d22c8"
85
+ },
86
+ {
87
+ "page": "08",
88
+ "els": 23,
89
+ "errors": 0,
90
+ "errorCodes": [],
91
+ "warns": 6,
92
+ "warnCodes": [
93
+ "line-diagonal-segment"
94
+ ],
95
+ "digest": "e6955cc6d03591df46be9312201f2a5088c1c26a64248304ed1d00d1a48030d4"
96
+ },
97
+ {
98
+ "page": "09",
99
+ "els": 29,
100
+ "errors": 0,
101
+ "errorCodes": [],
102
+ "warns": 1,
103
+ "warnCodes": [
104
+ "line-crossing"
105
+ ],
106
+ "digest": "3bd30c8dc70278153553e829575e05818449883b8a1a101f5f4025c4660a5711"
107
+ },
108
+ {
109
+ "page": "10",
110
+ "els": 27,
111
+ "errors": 0,
112
+ "errorCodes": [],
113
+ "warns": 2,
114
+ "warnCodes": [
115
+ "density",
116
+ "hotspot"
117
+ ],
118
+ "digest": "475fbd6aaa18735c00a211387a89a5eabc45e90294dbc36e480611c1554a8dc7"
119
+ },
120
+ {
121
+ "page": "11",
122
+ "els": 18,
123
+ "errors": 0,
124
+ "errorCodes": [],
125
+ "warns": 0,
126
+ "warnCodes": [],
127
+ "digest": "45e04425fe4a65830f3850c66f166efc492befee277862c3d62837f9d764a55a"
128
+ },
129
+ {
130
+ "page": "12",
131
+ "els": 29,
132
+ "errors": 0,
133
+ "errorCodes": [],
134
+ "warns": 1,
135
+ "warnCodes": [
136
+ "density"
137
+ ],
138
+ "digest": "6c29eaebbde6714ad9b54e10cfa078070aab86a7c581f2f985424f873a8e16a6"
139
+ },
140
+ {
141
+ "page": "13",
142
+ "els": 25,
143
+ "errors": 0,
144
+ "errorCodes": [],
145
+ "warns": 7,
146
+ "warnCodes": [
147
+ "attach-override",
148
+ "density",
149
+ "hotspot"
150
+ ],
151
+ "digest": "e4d6fc9e7aa69febc5af53cb659a3553f70655a0e2bafb85e2476ecb98ec6ee4"
152
+ }
153
+ ],
154
+ "pngs": {
155
+ "01-层级树(手画).png": "e58fadc991edb4b57b6832cad9830ed28aa62f34fb8097e7ef56352fee915855",
156
+ "02-矩阵(手画).png": "f72c62f4215b4f09643c90221adbd97ee8d725b3d55e8b7ca7f88ee25dee1a66",
157
+ "03-时间轴(手画).png": "df7bd35389ab379c6cabdfac2ae82ebec29aebb2bdfd5b5ce82f895fd897adb5",
158
+ "04-泳道(手画).png": "ee642e5989c1c4f09a079e26e82b1b720449910f3a92d8d6fdbebf5a23b888af",
159
+ "05-左右对比(手画).png": "afa2caf3a16bcebc8cb1d3ead09a5dc5b3eaed6ca682281d8268402d50d0bd71",
160
+ "06-闭环(手画).png": "f5b8cf822c503fbe1669fea5fc109fb84d946a87ddb35b6bd6a12ed90acf8bbd",
161
+ "07-漏斗(手画).png": "f00e0251aef97510aec6336fe2a32fcd8f5acb0c5b2c1e361be95598909d72b1",
162
+ "08-步骤环(手画).png": "50e1c1f175fea024c0c5b956cfb919dc44fa7e8329646199343097c538943d31",
163
+ "09-时序(手画).png": "1cc278d9a41f5f3e2217f0913af954b264a2e01a6326723fcce4584fdbe20331",
164
+ "10-状态机(手画).png": "9a84e28cabacf631118633e0c50ca4ef4a402fb659da0c8310b80c421a9ff36e",
165
+ "11-流程图(手画).png": "dcf2887e1824496448bfaf2be3757fb7087a989e63a8f2eb55e3be150c5727ab",
166
+ "12-分层架构(手画).png": "69989c267bf13a89279f4d192e752f8aff918d49b0583c1e0888a60ea22e37a0",
167
+ "13-平台架构(手画).png": "05a454b19ffd2aac351e2e343d4a545aeeb630f1bd5eda0183fe3fe2fb09c100"
168
+ },
169
+ "history": [
170
+ {
171
+ "at": "2026-09-30T04:24:38.616Z",
172
+ "from": "f675371b5e56d694d80e4915cc443ccd0aa3e88b7c97d367aceaf5539ae35fbd",
173
+ "to": "f675371b5e56d694d80e4915cc443ccd0aa3e88b7c97d367aceaf5539ae35fbd",
174
+ "reason": "摘要未变(重采): P2-3 补采:为基线补写变更沿革(history 断言上线前的一次留痕)"
175
+ }
176
+ ]
177
+ }
@@ -0,0 +1,178 @@
1
+ {
2
+ "measuredAt": "2026-09-27",
3
+ "estimator": "CJK=1 token/字;其余=4 字节/token",
4
+ "toolCount": 22,
5
+ "tools": [
6
+ {
7
+ "name": "ppt_verify",
8
+ "chars": 860,
9
+ "bytes": 1467,
10
+ "tokens": 436
11
+ },
12
+ {
13
+ "name": "ppt_patch",
14
+ "chars": 767,
15
+ "bytes": 1283,
16
+ "tokens": 380
17
+ },
18
+ {
19
+ "name": "ppt_visual",
20
+ "chars": 733,
21
+ "bytes": 1208,
22
+ "tokens": 355
23
+ },
24
+ {
25
+ "name": "ppt_template_add",
26
+ "chars": 795,
27
+ "bytes": 1211,
28
+ "tokens": 353
29
+ },
30
+ {
31
+ "name": "ppt_splice",
32
+ "chars": 677,
33
+ "bytes": 1071,
34
+ "tokens": 310
35
+ },
36
+ {
37
+ "name": "ppt_export",
38
+ "chars": 637,
39
+ "bytes": 955,
40
+ "tokens": 279
41
+ },
42
+ {
43
+ "name": "ppt_template_styleaudit",
44
+ "chars": 573,
45
+ "bytes": 853,
46
+ "tokens": 244
47
+ },
48
+ {
49
+ "name": "ppt_crosscheck",
50
+ "chars": 425,
51
+ "bytes": 761,
52
+ "tokens": 233
53
+ },
54
+ {
55
+ "name": "ppt_new",
56
+ "chars": 524,
57
+ "bytes": 712,
58
+ "tokens": 202
59
+ },
60
+ {
61
+ "name": "ppt_shot",
62
+ "chars": 533,
63
+ "bytes": 711,
64
+ "tokens": 199
65
+ },
66
+ {
67
+ "name": "ppt_slice",
68
+ "chars": 459,
69
+ "bytes": 657,
70
+ "tokens": 188
71
+ },
72
+ {
73
+ "name": "ppt_measure",
74
+ "chars": 407,
75
+ "bytes": 629,
76
+ "tokens": 182
77
+ },
78
+ {
79
+ "name": "ppt_templates",
80
+ "chars": 354,
81
+ "bytes": 580,
82
+ "tokens": 172
83
+ },
84
+ {
85
+ "name": "ppt_template_remove",
86
+ "chars": 352,
87
+ "bytes": 534,
88
+ "tokens": 157
89
+ },
90
+ {
91
+ "name": "ppt_preview",
92
+ "chars": 287,
93
+ "bytes": 443,
94
+ "tokens": 129
95
+ },
96
+ {
97
+ "name": "ppt_import",
98
+ "chars": 318,
99
+ "bytes": 410,
100
+ "tokens": 114
101
+ },
102
+ {
103
+ "name": "ppt_render",
104
+ "chars": 315,
105
+ "bytes": 399,
106
+ "tokens": 110
107
+ },
108
+ {
109
+ "name": "ppt_check",
110
+ "chars": 284,
111
+ "bytes": 380,
112
+ "tokens": 107
113
+ },
114
+ {
115
+ "name": "ppt_media",
116
+ "chars": 254,
117
+ "bytes": 340,
118
+ "tokens": 96
119
+ },
120
+ {
121
+ "name": "ppt_schema",
122
+ "chars": 214,
123
+ "bytes": 292,
124
+ "tokens": 83
125
+ },
126
+ {
127
+ "name": "ppt_state",
128
+ "chars": 249,
129
+ "bytes": 303,
130
+ "tokens": 83
131
+ },
132
+ {
133
+ "name": "ppt_status",
134
+ "chars": 213,
135
+ "bytes": 283,
136
+ "tokens": 80
137
+ }
138
+ ],
139
+ "skills": [
140
+ {
141
+ "name": "ppt-studio-manual",
142
+ "chars": 201,
143
+ "bytes": 403,
144
+ "tokens": 125
145
+ },
146
+ {
147
+ "name": "ppt-studio-data",
148
+ "chars": 127,
149
+ "bytes": 331,
150
+ "tokens": 107
151
+ },
152
+ {
153
+ "name": "ppt-studio-copy",
154
+ "chars": 131,
155
+ "bytes": 319,
156
+ "tokens": 102
157
+ },
158
+ {
159
+ "name": "ppt-studio-craft",
160
+ "chars": 121,
161
+ "bytes": 307,
162
+ "tokens": 99
163
+ }
164
+ ],
165
+ "workflow": {
166
+ "tokens": 4409,
167
+ "chars": 6941,
168
+ "quickTokens": 575,
169
+ "quickChars": 836,
170
+ "note": ""
171
+ },
172
+ "totals": {
173
+ "tools": 4492,
174
+ "skills": 433,
175
+ "workflow": 4409,
176
+ "all": 9334
177
+ }
178
+ }
@@ -0,0 +1,78 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ *
4
+ * · `pyramid: true` ⇒ 反向(逐段**放宽**的金字塔,而不是收窄的漏斗)
5
+ * · `minRatio` ⇒ 末段宽度相对首段的比例(末段宽 = 首段宽 × ratio)
6
+ * 本探针用**显式 custGeom 元素**把两种变体画出来并过门禁(不需要族代码)。
7
+ *
8
+ * 用法:node scripts/handdrawn-option-probe.mjs
9
+ */
10
+ import { mkdirSync, writeFileSync, readFileSync } from 'node:fs'
11
+ import { join } from 'node:path'
12
+ import { resolveDeck } from '../src/pptd/schema.js'
13
+ import { renderDeck } from '../src/pptd/render-html.js'
14
+ import { verifyDeck } from '../src/verify.js'
15
+
16
+ const ROOT = new URL('..', import.meta.url).pathname.replace(/^\/([A-Za-z]:)/, '$1')
17
+ const dir = join(ROOT, 'examples', 'option-probe')
18
+ mkdirSync(join(dir, 'pages'), { recursive: true })
19
+ writeFileSync(join(dir, 'deck.yaml'), [
20
+ 'version: 1', 'title: 选项探针', 'size: [960, 540]', 'theme:', ' colors:',
21
+ ' primary: "#7C3AED"', ' accent: "#E11D48"', ' soft: "#EDE9FE"',
22
+ ' text: "#2E1065"', ' bg: "#FAF5FF"', 'pages:',
23
+ ' - pages/pyramid.yaml', ' - pages/narrow.yaml', '',
24
+ ].join('\n'))
25
+
26
+ // 画一条"逐段变宽/变窄"的 custGeom 梯形带(手画等价物)
27
+ function bands(rows, title, sub, label) {
28
+ const bw = 800, bx = 80, bandH = 70, topY = 148
29
+ const L = ['pageType: content', 'elements:']
30
+ L.push(' - elementId: title', ' elementType: text', ' bounds: [60, 34, 760, 38]',
31
+ ` content: { text: '${title}', fontSize: 25, color: '#2E1065', bold: true }`)
32
+ L.push(' - elementId: sub', ' elementType: text', ' bounds: [60, 76, 820, 22]',
33
+ ` content: { text: '${sub}', fontSize: 12, color: '#64748B' }`)
34
+ rows.forEach(([name, topW], i) => {
35
+ const y = topY + i * bandH
36
+ const botW = i + 1 < rows.length ? rows[i + 1][1] : Math.round(topW * (label === 'pyramid' ? 1.5 : 0.5))
37
+ const lw = Math.max(120, Math.min(topW, botW) - 60)
38
+ L.push(` - elementId: b${i}`, ' elementType: shape', ' kind: custGeom',
39
+ ` bounds: [${bx}, ${y}, ${bw}, ${bandH}]`,
40
+ ` fill: '${i === rows.length - 1 ? '#E11D48' : '#7C3AED'}'`,
41
+ " line: { color: '#FFFFFF', width: 1.5 }", ` contains: [bt${i}]`,
42
+ ' path:', ` w: ${bw}`, ` h: ${bandH}`, ' commands:',
43
+ ` - { cmd: moveTo, pts: [[${Math.round((bw - topW) / 2)}, 0]] }`,
44
+ ` - { cmd: lnTo, pts: [[${Math.round((bw + topW) / 2)}, 0]] }`,
45
+ ` - { cmd: lnTo, pts: [[${Math.round((bw + botW) / 2)}, ${bandH}]] }`,
46
+ ` - { cmd: lnTo, pts: [[${Math.round((bw - botW) / 2)}, ${bandH}]] }`,
47
+ ' - { cmd: close }')
48
+ L.push(` - elementId: bt${i}`, ' elementType: text',
49
+ ` bounds: [${Math.round(480 - lw / 2)}, ${y + 24}, ${lw}, 26]`,
50
+ ` content: { text: '${name}', fontSize: 15, color: '#FFFFFF', bold: true, align: center }`)
51
+ })
52
+ return L.join('\n') + '\n'
53
+ }
54
+
55
+ // 变体 A:pyramid(逐段放宽 = 金字塔,族里 pyramid: true 的等价物)
56
+ writeFileSync(join(dir, 'pages', 'pyramid.yaml'), bands(
57
+ [['苗种', 240], ['育肥', 400], ['成鱼', 560], ['直供', 720]],
58
+ '金字塔变体(pyramid 等价)', '逐段放宽:苗种 240 → 育肥 400 → 成鱼 560 → 直供 720', 'pyramid'))
59
+
60
+ // 变体 B:minRatio(末段宽 ≈ 首段宽 × 比例;这里用 0.25 的收窄漏斗)
61
+ writeFileSync(join(dir, 'pages', 'narrow.yaml'), bands(
62
+ [['存栏', 760], ['达规格', 560], ['抽检', 360], ['直供', 190]],
63
+ '窄口漏斗变体(minRatio 等价)', '末段宽 ≈ 首段 × 0.25:760 → 560 → 360 → 190', 'narrow'))
64
+
65
+ const ctx = await resolveDeck(dir)
66
+ await renderDeck(ctx, { out: 'preview' })
67
+ const layout = JSON.parse(readFileSync(join(dir, 'preview', 'layout.json'), 'utf8'))
68
+ const pages = Array.isArray(layout.pages) ? layout.pages : Object.values(layout.pages)
69
+ let bad = 0
70
+ pages.forEach((p, i) => {
71
+ const v = verifyDeck({ pages: [p] })
72
+ const kind = i === 0 ? 'pyramid' : 'minRatio'
73
+ console.log(' ' + (v.errors.length ? '✗' : '✓') + ' ' + kind + ':元素 ' + p.elements.length + '|门禁 错' + v.errors.length + '/警' + v.warns.length)
74
+ v.errors.slice(0, 3).forEach((e) => console.log(' [错][' + e.code + '] ' + String(e.message).slice(0, 88)))
75
+ if (v.errors.length) bad++
76
+ })
77
+ console.log('==== 选项等价探针:' + (bad === 0 ? '两个变体均 0 错(族的 pyramid / minRatio 在手画下等价可复现)' : bad + ' 页未通过') + ' ====')
78
+ process.exit(bad === 0 ? 0 : 1)
@@ -0,0 +1,127 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * 常驻 prompt 成本探针(阶段 A 硬门槛之一:"无图任务 prompt 增幅 ≤5%")。
4
+ *
5
+ * 用法:
6
+ * node scripts/probe-prompt-cost.mjs # 测量并**写入**基线 scripts/fixtures/prompt-cost-baseline.json
7
+ * node scripts/probe-prompt-cost.mjs --check # 与基线对比;总成本增幅 > 5% ⇒ 退出码 1(阶段 B 引入 ppt_diagram 时用)
8
+ *
9
+ * 度量口径(**只看模型真正要读的部分**,不看执行结果文本):
10
+ * ① 工具面:每个工具的 name + description + parameters schema(JSON 形态)——output 渲染器不进模型
11
+ * ② 技能目录:skills/<name>/SKILL.md 的 front-matter(name + description)——正文只在加载时进上下文
12
+ * ③ 工作流提示段:router.workflowSection(...)(仅在 workflowActive 时注入)
13
+ *
14
+ * token 估计采用 CJK 感知的保守口径:CJK 字符按 1 token/字,其余按 4 字节/token。
15
+ * 这是**估算**,用途是"同一口径下的相对增幅"(正是 ≤5% 这条门槛要判的东西),不是绝对 token 数。
16
+ */
17
+ import { readFileSync, readdirSync, writeFileSync, existsSync } from 'node:fs'
18
+ import { join, dirname } from 'node:path'
19
+ import { fileURLToPath } from 'node:url'
20
+ import { registerTools } from '../lib/tools.js'
21
+ import { workflowSection } from '../lib/router.js'
22
+
23
+ const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..')
24
+ const BASELINE = join(ROOT, 'scripts', 'fixtures', 'prompt-cost-baseline.json')
25
+ const GROWTH_LIMIT = 1.05
26
+
27
+ /** CJK 感知的 token 估算(保守:CJK 每字 1 token)。 */
28
+ export function estTokens(s) {
29
+ let cjk = 0
30
+ for (const ch of s) if (/[\u3000-\u9fff\uff00-\uffef]/.test(ch)) cjk++
31
+ const rest = s.length - cjk
32
+ return Math.ceil(cjk + rest / 4)
33
+ }
34
+ const bytes = (s) => Buffer.byteLength(s, 'utf8')
35
+
36
+ // ① 工具面:用桩 ctx 捕获全部注册定义(registerTools 只用 effect/tools.register/get)
37
+ const captured = new Map()
38
+ const stub = {
39
+ effect: (cb) => { cb(); return () => {} },
40
+ tools: { register: (def) => captured.set(def.name, def) },
41
+ get: () => undefined,
42
+ }
43
+ registerTools(stub)
44
+ // ppt_state 由 statusToolFor 单独注册(也是模型可见的常驻工具)⇒ 一并计入
45
+ try {
46
+ const { statusToolFor } = await import('../lib/index.js')
47
+ statusToolFor(stub, 'profile')
48
+ } catch (e) {
49
+ console.log(`(提示:ppt_state 未能计入——${e.message})`)
50
+ }
51
+ const tools = [...captured.values()].map((d) => {
52
+ const text = JSON.stringify({ name: d.name, description: d.description ?? '', parameters: d.parameters ?? {} })
53
+ return { name: d.name, chars: text.length, bytes: bytes(text), tokens: estTokens(text) }
54
+ }).sort((a, b) => b.tokens - a.tokens)
55
+
56
+ // ② 技能目录:front-matter 的 name + description
57
+ const skills = []
58
+ const skillsDir = join(ROOT, 'skills')
59
+ for (const name of readdirSync(skillsDir).sort()) {
60
+ const f = join(skillsDir, name, 'SKILL.md')
61
+ if (!existsSync(f)) continue
62
+ const head = readFileSync(f, 'utf8').split(/^---\s*$/m)[1] ?? ''
63
+ const desc = (head.match(/^description:\s*(.*)$/m)?.[1] ?? '').trim()
64
+ const nm = (head.match(/^name:\s*(.*)$/m)?.[1] ?? name).trim()
65
+ const text = `${nm}\n${desc}`
66
+ skills.push({ name: nm, chars: text.length, bytes: bytes(text), tokens: estTokens(text) })
67
+ }
68
+ skills.sort((a, b) => b.tokens - a.tokens)
69
+
70
+ // ③ 工作流提示段(仅有 workflowActive 时注入;两种档都要测——quick 档另有字节不变守卫)
71
+ // 注意:workflowSection 返回 **{name, text}** 而不是字符串;cfg 需要 engine/mode/fidelity/quality/review/pauseAfter 等字段。
72
+ const WF_CFG = { mode: 'normal', engine: 'auto', fidelity: 'balanced', quality: 'standard', review: 'full', pauseAfter: [] }
73
+ function measureWorkflow(cfg) {
74
+ try {
75
+ const r = workflowSection('make', cfg)
76
+ const text = typeof r === 'string' ? r : (r?.text ?? '')
77
+ return text ? { tokens: estTokens(text), chars: text.length } : { tokens: 0, chars: 0 }
78
+ } catch (e) {
79
+ return { tokens: 0, chars: 0, error: e.message }
80
+ }
81
+ }
82
+ const wfStd = measureWorkflow(WF_CFG)
83
+ const wfQuick = measureWorkflow({ ...WF_CFG, quick: true })
84
+ let wf = { tokens: wfStd.tokens, chars: wfStd.chars, quickTokens: wfQuick.tokens, quickChars: wfQuick.chars, note: wfStd.error ?? '' }
85
+
86
+ const sum = (list) => list.reduce((n, x) => n + x.tokens, 0)
87
+ const totals = {
88
+ tools: sum(tools),
89
+ skills: sum(skills),
90
+ workflow: wf.tokens,
91
+ all: sum(tools) + sum(skills) + wf.tokens,
92
+ }
93
+ const report = {
94
+ measuredAt: new Date().toISOString().slice(0, 10),
95
+ estimator: 'CJK=1 token/字;其余=4 字节/token',
96
+ toolCount: tools.length,
97
+ tools,
98
+ skills,
99
+ workflow: wf,
100
+ totals,
101
+ }
102
+
103
+ const check = process.argv.includes('--check')
104
+ let fail = 0
105
+ console.log('==== 常驻 prompt 成本(模型要读的部分)====')
106
+ console.log(`工具面:${tools.length} 个工具 = ${totals.tools} tokens(前 5:${tools.slice(0, 5).map((t) => `${t.name} ${t.tokens}`).join('|')})`)
107
+ console.log(`技能目录:${skills.length} 个 = ${totals.skills} tokens(${skills.map((s) => `${s.name.replace('ppt-studio-', '')} ${s.tokens}`).join('|')})`)
108
+ console.log(`工作流段:${totals.workflow} tokens${wf.note ? `(${wf.note})` : ''}`)
109
+ console.log(`合计(不含工作流段,即"无图任务"的常驻面):${totals.tools + totals.skills} tokens;含工作流段 ${totals.all} tokens`)
110
+
111
+ if (check) {
112
+ if (!existsSync(BASELINE)) { console.log(`\n✗ 基线不存在(${BASELINE})——先不带 --check 跑一次生成基线`); process.exit(1) }
113
+ const base = JSON.parse(readFileSync(BASELINE, 'utf8'))
114
+ const baseResident = base.totals.tools + base.totals.skills
115
+ const nowResident = totals.tools + totals.skills
116
+ const growth = nowResident / baseResident
117
+ const okAll = totals.all <= base.totals.all * GROWTH_LIMIT
118
+ const okResident = nowResident <= baseResident * GROWTH_LIMIT
119
+ console.log(`\n基线(${base.measuredAt}):常驻 ${baseResident} / 含工作流 ${base.totals.all}`)
120
+ console.log(`${okResident ? '✓' : '✗'} 常驻面增幅 ${((growth - 1) * 100).toFixed(2)}%(上限 +5%)`)
121
+ console.log(`${okAll ? '✓' : '✗'} 含工作流段增幅 ${(((totals.all / base.totals.all) - 1) * 100).toFixed(2)}%(上限 +5%)`)
122
+ if (!okResident || !okAll) fail = 1
123
+ } else {
124
+ writeFileSync(BASELINE, JSON.stringify(report, null, 2) + '\n')
125
+ console.log(`\n✓ 基线已写入 ${BASELINE.replace(ROOT + '\\', '')}`)
126
+ }
127
+ if (fail) { console.log('\n✗ 常驻 prompt 成本超出门槛'); process.exit(1) }