dsh-ppt-studio 1.1.5 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +149 -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 +37 -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 +183 -12
- 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-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/diagram-drawing/SKILL.md +23 -0
- package/skills/diagram-drawing/capability-matrix.md +22 -0
- package/skills/diagram-drawing/composition.md +22 -0
- package/skills/diagram-drawing/mechanisms.md +141 -0
- package/skills/diagram-drawing/pitfalls.md +89 -0
- package/skills/diagram-drawing/routing.md +17 -0
- package/skills/diagram-drawing/text-and-spacing.md +16 -0
- package/skills/diagram-drawing/verify.md +50 -0
- package/skills/diagrams/compare.md +24 -0
- package/skills/diagrams/flow.md +24 -0
- package/skills/diagrams/funnel.md +25 -0
- package/skills/diagrams/layers.md +18 -0
- package/skills/diagrams/loop.md +25 -0
- package/skills/diagrams/matrix.md +23 -0
- package/skills/diagrams/sequence.md +17 -0
- package/skills/diagrams/state.md +34 -0
- package/skills/diagrams/steps-ring.md +24 -0
- package/skills/diagrams/swimlane.md +23 -0
- package/skills/diagrams/timeline.md +24 -0
- package/skills/diagrams/tree.md +24 -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,162 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 测试影响面映射(2026-09-27 新增,用户要求"轻量化测试")——**改了什么 ⇒ 该跑哪些**。
|
|
3
|
+
*
|
|
4
|
+
* 背景与代价(实测,2026-09-27):
|
|
5
|
+
* smoke 全量 311.8s(313 条 / 48 节)· e2e 146.3s · 隔离矩阵 61.7s · 原生图表 48.3s · 损失审计 48.2s
|
|
6
|
+
* · LF 4.6s · 手册事实审计 0.6s · lib 新鲜度 0.1s · 预设漂移 0.1s ⇒ 全部约 621.7s(10.4 分钟)
|
|
7
|
+
* 所以按改动选面:**廉价跨切面守卫永远全跑**(它们抓的正是"改 A 文件、断言在 B 文件"),
|
|
8
|
+
* 重活(真渲染/真装包/真跑 python)默认不进快测。
|
|
9
|
+
*
|
|
10
|
+
* 三条安全设计(缺一条就会从提效变埋雷):
|
|
11
|
+
* ① fail-safe:改动文件没映射到任何东西 ⇒ 视为"影响面未知",由调用方跑全量(不是跑零条)。
|
|
12
|
+
* ② 可解释:`--explain` 打印选中了哪些节/脚本、为什么、跳过了什么。
|
|
13
|
+
* ③ 可审计:`--audit` 保证每个 smoke 分节至少被一条规则覆盖,防"孤儿分节永远不跑"。
|
|
14
|
+
*/
|
|
15
|
+
import { readFileSync } from 'node:fs'
|
|
16
|
+
import { join, relative, sep } from 'node:path'
|
|
17
|
+
import { pathToFileURL } from 'node:url'
|
|
18
|
+
|
|
19
|
+
/** 仓库根(本文件在 scripts/ 下)。 */
|
|
20
|
+
export const root = join(import.meta.dirname, '..')
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* 分节分类规则:按 smoke.mjs 里的节标题自动打标签(不手工维护 48 条数据 ⇒ 新增节不会漏)。
|
|
24
|
+
* 顺序敏感:先匹配到的标签先生效;一条规则可给多个标签。
|
|
25
|
+
*/
|
|
26
|
+
const TAG_RULES = [
|
|
27
|
+
// 阶段 A(2026-09-28):结构声明/复杂图地基。放在最前,避免被下面的通用词表抢先打散。
|
|
28
|
+
{ re: /结构声明|复杂图|diagram|折线|attach|豁免/i, tags: ['diagram', 'relations'] },
|
|
29
|
+
{ re: /chart|图表|原生/i, tags: ['chart'] },
|
|
30
|
+
{ re: /python-pptx|pptxgenjs|兜底|引擎/i, tags: ['engine', 'fallback'] },
|
|
31
|
+
{ re: /导出|export|parity|连线|媒体|讲稿/i, tags: ['export'] },
|
|
32
|
+
{ re: /导入|import|手术|splice|patch|模板|template/i, tags: ['import', 'template'] },
|
|
33
|
+
{ re: /预览|preview|路由|webServer|shot|截图/i, tags: ['preview', 'render'] },
|
|
34
|
+
{ re: /真渲染|Office|渲染/i, tags: ['render'] },
|
|
35
|
+
{ re: /技能|手册|文档|事实|漂移/i, tags: ['docs', 'skill'] },
|
|
36
|
+
{ re: /预设|preset|隔离|作用域|bundle|安装|install|npm|发布/i, tags: ['preset', 'install'] },
|
|
37
|
+
{ re: /verify|门禁|断言|schema|速查/i, tags: ['verify'] },
|
|
38
|
+
{ re: /媒体|图片|image|roundRect/i, tags: ['media'] },
|
|
39
|
+
{ re: /文本|字号|字体|排版|美学|层叠|重叠/i, tags: ['layout'] },
|
|
40
|
+
{ re: /用户目录|DSH_HOME|隔离|测试隔离/i, tags: ['home', 'isolation'] },
|
|
41
|
+
// 以下几条是 `--audit` 抓出来的"曾经选不中"的分节(2026-09-27):标题措辞不在上面的词表里
|
|
42
|
+
{ re: /line bounds|safeArea|缩字|对比度|孤字|绝对路径/i, tags: ['layout', 'verify'] },
|
|
43
|
+
{ re: /prst|几何|custGeom|渐变/i, tags: ['export'] },
|
|
44
|
+
{ re: /静默降级|状态文件|质量档/i, tags: ['verify', 'engine'] },
|
|
45
|
+
{ re: /出界|分级/i, tags: ['verify', 'layout'] },
|
|
46
|
+
{ re: /surgicalMap|页映射/i, tags: ['import', 'template'] },
|
|
47
|
+
{ re: /内容自证|反推|成品内容/i, tags: ['export', 'verify'] },
|
|
48
|
+
]
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* **重节**(默认不进快测):真渲染 / 真装包 / 真起进程这类耗时大头。
|
|
52
|
+
* 判据是"这一节会不会 spawn 重型外部进程或做真实安装",不是"这一节重不重要"。
|
|
53
|
+
*/
|
|
54
|
+
export const HEAVY_SECTIONS = new Map([
|
|
55
|
+
['§17', '模板库 4 套回归(真实渲染/一致性)'],
|
|
56
|
+
['§22', 'Office 真渲染通道(spawn PowerPoint COM)'],
|
|
57
|
+
['§29', '预览路由双重防线(起 webServer)'],
|
|
58
|
+
['§40', 'profile bundle 安装路径(pnpm 真装)'],
|
|
59
|
+
['§41', '安装器 + 预设声明(pnpm 真装)'],
|
|
60
|
+
['§43', 'python-pptx 兜底真跑(spawn python)'],
|
|
61
|
+
])
|
|
62
|
+
|
|
63
|
+
/** 改动文件 → 标签/脚本。glob 用简单前缀与目录规则(避免引入依赖)。 */
|
|
64
|
+
const PATH_RULES = [
|
|
65
|
+
// 阶段 A:结构关系推导与几何反验证(改它必须跑 §58 与自证脚本)
|
|
66
|
+
{ match: (p) => p.startsWith('src/pptd/relations'), tags: ['diagram', 'relations', 'verify'], note: '结构关系推导/几何反验证' },
|
|
67
|
+
{ match: (p) => p.startsWith('src/pptd/render-html'), tags: ['preview', 'render', 'diagram'], note: '预览 HTML 与 layout.json 快照' },
|
|
68
|
+
{ match: (p) => p.startsWith('src/pptd/export-pptx') || p.startsWith('src/pptd/zips'), tags: ['export', 'chart', 'media', 'verify'], note: 'pptx 产物写出层' },
|
|
69
|
+
{ match: (p) => p.startsWith('src/pptd/schema') || p.startsWith('src/pptd/layout'), tags: ['verify', 'layout'], note: 'DSL 校验与排版' },
|
|
70
|
+
{ match: (p) => p.startsWith('src/pptd/svgCharts'), tags: ['chart', 'preview', 'render'], note: '图表模型/预览 SVG' },
|
|
71
|
+
{ match: (p) => p.startsWith('src/pptd/nativeChart'), tags: ['chart', 'export'], note: '原生图表部件' },
|
|
72
|
+
{ match: (p) => p.startsWith('src/pptd/import-pptx') || p.startsWith('src/pptd/loss-audit'), tags: ['import', 'template'], note: '导入与有损审计' },
|
|
73
|
+
{ match: (p) => p.startsWith('src/pptd/'), tags: ['export', 'verify'], note: 'pptd 其它模块' },
|
|
74
|
+
{ match: (p) => p.startsWith('src/tools'), tags: ['verify', 'engine', 'fallback', 'docs'], note: '工具注册/描述/输出' },
|
|
75
|
+
{ match: (p) => p.startsWith('src/router'), tags: ['docs', 'verify', 'engine'], note: '提示词与工作流段(含基线超集)' },
|
|
76
|
+
{ match: (p) => p.startsWith('src/index') || p.startsWith('src/preset-delivery'), tags: ['preset', 'isolation', 'install'], note: '装配层' },
|
|
77
|
+
{ match: (p) => p.startsWith('src/capabilities') || p.startsWith('src/office-qa'), tags: ['fallback', 'engine'], note: '能力探测/官方 QA' },
|
|
78
|
+
{ match: (p) => p.startsWith('src/pptxPy') || p.startsWith('src/msrender') || p.startsWith('src/verify'), tags: ['engine', 'fallback', 'verify', 'diagram'], note: '兜底引擎/渲染/门禁(verify 含阶段 A 集成)' },
|
|
79
|
+
{ match: (p) => p.startsWith('src/'), tags: ['verify'], note: 'src 其它' },
|
|
80
|
+
{ match: (p) => p.startsWith('skills/'), tags: ['docs', 'skill'], note: '内置技能(有 8000 字上限)' },
|
|
81
|
+
{ match: (p) => p.startsWith('docs/') || p === 'README.md', tags: ['docs'], note: '文档(含计数自证/手册事实审计)' },
|
|
82
|
+
{ match: (p) => p.startsWith('agent-presets/') || p.startsWith('cordis.patch') || p === 'package.json', tags: ['preset', 'install'], note: '预设/装配清单' },
|
|
83
|
+
{ match: (p) => p.startsWith('scripts/smoke') || p.startsWith('scripts/fixtures/'), tags: ['verify', 'docs'], note: '测试自身与基线' },
|
|
84
|
+
{ match: (p) => p.startsWith('scripts/'), tags: ['verify'], note: '验证脚本/工具链' },
|
|
85
|
+
{ match: (p) => p.startsWith('examples/'), tags: ['verify'], note: '样例工程' },
|
|
86
|
+
]
|
|
87
|
+
|
|
88
|
+
/** 从 smoke.mjs 源码扫出分节(`// ── N. 标题`),标题用于自动打标签。 */
|
|
89
|
+
export function sectionsFromSmoke(src = readFileSync(join(root, 'scripts', 'smoke.mjs'), 'utf8')) {
|
|
90
|
+
const out = []
|
|
91
|
+
for (const line of String(src).split(/\r?\n/)) {
|
|
92
|
+
const m = line.match(/^\/\/ ── (\d+(?:\.\d+)?)\.\s*(.*)$/)
|
|
93
|
+
if (m) out.push({ id: `§${m[1]}`, title: m[2].trim() })
|
|
94
|
+
}
|
|
95
|
+
return out
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** 给一节打标签。 */
|
|
99
|
+
export function tagsOf(section) {
|
|
100
|
+
const tags = new Set()
|
|
101
|
+
for (const r of TAG_RULES) if (r.re.test(section.title)) for (const t of r.tags) tags.add(t)
|
|
102
|
+
if (HEAVY_SECTIONS.has(section.id)) tags.add('heavy')
|
|
103
|
+
return [...tags]
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* 核心:改动文件 → 影响面。
|
|
108
|
+
* @returns {{files: string[], tags: Set<string>, sections: {id: string, title: string, tags: string[], heavy: boolean, why: string}[], scripts: string[], unmapped: string[], notes: string[]}}
|
|
109
|
+
*/
|
|
110
|
+
export function explain(files, { includeHeavy = false } = {}) {
|
|
111
|
+
const norm = files.map((f) => relative(root, f).split(sep).join('/')).filter((f) => f !== '' && !f.startsWith('..'))
|
|
112
|
+
const tags = new Set()
|
|
113
|
+
const scripts = new Set()
|
|
114
|
+
const notes = []
|
|
115
|
+
const unmapped = []
|
|
116
|
+
for (const f of norm) {
|
|
117
|
+
const hit = PATH_RULES.find((r) => r.match(f))
|
|
118
|
+
if (hit === undefined) { unmapped.push(f); continue }
|
|
119
|
+
for (const t of hit.tags) tags.add(t)
|
|
120
|
+
// 文件类型 → 也要跑的独立验证脚本(重脚本只在 includeHeavy 时进快测)
|
|
121
|
+
if (f.startsWith('src/') || f.startsWith('agent-presets/') || f.startsWith('cordis.patch')) scripts.add('check:lib')
|
|
122
|
+
if (f.startsWith('docs/') || f.startsWith('skills/') || f === 'README.md') scripts.add('audit:facts')
|
|
123
|
+
if (f.startsWith('scripts/smoke') || f.startsWith('scripts/fixtures/')) scripts.add('test:preset')
|
|
124
|
+
}
|
|
125
|
+
const all = sectionsFromSmoke()
|
|
126
|
+
const sections = all
|
|
127
|
+
.map((s) => ({ ...s, tags: tagsOf(s), heavy: HEAVY_SECTIONS.has(s.id), why: tagsOf(s).filter((t) => tags.has(t)).join(',') }))
|
|
128
|
+
.filter((s) => s.why !== '' && (includeHeavy || !s.heavy))
|
|
129
|
+
if (norm.length === 0) notes.push('没有改动文件(或不在仓库内)⇒ 影响面未知,调用方应跑全量')
|
|
130
|
+
if (unmapped.length) notes.push(`未映射文件 ${unmapped.length} 个 ⇒ fail-safe:调用方应跑全量(${unmapped.slice(0, 3).join('、')}${unmapped.length > 3 ? '…' : ''})`)
|
|
131
|
+
return { files: norm, tags, sections, scripts: [...scripts], unmapped, notes }
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** 覆盖率审计:每个 smoke 分节都得有标签(否则它永远选不中)。 */
|
|
135
|
+
export function audit() {
|
|
136
|
+
const sections = sectionsFromSmoke()
|
|
137
|
+
const untagged = sections.filter((s) => tagsOf(s).filter((t) => t !== 'heavy').length === 0)
|
|
138
|
+
const heavy = sections.filter((s) => HEAVY_SECTIONS.has(s.id))
|
|
139
|
+
const heavyMissing = [...HEAVY_SECTIONS.keys()].filter((id) => !sections.some((s) => s.id === id))
|
|
140
|
+
return { total: sections.length, untagged, heavy, heavyMissing }
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (process.argv[1] && pathToFileURL(process.argv[1]).href === import.meta.url) {
|
|
144
|
+
const args = process.argv.slice(2)
|
|
145
|
+
if (args.includes('--audit')) {
|
|
146
|
+
const a = audit()
|
|
147
|
+
console.log(`分节总数 = ${a.total}|重节 = ${a.heavy.length}(${a.heavy.map((s) => s.id).join(' ')})`)
|
|
148
|
+
if (a.heavyMissing.length) console.log(`⚠ HEAVY_SECTIONS 里已不存在的节:${a.heavyMissing.join(' ')}`)
|
|
149
|
+
if (a.untagged.length) { console.log('⚠ 无标签(永远选不中)的分节:'); a.untagged.forEach((s) => console.log(` ${s.id} ${s.title}`)) }
|
|
150
|
+
else console.log('✓ 每个分节都有标签(可被影响面选中)')
|
|
151
|
+
process.exit(a.untagged.length || a.heavyMissing.length ? 1 : 0)
|
|
152
|
+
}
|
|
153
|
+
const files = args.filter((a) => !a.startsWith('--'))
|
|
154
|
+
const ex = explain(files, { includeHeavy: args.includes('--with-heavy') })
|
|
155
|
+
console.log(`改动 ${ex.files.length} 个文件|标签 = ${[...ex.tags].join(',') || '(无)'}`)
|
|
156
|
+
console.log(`选中分节 ${ex.sections.length} 个:${ex.sections.map((s) => `${s.id}[${s.why}]`).join(' ')}`)
|
|
157
|
+
const skippedHeavy = sectionsFromSmoke().filter((s) => HEAVY_SECTIONS.has(s.id)).map((s) => s.id)
|
|
158
|
+
if (!args.includes('--with-heavy')) console.log(`默认跳过重节:${skippedHeavy.join(' ')}`)
|
|
159
|
+
for (const n of ex.notes) console.log(`ℹ ${n}`)
|
|
160
|
+
if (!args.includes('--quiet')) console.log(`建议顺带跑的脚本:${ex.scripts.join(' ') || '(无)'}`)
|
|
161
|
+
process.exit(ex.unmapped.length ? 2 : 0)
|
|
162
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 发版档(`npm run test:release`,2026-09-27 新增)——**发版与批次边界**跑这一档。
|
|
3
|
+
*
|
|
4
|
+
* 与 `npm test` 的区别:`npm test` 是"核心门禁全量"(CI 与本地都跑);本档在它之上再加
|
|
5
|
+
* e2e / 隔离矩阵 / 原生图表自证 / 损失审计自证 / 安装路径自证,并把**互相独立的重脚本并行跑**——
|
|
6
|
+
* 实测串行约 621.7s(10.4 分钟),并行后墙钟时间约为其中最慢的一项 + 少量开销。
|
|
7
|
+
*
|
|
8
|
+
* 每条子进程的输出写到**自己的日志**(`<tmp>/dsh-test-release/<name>.log`),末尾只汇总结论;
|
|
9
|
+
* 失败时把该日志最后若干行打出来——并行不等于看不清谁失败。
|
|
10
|
+
*/
|
|
11
|
+
import { spawn } from 'node:child_process'
|
|
12
|
+
import { mkdirSync, readFileSync, writeFileSync } from 'node:fs'
|
|
13
|
+
import { tmpdir } from 'node:os'
|
|
14
|
+
import { join } from 'node:path'
|
|
15
|
+
import { root } from './test-map.mjs'
|
|
16
|
+
|
|
17
|
+
const logDir = join(tmpdir(), 'dsh-test-release')
|
|
18
|
+
mkdirSync(logDir, { recursive: true })
|
|
19
|
+
|
|
20
|
+
const TASKS = [
|
|
21
|
+
{ name: 'core(build+LF+smoke 全量+预设门禁)', cmd: 'npm test', must: true },
|
|
22
|
+
{ name: 'e2e(1.0 端到端)', cmd: 'node scripts/e2e-1.0.mjs', must: true },
|
|
23
|
+
{ name: '隔离矩阵(真宿主作用域)', cmd: 'node scripts/verify-preset-scope.mjs', must: true },
|
|
24
|
+
{ name: '原生图表自证(COM+反读+负对照)', cmd: 'node scripts/verify-native-charts.mjs', must: true },
|
|
25
|
+
{ name: '损失审计自证', cmd: 'node scripts/verify-loss-audit.mjs', must: true },
|
|
26
|
+
{ name: '安装路径自证(bundle/预设声明)', cmd: 'node scripts/verify-bundle-install.mjs', must: false },
|
|
27
|
+
{ name: '手册事实审计', cmd: 'node scripts/audit-manual-facts.mjs', must: true },
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
const only = (process.argv.find((a) => a.startsWith('--only=')) ?? '').slice(7)
|
|
31
|
+
const picked = only ? TASKS.filter((t) => only.split(',').some((o) => t.name.includes(o))) : TASKS
|
|
32
|
+
|
|
33
|
+
function launch(t) {
|
|
34
|
+
return new Promise((resolve) => {
|
|
35
|
+
const logPath = join(logDir, `${t.name.replace(/[^\w\u4e00-\u9fa5]+/g, '_')}.log`)
|
|
36
|
+
const fd = writeFileSync(logPath, `$ ${t.cmd}\n`, 'utf8')
|
|
37
|
+
const p = spawn(t.cmd, { shell: true, cwd: root, stdio: ['ignore', 'pipe', 'pipe'] })
|
|
38
|
+
let buf = ''
|
|
39
|
+
const onData = (d) => { buf += d.toString() }
|
|
40
|
+
p.stdout.on('data', onData)
|
|
41
|
+
p.stderr.on('data', onData)
|
|
42
|
+
p.on('close', (code) => {
|
|
43
|
+
writeFileSync(logPath, `$ ${t.cmd}\n${buf}`, 'utf8')
|
|
44
|
+
resolve({ ...t, code: code ?? 1, logPath, tail: buf.split(/\r?\n/).filter((l) => l.trim()).slice(-6).join('\n') })
|
|
45
|
+
})
|
|
46
|
+
void fd
|
|
47
|
+
})
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
console.log(`==== 发版档(test:release):并行 ${picked.length} 项|日志目录 ${logDir} ====`)
|
|
51
|
+
const t0 = Date.now()
|
|
52
|
+
const results = await Promise.all(picked.map(launch))
|
|
53
|
+
const wall = ((Date.now() - t0) / 1000).toFixed(1)
|
|
54
|
+
for (const r of results) console.log(`${r.code === 0 ? '✓' : '✗'} ${r.name}(${r.code === 0 ? '通过' : `exit ${r.code}`})→ ${r.logPath}`)
|
|
55
|
+
|
|
56
|
+
const failed = results.filter((r) => r.code !== 0)
|
|
57
|
+
console.log(`==== 发版档结果:${results.length - failed.length}/${results.length} 通过|墙钟 ${wall}s ====`)
|
|
58
|
+
for (const f of failed) {
|
|
59
|
+
console.log(`\n── ✗ ${f.name} 的日志末尾(完整:${f.logPath})──\n${f.tail}`)
|
|
60
|
+
}
|
|
61
|
+
// 只为让"日志文件确实写了内容"这件事可自证(避免并行下丢失输出)
|
|
62
|
+
const empty = results.filter((r) => readFileSync(r.logPath, 'utf8').trim().split(/\r?\n/).length < 2)
|
|
63
|
+
if (empty.length) console.log(`⚠ 这些任务的日志是空的(并行捕获可能失败):${empty.map((e) => e.name).join('、')}`)
|
|
64
|
+
process.exit(failed.length ? 1 : 0)
|