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.
- package/README.md +4 -1
- package/docs/00-/350/256/276/350/256/241/346/272/220.md +4 -0
- package/docs/02-/346/212/200/346/234/257/346/212/245/345/221/212.md +114 -1
- package/docs/03-/346/233/264/346/226/260/346/227/245/345/277/227.md +159 -0
- package/docs/05-/350/277/255/344/273/243/346/265/201/347/250/213.md +23 -0
- package/docs/06-/350/257/204/345/256/241/344/270/216/346/265/213/350/257/225.md +2 -1
- 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
- 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
- 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
- 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
- package/docs/11-/346/234/257/350/257/255/350/241/250.md +42 -0
- 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
- package/docs/13-/345/217/221/347/211/210/345/207/206/345/244/207/350/256/241/345/210/222.md +79 -0
- 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
- package/docs/15-1.2.0 /345/217/221/345/270/203/346/212/245/345/221/212.md" +46 -0
- 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
- package/examples/fx/fx-pro/preview/layout.json +12 -0
- package/examples/fx/fx-pro/preview/measured.json +1 -1
- package/examples/fx/fx-pro.pptx +0 -0
- package/examples/fx/seed.pptx +0 -0
- package/examples/fx/single.pptx +0 -0
- package/examples/fx/spliced.pptx +0 -0
- package/examples/smoke/imported/preview-rt/layout.json +3 -0
- package/examples/smoke/imported/source.pptx +0 -0
- package/examples/smoke/out-smoke.pptx +0 -0
- package/examples/smoke/preview/layout.json +3 -0
- package/lib/group-render.js +177 -0
- package/lib/msrender.js +27 -12
- package/lib/pptd/blocks.js +52 -0
- package/lib/pptd/export-pptx.js +266 -11
- package/lib/pptd/layout.js +54 -3
- package/lib/pptd/line-rules.js +259 -0
- package/lib/pptd/line-text-rule.js +90 -0
- package/lib/pptd/relations.js +615 -0
- package/lib/pptd/render-html.js +19 -1
- package/lib/pptd/route-geometry.js +120 -0
- package/lib/pptd/schema.js +146 -11
- package/lib/pptd/style-profile.js +133 -0
- package/lib/scaffold.js +54 -1
- package/lib/tools.js +20 -2
- package/lib/verify.js +69 -1
- package/package.json +20 -3
- package/scripts/audit-manual-facts.mjs +15 -3
- package/scripts/bench-diagram-scale.mjs +165 -0
- package/scripts/check-generators.mjs +22 -0
- package/scripts/check-lib-sync.mjs +33 -0
- package/scripts/check-line-text.mjs +30 -0
- package/scripts/check-package.mjs +60 -0
- package/scripts/check-removed-traces.mjs +37 -0
- package/scripts/check-syntax.mjs +44 -0
- package/scripts/check-unknown-keys.mjs +114 -0
- package/scripts/fixtures/handdrawn-baseline.json +177 -0
- package/scripts/fixtures/prompt-cost-baseline.json +178 -0
- package/scripts/handdrawn-option-probe.mjs +78 -0
- package/scripts/probe-prompt-cost.mjs +127 -0
- package/scripts/red-probe.mjs +75 -0
- package/scripts/render-group.mjs +72 -0
- package/scripts/smoke.mjs +191 -20
- package/scripts/test-fast.mjs +108 -0
- package/scripts/test-map.mjs +162 -0
- package/scripts/test-release.mjs +64 -0
- package/scripts/verify-diagram-relations.mjs +405 -0
- package/scripts/verify-handdrawn.mjs +115 -0
- package/scripts/verify-handwritten.mjs +203 -0
- package/scripts/verify-preset-scope.mjs +1 -1
- package/scripts/verify-primitives.mjs +714 -0
- package/scripts/verify-products.mjs +37 -0
- package/scripts/verify-shape.mjs +84 -0
- package/scripts/visual-pair-batch.mjs +77 -0
- package/skills/ppt-studio-diagram/SKILL.md +30 -0
- package/skills/ppt-studio-diagram/capability-matrix.md +22 -0
- package/skills/ppt-studio-diagram/composition.md +22 -0
- package/skills/ppt-studio-diagram/mechanisms.md +141 -0
- package/skills/ppt-studio-diagram/pitfalls.md +97 -0
- package/skills/ppt-studio-diagram/routing.md +17 -0
- package/skills/ppt-studio-diagram/text-and-spacing.md +16 -0
- package/skills/ppt-studio-diagram/verify.md +50 -0
- package/skills/ppt-studio-diagram-compare/SKILL.md +38 -0
- package/skills/ppt-studio-diagram-flow/SKILL.md +38 -0
- package/skills/ppt-studio-diagram-funnel/SKILL.md +39 -0
- package/skills/ppt-studio-diagram-layers/SKILL.md +32 -0
- package/skills/ppt-studio-diagram-loop/SKILL.md +39 -0
- package/skills/ppt-studio-diagram-matrix/SKILL.md +37 -0
- package/skills/ppt-studio-diagram-sequence/SKILL.md +31 -0
- package/skills/ppt-studio-diagram-state/SKILL.md +48 -0
- package/skills/ppt-studio-diagram-steps-ring/SKILL.md +38 -0
- package/skills/ppt-studio-diagram-swimlane/SKILL.md +37 -0
- package/skills/ppt-studio-diagram-timeline/SKILL.md +38 -0
- package/skills/ppt-studio-diagram-tree/SKILL.md +38 -0
- package/skills/ppt-studio-manual/SKILL.md +2 -2
- package/templates/academic-white/preview.png +0 -0
- package/templates/business-blue/preview.png +0 -0
- package/templates/pitch-bold/preview.png +0 -0
- 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) }
|