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
package/lib/verify.js
CHANGED
|
@@ -19,6 +19,11 @@
|
|
|
19
19
|
* 不得静默跳过(静默返空与"真没问题"在输出上不可区分,2026-09-18 假绿灯事故的根因)
|
|
20
20
|
*/
|
|
21
21
|
|
|
22
|
+
// 阶段 A(docs/08 §5):结构关系(推导 + 几何反验证)。注意方向是 verify → relations,不反向
|
|
23
|
+
// (relations.js 不依赖 verify),因此不存在循环依赖。
|
|
24
|
+
import { deriveRelations, checkDiagram, summaryLine, usesStructure } from './pptd/relations.js'
|
|
25
|
+
import { checkLineRules } from './pptd/line-rules.js'
|
|
26
|
+
|
|
22
27
|
const TOL = 1 // px
|
|
23
28
|
|
|
24
29
|
const NEUTRAL_GRAY_RE = /^#(?:[0-9a-fA-F]{6})$/ // 黑白灰中性色宽判:RGB 各分量近等
|
|
@@ -256,7 +261,34 @@ export function analyzePage(page, size) {
|
|
|
256
261
|
const minY = sa?.top ?? 0
|
|
257
262
|
const maxX = pw - (sa?.right ?? 0)
|
|
258
263
|
const maxY = ph - (sa?.bottom ?? 0)
|
|
259
|
-
const
|
|
264
|
+
const declaredBase = declaredClosure(page) // P0-2:声明闭包(嵌套承载相邻层声明 → 隔层自动通过)
|
|
265
|
+
// ── 阶段 A(docs/08 §5):结构关系 = 推导 + 几何反验证 ──
|
|
266
|
+
// ① **有效**关系的豁免注入声明集(推导结果**不落盘**,每次现算);
|
|
267
|
+
// ② 声明不成立 ⇒ 点名错误;③ 已失效的声明降级为可见警告("声明复核"段);
|
|
268
|
+
// ④ 图专属机械检查。以上**仅在"本页用了结构声明"时启用** ⇒ 没用新特性的页面行为与从前完全一致。
|
|
269
|
+
const usedStructure = usesStructure(page)
|
|
270
|
+
const rel = usedStructure ? deriveRelations(page) : { relations: [], exempt: [], invalid: [], errors: [], stats: { groups: 0, contains: 0, attach: 0, badge: 0, overlapDecl: 0, exempt: 0 } }
|
|
271
|
+
const declared = new Set([...declaredBase, ...(usedStructure ? rel.exempt : [])])
|
|
272
|
+
if (usedStructure) {
|
|
273
|
+
for (const e of rel.errors) findings.push({ severity: 'error', code: e.code, message: e.detail })
|
|
274
|
+
for (const iv of rel.invalid) {
|
|
275
|
+
if (iv.type !== 'overlapDecl') continue // 不成立的 contains/attach/badge 已进 rel.errors
|
|
276
|
+
findings.push({ severity: 'warning', code: 'declared-stale', message: `预期重叠声明已失效:${iv.from} × ${iv.to}(两端当前并不相交,建议移除)` })
|
|
277
|
+
}
|
|
278
|
+
const dg = checkDiagram(page)
|
|
279
|
+
for (const e of dg.errors) {
|
|
280
|
+
// text-over-text 已由既有 content-collision 覆盖 ⇒ 不重复上抛(自证脚本仍直接断言该检查本身)
|
|
281
|
+
if (e.code === 'text-over-text') continue
|
|
282
|
+
findings.push({ severity: 'error', code: e.code, message: e.detail })
|
|
283
|
+
}
|
|
284
|
+
for (const w of dg.warnings) findings.push({ severity: 'warning', code: w.code, message: w.detail })
|
|
285
|
+
// attach 改写过手写坐标 ⇒ 必须可见("attach 优先"可以,静默覆盖不行)
|
|
286
|
+
for (const el of els) for (const n of (el.attachNotes ?? [])) findings.push({ severity: 'warning', code: 'attach-override', message: `${el.id}: ${n}` })
|
|
287
|
+
// 结构关系豁免的对数(供报告"结构关系"行使用)
|
|
288
|
+
findings.structureStats = { ...rel.stats, usedStructure: true }
|
|
289
|
+
}
|
|
290
|
+
// 折线绘制规则(阶段 A1,泛用性优先):对**所有**页面生效(含手写自由发挥)——只警告,不改判定
|
|
291
|
+
for (const w of checkLineRules(els)) findings.push({ severity: 'warning', code: w.code, message: w.message })
|
|
260
292
|
const outOfSafe = new Set(page.expectedOutOfSafeArea ?? []) // 出界分级声明制(C3 修订)
|
|
261
293
|
const lenient = page.overlapMode === 'lenient'
|
|
262
294
|
const pairKey = (a, b) => [a, b].sort().join(' × ')
|
|
@@ -355,6 +387,25 @@ export function analyzePage(page, size) {
|
|
|
355
387
|
let code = 'overlap'
|
|
356
388
|
let note = ''
|
|
357
389
|
if (ki === 'line' || kj === 'line') {
|
|
390
|
+
// R12:折线的**整体外接框**不是它的绘制区域——绕开盒子的路线,AABB 必然罩住那个盒子 ⇒ 误报
|
|
391
|
+
//(实测:密集状态机里"上方绕行"的退货线 AABB 正好罩住 s3,报出 58.5×54 = 整框面积)。
|
|
392
|
+
// 改为**逐段真实相交**:只有某一段确实穿过对方矩形**内部**才算重叠(贴边不算、绕行不算、穿过才算)。
|
|
393
|
+
const lnEl = ki === 'line' ? els[i] : els[j]
|
|
394
|
+
const otherEl = ki === 'line' ? els[j] : els[i]
|
|
395
|
+
const unt = otherEl.bounds
|
|
396
|
+
const rOther = unt && (Array.isArray(unt) ? { x: unt[0], y: unt[1], w: unt[2], h: unt[3] } : unt)
|
|
397
|
+
const segHitsRect = (pts, r) => {
|
|
398
|
+
if (!r || !Array.isArray(pts)) return true
|
|
399
|
+
for (let k = 0; k + 1 < pts.length; k++) {
|
|
400
|
+
const [ax1, ay1] = pts[k]
|
|
401
|
+
const [ax2, ay2] = pts[k + 1]
|
|
402
|
+
const x0 = Math.min(ax1, ax2); const x1 = Math.max(ax1, ax2)
|
|
403
|
+
const y0 = Math.min(ay1, ay2); const y1 = Math.max(ay1, ay2)
|
|
404
|
+
if (x1 > r.x + 0.5 && x0 < r.x + r.w - 0.5 && y1 > r.y + 0.5 && y0 < r.y + r.h - 0.5) return true
|
|
405
|
+
}
|
|
406
|
+
return false
|
|
407
|
+
}
|
|
408
|
+
if (!segHitsRect(lnEl.points, rOther)) continue
|
|
358
409
|
severity = 'warning'
|
|
359
410
|
note = '—— 连线/箭头跨越,视为引导或标注线'
|
|
360
411
|
} else if (ri === 'content' && rj === 'content') {
|
|
@@ -482,6 +533,23 @@ export function verifyDeck(layout) {
|
|
|
482
533
|
if (f.severity === 'confirmed') continue
|
|
483
534
|
out.push(` - [${f.severity === 'error' ? '✗' : '~'}] ${f.code}|${f.message}`)
|
|
484
535
|
}
|
|
536
|
+
// ── 阶段 A 报告形态(docs/08 §5):结构关系只计数;**声明复核段固定存在**(豁免必须可见)──
|
|
537
|
+
if (findings.structureStats !== undefined) {
|
|
538
|
+
const rel = deriveRelations(page)
|
|
539
|
+
out.push(` · ${summaryLine(rel)}`)
|
|
540
|
+
const decls = Array.isArray(page.expectedOverlaps) ? page.expectedOverlaps : []
|
|
541
|
+
const noReason = decls.filter((d) => !d || typeof d.reason !== 'string' || d.reason.trim() === '').length
|
|
542
|
+
const stale = rel.invalid.filter((iv) => iv.type === 'overlapDecl').length
|
|
543
|
+
// "是否覆盖"必须按**推断**角色判(roleOf 返回的是生效角色,显式优先 ⇒ 直接比会永远相等)
|
|
544
|
+
const inferred = (el) => (['text', 'table', 'chart'].includes(el.type ?? el.kind) ? 'content' : ((el.type === 'line' || el.kind === 'line') ? 'line' : 'background'))
|
|
545
|
+
const overrides = (page.elements ?? []).filter((el) => el.role !== undefined && el.role !== inferred(el))
|
|
546
|
+
const overNoReason = overrides.filter((el) => typeof el.roleReason !== 'string' || el.roleReason.trim() === '')
|
|
547
|
+
const parts = []
|
|
548
|
+
parts.push(decls.length ? `预期重叠声明 ${decls.length} 处${stale ? `(${stale} 处已失效)` : ''}${noReason ? `(${noReason} 处缺 reason)` : ''}` : '预期重叠声明 0 处')
|
|
549
|
+
parts.push(overrides.length ? `role 覆盖 ${overrides.length} 处${overNoReason.length ? `(${overNoReason.length} 处缺 roleReason)` : ''}` : 'role 覆盖 0 处')
|
|
550
|
+
parts.push(`自证:关系全部通过几何反验证${rel.invalid.filter((iv) => iv.type !== 'overlapDecl').length ? `(${rel.invalid.filter((iv) => iv.type !== 'overlapDecl').length} 处不成立已按冲突报)` : ''}`)
|
|
551
|
+
out.push(` · 设计声明复核:${parts.join('|')}`)
|
|
552
|
+
}
|
|
485
553
|
for (const s of suggestions) {
|
|
486
554
|
out.push(` - [·] ${s.code}|${s.message}`)
|
|
487
555
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-ppt-studio",
|
|
3
|
-
"version": "1.1
|
|
3
|
+
"version": "1.2.1",
|
|
4
4
|
"description": "在 DeepSeek Harness 里做 PPT:说需求 → 逐页制作 → 导出可编辑的 .pptx。含布局检查、Office 真渲染复核,以及可扩展的本地模板库",
|
|
5
5
|
"//publish": "2026-09-18 定:发 npm,**无 scope** 包名 `dsh-ppt-studio`。**状态(2026-09-26):已发布** —— `dsh-ppt-studio@1.1.0` 在 npm 上(`dist-tags.latest`、public,发布者 `zbsph`),registry 的 tarball 与 GitHub Release 资产**逐字节相同**(sha256 `9303cc5c…`)。首个版本是 `npm publish <发版 tgz>` 直发的;**从下一版起改走 Trusted Publishing(OIDC)全自动通道**:`.github/workflows/publish.yml` 由 release 事件触发 → 下载 Release 资产 → OIDC `npm publish` → 发后自证 integrity —— 零长期 token、零手工(npm 已公告:Bypass-2FA 直发 2027-01 移除)。发布纪律不变:**用发版时那个 tgz**(`.sync-state.json` 的 `localTgz`),**别在 main 前进后跑裸 `npm publish`**(会按当前工作区重新打包 ⇒ 破坏'两条通道同一份字节')。手动补发入口:Actions → Publish to npm → Run workflow。见 README §10 与 docs/05「发布-同步纪律」。",
|
|
6
6
|
"repository": { "type": "git", "url": "git+https://github.com/zbsph/dsh-ppt-studio.git" },
|
|
@@ -28,7 +28,6 @@
|
|
|
28
28
|
},
|
|
29
29
|
"files": [
|
|
30
30
|
"lib",
|
|
31
|
-
"vendor",
|
|
32
31
|
"templates",
|
|
33
32
|
"docs",
|
|
34
33
|
"skills",
|
|
@@ -60,10 +59,21 @@
|
|
|
60
59
|
}
|
|
61
60
|
},
|
|
62
61
|
"scripts": {
|
|
62
|
+
"verify:package": "node scripts/check-package.mjs",
|
|
63
|
+
"verify:products": "node scripts/verify-products.mjs",
|
|
64
|
+
"verify:handdrawn": "node scripts/verify-handdrawn.mjs",
|
|
65
|
+
"verify:optionprobe": "node scripts/handdrawn-option-probe.mjs",
|
|
66
|
+
"verify:redprobe": "node scripts/red-probe.mjs",
|
|
67
|
+
"verify:linetext": "node scripts/check-line-text.mjs",
|
|
63
68
|
"build": "bash scripts/build.sh",
|
|
64
69
|
"build:node": "node scripts/build.mjs",
|
|
65
70
|
"sync": "node scripts/release-sync.mjs",
|
|
66
|
-
"test": "node scripts/build.mjs && node scripts/check-tracked-lf.mjs && node scripts/smoke.mjs && node scripts/check-preset.mjs",
|
|
71
|
+
"test": "node scripts/build.mjs && node scripts/check-tracked-lf.mjs && node scripts/check-lib-sync.mjs && node scripts/check-unknown-keys.mjs && node scripts/check-removed-traces.mjs && node scripts/check-generators.mjs && node scripts/smoke.mjs && node scripts/check-preset.mjs && node scripts/verify-handdrawn.mjs && node scripts/red-probe.mjs && node scripts/check-line-text.mjs && node scripts/handdrawn-option-probe.mjs",
|
|
72
|
+
"test:fast": "node scripts/test-fast.mjs",
|
|
73
|
+
"test:release": "node scripts/test-release.mjs",
|
|
74
|
+
"test:map": "node scripts/test-map.mjs",
|
|
75
|
+
"check:syntax": "node scripts/check-syntax.mjs",
|
|
76
|
+
"test:map:audit": "node scripts/test-map.mjs --audit",
|
|
67
77
|
"check:eol": "node scripts/check-tracked-lf.mjs",
|
|
68
78
|
"fresh": "node scripts/verify-fresh-install.mjs",
|
|
69
79
|
"test:real": "node scripts/regression-real.mjs",
|
|
@@ -77,6 +87,13 @@
|
|
|
77
87
|
"probe:effect": "node scripts/probe-effect-semantics.mjs",
|
|
78
88
|
"probe:reparse": "node scripts/probe-reparse-safety.mjs",
|
|
79
89
|
"probe:asset": "node scripts/probe-asset-freshness.mjs",
|
|
90
|
+
"probe:prompt": "node scripts/probe-prompt-cost.mjs",
|
|
91
|
+
"probe:prompt:check": "node scripts/probe-prompt-cost.mjs --check",
|
|
92
|
+
"bench:scale": "node scripts/bench-diagram-scale.mjs",
|
|
93
|
+
"verify:relations": "node scripts/verify-diagram-relations.mjs",
|
|
94
|
+
"verify:handwritten": "node scripts/verify-handwritten.mjs",
|
|
95
|
+
"verify:shape": "node scripts/verify-shape.mjs",
|
|
96
|
+
"render:group": "node scripts/render-group.mjs",
|
|
80
97
|
"eval:skills": "node scripts/eval-skills-score.mjs"
|
|
81
98
|
}
|
|
82
99
|
}
|
|
@@ -64,6 +64,8 @@ const vLines = src.verify.split(/\r?\n/)
|
|
|
64
64
|
const codes = new Map()
|
|
65
65
|
vLines.forEach((line, i) => {
|
|
66
66
|
if (/\b(?:let|const|var)\s+code\s*=/.test(line)) return
|
|
67
|
+
// 阶段 A(2026-09-28):本行**显式**写了非 error 的 severity ⇒ 不是错误码(同上,避免被上 3 行的 'error' 误收)
|
|
68
|
+
if (/severity\s*[:=]\s*'(?:warning|confirmed|suggestion|info)'/.test(line)) return
|
|
67
69
|
for (const m of line.matchAll(/code\s*[:=]\s*'([a-z-]+)'/g)) {
|
|
68
70
|
const w = vLines.slice(Math.max(0, i - 3), i + 1).join('\n')
|
|
69
71
|
codes.set(m[1], /'error'/.test(w) ? 'error' : 'warn|suggest')
|
|
@@ -84,8 +86,10 @@ for (const c of ['density', 'hotspot', 'near-align']) {
|
|
|
84
86
|
const conformanceMode = src.verify.match(/severity: mode === 'strict' \? 'error' : 'warning',\s*\n?\s*code: 'theme-conformance'/)
|
|
85
87
|
check('theme-conformance:strict=error / suggest=warning / off=跳过(手册如此描述)', !!conformanceMode, 'verify.js themeConformance(): severity 随 mode 切换;无 colors 自动跳过')
|
|
86
88
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
+
// 阶段 A(2026-09-28)事实更新:line 不再"只支持两点"——>2 点折线已支持(导出为 custGeom 开放路径),
|
|
90
|
+
// 非法点列(<2 点/非数字)仍显式报错。手册必须写新事实。
|
|
91
|
+
const lineValidator = src.schema.match(/至少 2 点、每点两个有限数字/)
|
|
92
|
+
check('line 支持 ≥2 点折线(手册已写明;非法点列仍报错)', !!lineValidator, `schema.js validateLine: ${(lineValidator?.[0] ?? '未找到').slice(0, 60)}`)
|
|
89
93
|
|
|
90
94
|
const tableKeys = src.schema.match(/table: \['cols', 'rows', 'header'\]/)
|
|
91
95
|
check('table 只有 cols/rows/header(手册"没有逐列对齐字段"成立)', !!tableKeys, `schema.js ELEMENT_KEYS: ${tableKeys?.[0] ?? '未找到'}`)
|
|
@@ -177,7 +181,15 @@ const dirG = join(tmp, 'g-3pts-line')
|
|
|
177
181
|
await writeDeck(dirG, [' - elementId: poly', ' elementType: line', ' points: [[10, 10], [50, 30], [90, 10]]'])
|
|
178
182
|
let badLine = null
|
|
179
183
|
try { await resolveDeck(dirG) } catch (e) { badLine = e }
|
|
180
|
-
|
|
184
|
+
// 阶段 A(2026-09-28)事实更新:三点折线**被接受**(导出为 custGeom 开放路径);手册已改写。
|
|
185
|
+
// 同时钉住反面:**一点**折线仍必须报错("≥2 点"这条下限不能丢)。
|
|
186
|
+
const dirG2 = join(tmp, 'g-1pt-line')
|
|
187
|
+
await writeDeck(dirG2, [' - elementId: dot', ' elementType: line', ' points: [[10, 10]]'])
|
|
188
|
+
let onePt = null
|
|
189
|
+
try { await resolveDeck(dirG2) } catch (e) { onePt = e }
|
|
190
|
+
check('运行时:三点折线被接受、一点折线被拒(手册"≥2 点,>2 点为折线"成立)',
|
|
191
|
+
badLine === null && !!onePt && /至少 2 点/.test(onePt.messages?.join(';') ?? ''),
|
|
192
|
+
`三点=${badLine ? `被拒(${(badLine.messages ?? []).join('; ').slice(0, 60)})` : '通过'}|一点=${onePt ? '被拒' : '竟然通过'}`)
|
|
181
193
|
|
|
182
194
|
const dirH = join(tmp, 'h-notes')
|
|
183
195
|
await mkdir(join(dirH, 'pages'), { recursive: true })
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* 150 元素规模基线(阶段 A 硬门槛之一,docs/08 §8):`node scripts/bench-diagram-scale.mjs`
|
|
4
|
+
*
|
|
5
|
+
* 为什么单独一个脚本而不是塞进 smoke:**计时断言在 CI 上必然抖动**(共享 runner、冷启动),
|
|
6
|
+
* 把抖动带进门禁会让"红"失去意义。所以这里做"可复跑的量化基线",把数字写进 docs/02,
|
|
7
|
+
* 由人在批次边界核对;门槛不达标时退出码 1。
|
|
8
|
+
*
|
|
9
|
+
* 门槛(初值,docs/08 §8):verify ≤ 3s|预览 HTML ≤ 1.5MB|单页总处理 ≤ 10s
|
|
10
|
+
*
|
|
11
|
+
* 夹具刻意**用满阶段 A 的新能力**:1 容器 + 40 盒(各自 contains 自己的标签)+ 68 文本 + 20 线
|
|
12
|
+
* (8 条 attach、6 条 3 点折线、6 条双箭头)+ 1 徽章 + 5 组(含一层嵌套)+ 2 条有意重叠声明
|
|
13
|
+
* + 1 处 role 覆盖(带 roleReason)——即"最坏情况"的形状:关系多、交叉检查多、报告段最长。
|
|
14
|
+
*/
|
|
15
|
+
import { performance } from 'node:perf_hooks'
|
|
16
|
+
import { mkdirSync, writeFileSync, readFileSync, rmSync, statSync, readdirSync } from 'node:fs'
|
|
17
|
+
import { join } from 'node:path'
|
|
18
|
+
import { tmpdir } from 'node:os'
|
|
19
|
+
import { resolveDeck } from '../lib/pptd/schema.js'
|
|
20
|
+
import { renderDeck } from '../lib/pptd/render-html.js'
|
|
21
|
+
import { analyzePage, verifyDeck } from '../lib/verify.js'
|
|
22
|
+
import { deriveRelations, checkDiagram } from '../lib/pptd/relations.js'
|
|
23
|
+
import { exportPptx } from '../lib/pptd/export-pptx.js'
|
|
24
|
+
|
|
25
|
+
const WORK = join(tmpdir(), `pptd-bench-scale-${Date.now()}`)
|
|
26
|
+
rmSync(WORK, { recursive: true, force: true })
|
|
27
|
+
mkdirSync(join(WORK, 'pages'), { recursive: true })
|
|
28
|
+
|
|
29
|
+
// ── 造 150 个元素 ────────────────────────────────────────────────────────────
|
|
30
|
+
const COLORS = ['#2563EB', '#0EA5E9', '#F59E0B', '#10B981', '#EF4444', '#7C3AED']
|
|
31
|
+
const els = []
|
|
32
|
+
const groups = []
|
|
33
|
+
const boxIds = []
|
|
34
|
+
let n = 0
|
|
35
|
+
const push = (el) => { els.push(el); n++; return el.elementId }
|
|
36
|
+
|
|
37
|
+
// 1 个容器(覆盖下面所有"内容":盒子/标签/线/注记都在它内部 ⇒ 都能被 contains(含传递闭包)结构豁免)
|
|
38
|
+
push({ elementId: 'zone', elementType: 'shape', kind: 'roundRect', bounds: [20, 20, 920, 500], fill: '#F8FAFC', role: 'background' })
|
|
39
|
+
// 40 个盒子 + 40 个标签(标签贴在盒子内部下缘,盒子声明 contains 自己的标签)
|
|
40
|
+
const lineIds = []
|
|
41
|
+
const noteIds = []
|
|
42
|
+
for (let j = 0; j < 4; j++) {
|
|
43
|
+
const row = []
|
|
44
|
+
for (let i = 0; i < 10; i++) {
|
|
45
|
+
const x = 50 + i * 88
|
|
46
|
+
const y = 45 + j * 105
|
|
47
|
+
const id = `box_${j}_${i}`
|
|
48
|
+
push({ elementId: id, elementType: 'shape', kind: 'roundRect', bounds: [x, y, 76, 56], fill: COLORS[(i + j) % COLORS.length], line: { color: '#1F2937', width: 1 } })
|
|
49
|
+
push({ elementId: `lbl_${j}_${i}`, elementType: 'text', bounds: [x + 4, y + 30, 68, 22], content: { text: `节点${j}${i}`, style: 'body' }, role: 'content' })
|
|
50
|
+
// 盒子包含自己的标签(结构关系 ⇒ 盒子×标签 的重叠被豁免,而不是逐对写 expectedOverlaps)
|
|
51
|
+
// 注意:push 了两个元素之后,**盒子**的数组下标是 length-2(length-3 会指到上一个标签——写夹具时踩过)
|
|
52
|
+
els[els.length - 2].contains = [`lbl_${j}_${i}`]
|
|
53
|
+
row.push(id)
|
|
54
|
+
boxIds.push(id)
|
|
55
|
+
}
|
|
56
|
+
groups.push({ id: `g_row${j}`, label: `第 ${j} 行`, members: row })
|
|
57
|
+
}
|
|
58
|
+
groups.push({ id: 'g_all', label: '整块', members: ['g_row0', 'g_row1', 'g_row2', 'g_row3'] })
|
|
59
|
+
|
|
60
|
+
// 20 条线:8 条 attach(行内相邻盒子右→左)、6 条 3 点折线(落在盒子行之间的水平间隙里)、6 条双箭头
|
|
61
|
+
for (let k = 0; k < 8; k++) {
|
|
62
|
+
const j = k % 4
|
|
63
|
+
const i = k % 9
|
|
64
|
+
const x1 = 50 + i * 88 + 76
|
|
65
|
+
const x2 = 50 + (i + 1) * 88
|
|
66
|
+
const y = 45 + j * 105 + 28
|
|
67
|
+
const id = `ln_a${k}`
|
|
68
|
+
push({ elementId: id, elementType: 'line', points: [[x1, y], [x2, y]], arrow: 'end', attach: { from: { ref: `box_${j}_${i}`, side: 'right' }, to: { ref: `box_${j}_${i + 1}`, side: 'left' } }, line: { color: '#1F2937', width: 1 } })
|
|
69
|
+
lineIds.push(id)
|
|
70
|
+
}
|
|
71
|
+
// 折线落在盒子左侧外沿(x=40..45,盒子从 x=50 起 ⇒ 不与盒子相交),高度取行间间隙(不与盒子重叠)
|
|
72
|
+
for (let k = 0; k < 6; k++) {
|
|
73
|
+
const y = [128, 140, 233, 245, 338, 350][k]
|
|
74
|
+
const id = `ln_p${k}`
|
|
75
|
+
push({ elementId: id, elementType: 'line', points: [[40, y], [45, y], [45, y - 6]], arrow: 'both', line: { color: '#7C3AED', width: 1, dash: 'dash' } })
|
|
76
|
+
lineIds.push(id)
|
|
77
|
+
}
|
|
78
|
+
for (let k = 0; k < 6; k++) {
|
|
79
|
+
const x = 60 + k * 140
|
|
80
|
+
const id = `ln_b${k}`
|
|
81
|
+
// y=423:落在最后一行盒子(底 416)与注记带(430 起)之间的空隙、容器内 ⇒ 与两者都不相交
|
|
82
|
+
push({ elementId: id, elementType: 'line', points: [[x, 423], [x + 60, 423]], arrow: 'both', line: { color: '#EF4444', width: 1 } })
|
|
83
|
+
lineIds.push(id)
|
|
84
|
+
}
|
|
85
|
+
// 1 个徽章(压在容器顶边,声明归属)
|
|
86
|
+
push({ elementId: 'chip', elementType: 'shape', kind: 'roundRect', bounds: [60, 6, 120, 28], fill: '#1F2937', badgeOf: 'zone', roleReason: '标题胶囊压在容器顶边' })
|
|
87
|
+
// 注记补齐到 150:12 列 × 4 行网格(宽 70 < 间距 76 ⇒ 互不重叠;全在容器内 ⇒ 可被 contains 传递闭包豁免)
|
|
88
|
+
for (let r = 0; r < 4; r++) {
|
|
89
|
+
for (let i = 0; i < 12; i++) {
|
|
90
|
+
const id = `note_${r}_${i}`
|
|
91
|
+
push({ elementId: id, elementType: 'text', bounds: [30 + i * 76, 430 + r * 22, 70, 22], content: { text: `注${r}${i}`, style: 'body' } })
|
|
92
|
+
noteIds.push(id)
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
// 容器包含**所有**内部内容(盒子、线、注记)——这才是"容器"的语义;标签由各自的盒子声明
|
|
96
|
+
els.find((e) => e.elementId === 'zone').contains = [...boxIds, ...lineIds, ...noteIds]
|
|
97
|
+
|
|
98
|
+
const pageYaml = ['pageType: content', 'groups:', ...groups.flatMap((g) => [` - id: ${g.id}`, ` label: ${g.label}`, ` members: [${g.members.join(', ')}]`]),
|
|
99
|
+
'expectedOverlaps:', ' - pair: [chip, zone]', ' reason: "徽章压在容器顶边(设计意图)"',
|
|
100
|
+
'elements:', ...els.map((e) => {
|
|
101
|
+
const lines = [` - elementId: ${e.elementId}`, ` elementType: ${e.elementType}`]
|
|
102
|
+
if (e.bounds) lines.push(` bounds: [${e.bounds.join(', ')}]`)
|
|
103
|
+
if (e.kind) lines.push(` kind: ${e.kind}`)
|
|
104
|
+
if (e.fill) lines.push(` fill: "${e.fill}"`)
|
|
105
|
+
if (e.line) lines.push(` line: {color: "${e.line.color}", width: ${e.line.width}${e.line.dash ? `, dash: ${e.line.dash}` : ''}}`)
|
|
106
|
+
if (e.points) lines.push(` points: [${e.points.map((p) => `[${p.join(', ')}]`).join(', ')}]`)
|
|
107
|
+
if (e.arrow) lines.push(` arrow: ${typeof e.arrow === 'string' ? `'${e.arrow}'` : e.arrow}`)
|
|
108
|
+
if (e.attach) lines.push(` attach: {from: {ref: ${e.attach.from.ref}, side: ${e.attach.from.side}}, to: {ref: ${e.attach.to.ref}, side: ${e.attach.to.side}}}`)
|
|
109
|
+
if (e.role) lines.push(` role: ${e.role}`)
|
|
110
|
+
if (e.roleReason) lines.push(` roleReason: "${e.roleReason}"`)
|
|
111
|
+
if (e.badgeOf) lines.push(` badgeOf: ${e.badgeOf}`)
|
|
112
|
+
if (e.contains) lines.push(` contains: [${e.contains.join(', ')}]`)
|
|
113
|
+
if (e.content) lines.push(` content: {text: "${e.content.text}", style: ${e.content.style}}`)
|
|
114
|
+
return lines.join('\n')
|
|
115
|
+
}), ''].join('\n')
|
|
116
|
+
writeFileSync(join(WORK, 'deck.yaml'), ['version: 1', 'title: bench-150', 'size: [960, 540]', 'theme:',
|
|
117
|
+
` colors: {primary: "#2563EB", text: "#1F2937"${COLORS.map((c, i) => `, c${i}: "${c}"`).join('')}}`, ' textStyles:',
|
|
118
|
+
' body: {fontSize: 12, color: "$text"}', 'pages:', ' - pages/01.yaml', ''].join('\n'))
|
|
119
|
+
writeFileSync(join(WORK, 'pages', '01.yaml'), pageYaml)
|
|
120
|
+
|
|
121
|
+
// ── 计时 ────────────────────────────────────────────────────────────────────
|
|
122
|
+
const t = async (fn) => { const t0 = performance.now(); const v = await fn(); return { ms: performance.now() - t0, v } }
|
|
123
|
+
const out = {}
|
|
124
|
+
const size = { width: 960, height: 540 }
|
|
125
|
+
|
|
126
|
+
const A = await t(() => resolveDeck(WORK)); out.resolveDeck = A.ms
|
|
127
|
+
const ctx = A.v
|
|
128
|
+
// 注意:renderDeck 的 `out` 是**相对 deck 目录**的相对路径(与 exportPptx 的绝对路径语义不同)
|
|
129
|
+
const B = await t(() => renderDeck(ctx, { out: 'preview' })); out.renderDeck = B.ms
|
|
130
|
+
const previewDir = join(WORK, 'preview')
|
|
131
|
+
const layoutPath = join(previewDir, 'layout.json')
|
|
132
|
+
const layout = JSON.parse(readFileSync(layoutPath, 'utf8'))
|
|
133
|
+
const page = layout.pages[0]
|
|
134
|
+
const C = await t(() => analyzePage(page, size)); out.analyzePage = C.ms
|
|
135
|
+
const D = await t(() => deriveRelations(page)); out.deriveRelations = D.ms
|
|
136
|
+
const E = await t(() => checkDiagram(page)); out.checkDiagram = E.ms
|
|
137
|
+
const F = await t(() => verifyDeck(layout)); out.verifyDeck = F.ms
|
|
138
|
+
const G = await t(() => exportPptx(ctx, { out: join(WORK, 'out.pptx') })); out.exportPptx = G.ms
|
|
139
|
+
const htmlBytes = readdirSync(previewDir).filter((f) => f.endsWith('.html'))
|
|
140
|
+
.reduce((a, f) => a + statSync(join(previewDir, f)).size, 0)
|
|
141
|
+
out.previewHtmlBytes = htmlBytes
|
|
142
|
+
out.totalMs = out.resolveDeck + out.renderDeck + out.analyzePage + out.verifyDeck + out.exportPptx
|
|
143
|
+
|
|
144
|
+
const TH = { verify: 3000, html: 1.5 * 1024 * 1024, total: 10000 }
|
|
145
|
+
const checks = [
|
|
146
|
+
['元素数 = 150(夹具自证)', layout.pages[0].elements.length === 150, `elements=${layout.pages[0].elements.length}`],
|
|
147
|
+
['关系推导无错误(夹具自证:结构声明全部成立)', D.v.errors.length === 0, D.v.errors.length ? `${D.v.errors.length} 条,前 3 条:${D.v.invalid.slice(0, 3).map((x) => `${x.type}:${x.from}→${x.to}:${x.why}`).join('|')}` : '无'],
|
|
148
|
+
['结构关系被真的用到(组/包含/附着/徽章)', D.v.stats.groups === 5 && D.v.stats.contains >= 40 && D.v.stats.attach === 16 && D.v.stats.badge === 1, JSON.stringify(D.v.stats)],
|
|
149
|
+
[`verify 用时 ≤ ${TH.verify}ms`, out.verifyDeck <= TH.verify, `${out.verifyDeck.toFixed(1)}ms`],
|
|
150
|
+
[`预览 HTML ≤ ${(TH.html / 1024 / 1024).toFixed(1)}MB`, htmlBytes <= TH.html, `${(htmlBytes / 1024).toFixed(0)}KB`],
|
|
151
|
+
[`单页总处理 ≤ ${TH.total}ms`, out.totalMs <= TH.total, `${out.totalMs.toFixed(1)}ms`],
|
|
152
|
+
]
|
|
153
|
+
let fail = 0
|
|
154
|
+
console.log('==== 150 元素规模基线(阶段 A 硬门槛)====')
|
|
155
|
+
for (const [name, ok, detail] of checks) { if (!ok) fail++; console.log(`${ok ? '✓' : '✗'} ${name} — ${detail}`) }
|
|
156
|
+
console.log('\n耗时明细(ms): ' + JSON.stringify(Object.fromEntries(Object.entries(out).filter(([k]) => k.endsWith('Ms') || k === 'htmlBytes').map(([k, v]) => [k, typeof v === 'number' ? Math.round(v * 10) / 10 : v]))))
|
|
157
|
+
console.log('报告规模(行数):', F.v.text.split('\n').length, '|错误/警告:', F.v.errors.length, '/', F.v.warns.length)
|
|
158
|
+
// 错误码直方图(基线必须能解释每一个错误:干净夹具应当趋近 0,否则说明夹具自身有真冲突)
|
|
159
|
+
const hist = (list) => [...list.reduce((m, f) => m.set(f.code, (m.get(f.code) ?? 0) + 1), new Map())]
|
|
160
|
+
.sort((a, b) => b[1] - a[1]).slice(0, 6).map(([c, n]) => `${c}×${n}`).join('|')
|
|
161
|
+
console.log('错误码前 6:', hist(F.v.errors) || '无')
|
|
162
|
+
console.log('警告码前 6:', hist(F.v.warns) || '无')
|
|
163
|
+
rmSync(WORK, { recursive: true, force: true })
|
|
164
|
+
if (fail) { console.log(`\n✗ ${fail} 项未达门槛——按 docs/08 §8 需先优化再进入下一阶段`); process.exit(1) }
|
|
165
|
+
console.log('\n✓ 全部达标')
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* 生成器检查网:`examples/hand-drawn/gen/*.cjs` 全部过 `node --check`。
|
|
4
|
+
*
|
|
5
|
+
* 为什么需要:生成器**不在** `npm test` 网内 ⇒ 一个语法/TDZ 级错误(实测:参数化时写成
|
|
6
|
+
* `… || PPTX` 自引用 ⇒ 缺省路径必崩)能躲过所有测试,直到某次真跑才炸。
|
|
7
|
+
* 这里只做静态检查(快、无副作用);缺省路径的**运行时**检查由 `verify:handdrawn` 等覆盖。
|
|
8
|
+
*/
|
|
9
|
+
import { execFileSync } from 'node:child_process'
|
|
10
|
+
import { readdirSync } from 'node:fs'
|
|
11
|
+
import { join } from 'node:path'
|
|
12
|
+
|
|
13
|
+
const ROOT = new URL('..', import.meta.url).pathname.replace(/^\/([A-Za-z]:)/, '$1')
|
|
14
|
+
const DIR = join(ROOT, 'examples', 'hand-drawn', 'gen')
|
|
15
|
+
const files = readdirSync(DIR).filter((f) => f.endsWith('.cjs')).sort()
|
|
16
|
+
let bad = 0
|
|
17
|
+
for (const f of files) {
|
|
18
|
+
try { execFileSync('node', ['--check', join(DIR, f)], { stdio: 'pipe' }) }
|
|
19
|
+
catch (e) { bad++; console.log(' ✗ 语法错误:' + f + '\n' + String(e.stderr ?? '').split('\n').slice(0, 4).join('\n')) }
|
|
20
|
+
}
|
|
21
|
+
console.log('生成器检查网:' + files.length + ' 个脚本|语法错 ' + bad + ' 个' + (bad === 0 ? ' ✓' : ''))
|
|
22
|
+
process.exit(bad === 0 ? 0 : 1)
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* lib ⟷ src 一致性守卫。
|
|
4
|
+
* 为什么必须有:`package.json` 的 files 白名单**只发 lib**(main = ./lib/index.js),且没有 prepack 钩子
|
|
5
|
+
* ⇒ 一旦 lib 与 src 不同步(漏 build / 半途改 src),**装出来的包就是错的**。
|
|
6
|
+
* 判据:文件集合相同 + 逐字节相同。
|
|
7
|
+
*/
|
|
8
|
+
import { readdirSync, readFileSync } from 'node:fs'
|
|
9
|
+
import { join } from 'node:path'
|
|
10
|
+
import { createHash } from 'node:crypto'
|
|
11
|
+
|
|
12
|
+
const ROOT = new URL('..', import.meta.url).pathname.replace(/^\/([A-Za-z]:)/, '$1')
|
|
13
|
+
const walk = (base) => {
|
|
14
|
+
const out = []
|
|
15
|
+
const rec = (dir, rel) => {
|
|
16
|
+
for (const e of readdirSync(dir, { withFileTypes: true })) {
|
|
17
|
+
const r = rel ? rel + '/' + e.name : e.name
|
|
18
|
+
if (e.isDirectory()) rec(join(dir, e.name), r)
|
|
19
|
+
else out.push(r)
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
rec(join(ROOT, base), '')
|
|
23
|
+
return out.sort()
|
|
24
|
+
}
|
|
25
|
+
const sha = (p) => createHash('sha256').update(readFileSync(p)).digest('hex')
|
|
26
|
+
const a = walk('src'), b = walk('lib')
|
|
27
|
+
const onlySrc = a.filter((f) => !b.includes(f))
|
|
28
|
+
const onlyLib = b.filter((f) => !a.includes(f))
|
|
29
|
+
const differ = a.filter((f) => b.includes(f) && sha(join(ROOT, 'src', f)) !== sha(join(ROOT, 'lib', f)))
|
|
30
|
+
console.log('lib ⟷ src:src ' + a.length + ' 个 / lib ' + b.length + ' 个|仅 src ' + onlySrc.length + '|仅 lib ' + onlyLib.length + '|字节不同 ' + differ.length)
|
|
31
|
+
for (const f of [...onlySrc.map((x) => '仅 src: ' + x), ...onlyLib.map((x) => '仅 lib: ' + x), ...differ.map((x) => '字节不同: ' + x)].slice(0, 12)) console.log(' · ' + f)
|
|
32
|
+
if (onlySrc.length || onlyLib.length || differ.length) { console.log('✗ lib 与 src 不一致 ⇒ 先跑 node scripts/build.mjs 并提交(files 只发 lib,装出来会是错的)'); process.exit(1) }
|
|
33
|
+
console.log('✓ lib 与 src 完全一致(files 只发 lib ⇒ 这一步守住"装出来等于仓库")')
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 线 × 文字检查的单元证据(先红后绿)
|
|
3
|
+
* ① 穿过文字 ⇒ 必须报 line-cross-text
|
|
4
|
+
* ② 不穿过 ⇒ 必须沉默
|
|
5
|
+
* ③ 声明刻意穿过(roleReason) ⇒ 必须沉默(豁免有效)
|
|
6
|
+
* 用法:node scripts/check-line-text.mjs
|
|
7
|
+
*/
|
|
8
|
+
import { checkLineText } from '../src/pptd/line-text-rule.js'
|
|
9
|
+
|
|
10
|
+
const line = { elementType: 'line', elementId: 'life', points: [[100, 0], [100, 100]] }
|
|
11
|
+
const over = { elementType: 'text', elementId: 'lab', bounds: [80, 40, 60, 24] }
|
|
12
|
+
const clear = { elementType: 'text', elementId: 'lab2', bounds: [200, 40, 60, 24] }
|
|
13
|
+
|
|
14
|
+
const a = checkLineText([line, over])
|
|
15
|
+
const b = checkLineText([line, clear])
|
|
16
|
+
const c = checkLineText([{ ...line, roleReason: '刻意穿过示意' }, over])
|
|
17
|
+
|
|
18
|
+
const rows = [
|
|
19
|
+
['① 穿过文字 ⇒ 报错', a.length === 1 && a[0].code === 'line-cross-text', a.length ? a[0].message : '(未报)'],
|
|
20
|
+
['② 不穿过 ⇒ 沉默', b.length === 0, '命中 ' + b.length],
|
|
21
|
+
['③ 声明刻意穿过 ⇒ 沉默', c.length === 0, '命中 ' + c.length],
|
|
22
|
+
]
|
|
23
|
+
let bad = 0
|
|
24
|
+
console.log('线×文字检查单元证据:')
|
|
25
|
+
for (const [name, ok, detail] of rows) {
|
|
26
|
+
if (!ok) bad++
|
|
27
|
+
console.log(' ' + (ok ? '✓' : '✗') + ' ' + name.padEnd(22) + ' ' + detail)
|
|
28
|
+
}
|
|
29
|
+
console.log('==== ' + (bad === 0 ? '全部通过' : bad + ' 项未通过') + ' ====')
|
|
30
|
+
process.exit(bad === 0 ? 0 : 1)
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* 打包与安装一致性(P3)。本机专用,不进 npm test(要 npm pack + 真实安装)。
|
|
4
|
+
* ① npm pack 出 tarball ⇒ 断言必含发行所需目录/文件;
|
|
5
|
+
* ② 装到干净临时目录 ⇒ require 入口可用;
|
|
6
|
+
* ③ 扫描装出的树 ⇒ 不得含已移除方案痕迹;手画回归语料不入包。
|
|
7
|
+
* 注:Windows 上 npm/tar 是垫片 ⇒ 一律 execSync(shell),不用 execFileSync('npm')(会 ENOENT)。
|
|
8
|
+
*/
|
|
9
|
+
import { execSync } from 'node:child_process'
|
|
10
|
+
import { readdirSync, readFileSync, writeFileSync, statSync, mkdtempSync, existsSync } from 'node:fs'
|
|
11
|
+
import { join } from 'node:path'
|
|
12
|
+
import { tmpdir } from 'node:os'
|
|
13
|
+
|
|
14
|
+
const ROOT = new URL('..', import.meta.url).pathname.replace(/^\/([A-Za-z]:)/, '$1')
|
|
15
|
+
const tmp = mkdtempSync(join(tmpdir(), 'pptd-pkg-'))
|
|
16
|
+
const MUST = ['package/package.json', 'package/README.md', 'package/cordis.patch.yml']
|
|
17
|
+
const MUSTDIR = ['package/lib', 'package/skills', 'package/agent-presets', 'package/docs', 'package/templates']
|
|
18
|
+
const TERM = /图族|diagram-families|diagram-ir|FAMILIES|d1_|diagram:|diagram\.style|layoutDiagram|validateDiagram|图编译/
|
|
19
|
+
|
|
20
|
+
console.log('… ① npm pack')
|
|
21
|
+
const out = execSync('npm pack --pack-destination "' + tmp + '"', { cwd: ROOT, encoding: 'utf8' }).trim()
|
|
22
|
+
const tgz = join(tmp, out.split('\n').pop().trim())
|
|
23
|
+
console.log(' ' + tgz.split(/[\\/]/).pop() + '(' + Math.round(statSync(tgz).size / 1024) + ' KB)')
|
|
24
|
+
const list = execSync('tar -tzf "' + tgz + '"', { encoding: 'utf8' }).replace(/\r/g, '').split('\n').filter(Boolean)
|
|
25
|
+
let bad = 0
|
|
26
|
+
for (const m of MUST) if (!list.includes(m)) { console.log(' ✗ 缺 ' + m); bad++ }
|
|
27
|
+
for (const d of MUSTDIR) if (!list.some((l) => l.startsWith(d + '/'))) { console.log(' ✗ 缺目录 ' + d); bad++ }
|
|
28
|
+
if (list.some((l) => l.startsWith('package/examples/hand-drawn'))) { console.log(' ✗ 手画回归语料竟被打包'); bad++ }
|
|
29
|
+
if (list.some((l) => l.startsWith('package/src/'))) { console.log(' ✗ src 被打包(应只发 lib)'); bad++ }
|
|
30
|
+
console.log(' 包内文件 ' + list.length + ' 个|必需项 ' + (bad ? '✗ ' + bad + ' 项不符' : '✓ 全部存在'))
|
|
31
|
+
if (bad) process.exit(1)
|
|
32
|
+
|
|
33
|
+
console.log('… ② 装到干净临时目录并 require 入口')
|
|
34
|
+
const inst = join(tmp, 'inst')
|
|
35
|
+
execSync('mkdir "' + inst + '"', { shell: 'cmd.exe' })
|
|
36
|
+
writeFileSync(join(inst, 'package.json'), '{"name":"pkgtest","private":true}\n')
|
|
37
|
+
execSync('npm install --no-save --silent "' + tgz + '"', { cwd: inst, stdio: 'inherit' })
|
|
38
|
+
const entry = join(inst, 'node_modules', 'dsh-ppt-studio', 'lib', 'index.js')
|
|
39
|
+
if (!existsSync(entry)) { console.log(' ✗ 入口不存在:' + entry); process.exit(1) }
|
|
40
|
+
const keys = execSync('node -e "const m=require(process.argv[1]);console.log(Object.keys(m).join(String.fromCharCode(32)))" "' + entry + '"', { encoding: 'utf8' }).trim()
|
|
41
|
+
const kn = keys ? keys.split(/\s+/).length : 0
|
|
42
|
+
console.log(' ✓ require 可用,导出 ' + kn + ' 项:' + (keys ? keys.split(/\s+/).slice(0, 10).join(' ') : '') + (kn > 10 ? ' …' : ''))
|
|
43
|
+
|
|
44
|
+
console.log('… ③ 装出的树里不得含已移除方案的痕迹')
|
|
45
|
+
const hits = []
|
|
46
|
+
const walk = (d) => {
|
|
47
|
+
for (const e of readdirSync(d, { withFileTypes: true })) {
|
|
48
|
+
const p = join(d, e.name)
|
|
49
|
+
if (/\.(png|jpg|pptx|tgz|zip)$/.test(e.name)) continue
|
|
50
|
+
if (e.name === 'check-package.mjs' || e.name === 'check-removed-traces.mjs') continue // 本文件自带该词表(检查器自指), 不算痕迹
|
|
51
|
+
if (e.isDirectory()) walk(p)
|
|
52
|
+
else if (/\.(js|mjs|cjs|md|ya?ml|json|txt)$/.test(e.name)) {
|
|
53
|
+
readFileSync(p, 'utf8').split('\n').forEach((l, i) => { if (TERM.test(l)) hits.push(p.replace(inst + '/', '') + ':' + (i + 1)) })
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
walk(join(inst, 'node_modules', 'dsh-ppt-studio'))
|
|
58
|
+
console.log(' 命中 ' + hits.length + ' 处' + (hits.length ? '(' + hits.slice(0, 5).join(', ') + ')' : ' ✓'))
|
|
59
|
+
if (hits.length) process.exit(1)
|
|
60
|
+
console.log('==== 打包与安装一致性:通过 ====')
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* 可见文本痕迹守卫:仓库里不得残留"已移除的开发期方案"的任何字样(含根目录、templates、agent-presets 等)。
|
|
4
|
+
*
|
|
5
|
+
* 为什么固化成脚本:这条判据此前用的是临时脚本,且一度只扫 6 个目录、还出现过"检查器扫到自己"的假阳性。
|
|
6
|
+
* 允许清单(**唯一**允许出现这些字样之处,且都在代码里作为**禁词表**本身):
|
|
7
|
+
* · scripts/check-removed-traces.mjs(本文件)
|
|
8
|
+
* · scripts/check-package.mjs(打包检查的词表)
|
|
9
|
+
* 用法:node scripts/check-removed-traces.mjs
|
|
10
|
+
*/
|
|
11
|
+
import { readdirSync, readFileSync } 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 TERM = /图族|diagram-families|diagram-ir|FAMILIES|d1_|diagram:|diagram\.style|layoutDiagram|validateDiagram|图编译/
|
|
16
|
+
const ALLOW = new Set(['scripts/check-removed-traces.mjs', 'scripts/check-package.mjs'])
|
|
17
|
+
const SKIPDIR = /(^|\/)(\.git|node_modules|preview|\.tmp[^/]*)$/
|
|
18
|
+
const EXT = /\.(js|mjs|cjs|md|ya?ml|json|txt|yml)$/
|
|
19
|
+
|
|
20
|
+
const hits = []
|
|
21
|
+
const walk = (rel) => {
|
|
22
|
+
for (const e of readdirSync(join(ROOT, rel), { withFileTypes: true })) {
|
|
23
|
+
const r = rel ? rel + '/' + e.name : e.name
|
|
24
|
+
if (SKIPDIR.test(r)) continue
|
|
25
|
+
if (e.isDirectory()) { walk(r); continue }
|
|
26
|
+
if (!EXT.test(e.name)) continue
|
|
27
|
+
if (ALLOW.has(r)) continue
|
|
28
|
+
const p = join(ROOT, r)
|
|
29
|
+
let t = ''
|
|
30
|
+
try { t = readFileSync(p, 'utf8') } catch { continue }
|
|
31
|
+
t.split('\n').forEach((l, i) => { if (TERM.test(l)) hits.push(r + ':' + (i + 1) + ' ' + l.trim().slice(0, 80)) })
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
walk('')
|
|
35
|
+
console.log('可见文本痕迹扫描:命中 ' + hits.length + ' 处' + (hits.length ? '' : ' ✓'))
|
|
36
|
+
for (const h of hits.slice(0, 12)) console.log(' · ' + h)
|
|
37
|
+
process.exit(hits.length ? 1 : 0)
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* 语法体检(廉价跨切面守卫):`node scripts/check-syntax.mjs`
|
|
4
|
+
*
|
|
5
|
+
* 为什么单列一条:本会话在 `SCHEMA_REF`(模板字符串)里写**行内反引号**踩了四次——
|
|
6
|
+
* 每次都是"文件直接解析失败 ⇒ 全链路崩",但只看 exit code 容易漏读报错内容。
|
|
7
|
+
* 这条守卫把"所有源文件至少能解析"变成 1–2 秒内可得的确定结论。
|
|
8
|
+
*
|
|
9
|
+
* 覆盖:`src/**\/*.js`、`lib/**\/*.js`、`scripts/*.mjs`(跳过 node_modules 与临时目录)。
|
|
10
|
+
*/
|
|
11
|
+
import { readdirSync, statSync } from 'node:fs'
|
|
12
|
+
import { join, extname, relative } from 'node:path'
|
|
13
|
+
import { execFileSync } from 'node:child_process'
|
|
14
|
+
|
|
15
|
+
const root = join(import.meta.dirname, '..')
|
|
16
|
+
|
|
17
|
+
function walk(dir, out = []) {
|
|
18
|
+
for (const name of readdirSync(dir).sort()) {
|
|
19
|
+
if (name === 'node_modules' || name.startsWith('.')) continue
|
|
20
|
+
const p = join(dir, name)
|
|
21
|
+
const st = statSync(p)
|
|
22
|
+
if (st.isDirectory()) walk(p, out)
|
|
23
|
+
else if (['.js', '.mjs'].includes(extname(name))) out.push(p)
|
|
24
|
+
}
|
|
25
|
+
return out
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const files = [...walk(join(root, 'src')), ...walk(join(root, 'lib')), ...walk(join(root, 'scripts'))]
|
|
29
|
+
const bad = []
|
|
30
|
+
for (const f of files) {
|
|
31
|
+
try {
|
|
32
|
+
execFileSync(process.execPath, ['--check', f], { stdio: 'pipe' })
|
|
33
|
+
} catch (e) {
|
|
34
|
+
const msg = String(e.stderr ?? e.message ?? '').split('\n').filter((l) => l.trim()).slice(0, 3).join(' / ')
|
|
35
|
+
bad.push(`${relative(root, f)}: ${msg.slice(0, 200)}`)
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
if (bad.length) {
|
|
39
|
+
console.log(`✗ 语法体检:${bad.length} 个文件解析失败`)
|
|
40
|
+
for (const b of bad) console.log(` · ${b}`)
|
|
41
|
+
console.log(' 提示:模板字符串里出现行内反引号是历史高频坑(SCHEMA_REF 等长文本块)。')
|
|
42
|
+
process.exit(1)
|
|
43
|
+
}
|
|
44
|
+
console.log(`✓ 语法体检:${files.length} 个源文件全部通过 node --check`)
|