dsh-ppt-studio 1.1.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/LICENSE +21 -0
- package/README.md +480 -0
- package/agent-presets/ppt/agent.cordis.yml +308 -0
- package/agent-presets/ppt/preset.yml +3 -0
- package/cordis.patch.yml +29 -0
- package/docs/00-/350/256/276/350/256/241/346/272/220.md +537 -0
- package/docs/01-/351/234/200/346/261/202/344/270/216/347/233/256/346/240/207.md +136 -0
- package/docs/02-/346/212/200/346/234/257/346/212/245/345/221/212.md +483 -0
- package/docs/03-/346/233/264/346/226/260/346/227/245/345/277/227.md +2898 -0
- package/docs/04-/350/267/257/347/272/277/345/233/276/344/270/216/351/207/214/347/250/213/347/242/221.md +113 -0
- package/docs/05-/350/277/255/344/273/243/346/265/201/347/250/213.md +210 -0
- package/docs/06-/350/257/204/345/256/241/344/270/216/346/265/213/350/257/225.md +296 -0
- package/docs/fidelity-report.md +225 -0
- package/examples/fx/fx-mini/deck.yaml +39 -0
- package/examples/fx/fx-mini/pages/01.yaml +6 -0
- package/examples/fx/fx-mini/pages/02.yaml +2 -0
- package/examples/fx/fx-mini/pages/03.yaml +6 -0
- package/examples/fx/fx-pro/deck.yaml +48 -0
- package/examples/fx/fx-pro/pages/01_cover.yaml +40 -0
- package/examples/fx/fx-pro/pages/02_arch.yaml +55 -0
- package/examples/fx/fx-pro/pages/03_table.yaml +20 -0
- package/examples/fx/fx-pro/pages/04_chart.yaml +40 -0
- package/examples/fx/fx-pro/pages/05_timeline.yaml +71 -0
- package/examples/fx/fx-pro/pages/06_longtext.yaml +13 -0
- package/examples/fx/fx-pro/pages/07_safearea.yaml +33 -0
- package/examples/fx/fx-pro/pages/08_contrast.yaml +22 -0
- package/examples/fx/fx-pro/pages/09_custgeom.yaml +32 -0
- package/examples/fx/fx-pro/pages/10_numbers.yaml +25 -0
- package/examples/fx/fx-pro/pages/11_section.yaml +20 -0
- package/examples/fx/fx-pro/pages/12_summary.yaml +57 -0
- package/examples/fx/fx-pro/preview/01_pages_01_cover.html +1 -0
- package/examples/fx/fx-pro/preview/02_pages_02_arch.html +3 -0
- package/examples/fx/fx-pro/preview/03_pages_03_table.html +1 -0
- package/examples/fx/fx-pro/preview/04_pages_04_chart.html +3 -0
- package/examples/fx/fx-pro/preview/05_pages_05_timeline.html +2 -0
- package/examples/fx/fx-pro/preview/06_pages_06_longtext.html +4 -0
- package/examples/fx/fx-pro/preview/07_pages_07_safearea.html +2 -0
- package/examples/fx/fx-pro/preview/08_pages_08_contrast.html +1 -0
- package/examples/fx/fx-pro/preview/09_pages_09_custgeom.html +1 -0
- package/examples/fx/fx-pro/preview/10_pages_10_numbers.html +1 -0
- package/examples/fx/fx-pro/preview/11_pages_11_section.html +1 -0
- package/examples/fx/fx-pro/preview/12_pages_12_summary.html +1 -0
- package/examples/fx/fx-pro/preview/deck.html +10 -0
- package/examples/fx/fx-pro/preview/layout.json +2325 -0
- package/examples/fx/fx-pro/preview/measured.json +814 -0
- package/examples/fx/fx-pro.pptx +0 -0
- package/examples/fx/ren-single/01.png +0 -0
- package/examples/fx/ren-spliced/01.png +0 -0
- package/examples/fx/ren-spliced/03.png +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/deck.yaml +26 -0
- package/examples/smoke/imported/deck.yaml +30 -0
- package/examples/smoke/imported/import-styles.json +126 -0
- package/examples/smoke/imported/pages/slide_01.yaml +31 -0
- package/examples/smoke/imported/pages/slide_02.yaml +38 -0
- package/examples/smoke/imported/pages/slide_03.yaml +55 -0
- package/examples/smoke/imported/preview-rt/01_pages_slide_01.html +1 -0
- package/examples/smoke/imported/preview-rt/02_pages_slide_02.html +1 -0
- package/examples/smoke/imported/preview-rt/03_pages_slide_03.html +1 -0
- package/examples/smoke/imported/preview-rt/deck.html +1 -0
- package/examples/smoke/imported/preview-rt/layout.json +434 -0
- package/examples/smoke/imported/reference/previews/01.png +0 -0
- package/examples/smoke/imported/reference/previews/02.png +0 -0
- package/examples/smoke/imported/reference/previews/03.png +0 -0
- package/examples/smoke/imported/source.pptx +0 -0
- package/examples/smoke/media-test.png +0 -0
- package/examples/smoke/out-smoke.pptx +0 -0
- package/examples/smoke/pages/01_cover.yaml +27 -0
- package/examples/smoke/pages/02_content.yaml +42 -0
- package/examples/smoke/pages/03_chart.yaml +35 -0
- package/examples/smoke/preview/01_pages_01_cover.html +1 -0
- package/examples/smoke/preview/02_pages_02_content.html +1 -0
- package/examples/smoke/preview/03_pages_03_chart.html +1 -0
- package/examples/smoke/preview/deck.html +1 -0
- package/examples/smoke/preview/layout.json +378 -0
- package/lib/autodeclare.js +40 -0
- package/lib/commands.js +161 -0
- package/lib/crosscheck.js +300 -0
- package/lib/imgmeta.js +55 -0
- package/lib/index.js +239 -0
- package/lib/measurement.js +209 -0
- package/lib/msrender.js +123 -0
- package/lib/pptd/export-pptx.js +686 -0
- package/lib/pptd/import-pptx.js +774 -0
- package/lib/pptd/layout.js +209 -0
- package/lib/pptd/render-html.js +254 -0
- package/lib/pptd/schema.js +518 -0
- package/lib/pptd/svgCharts.js +147 -0
- package/lib/pptxPy.js +205 -0
- package/lib/preset-delivery.js +195 -0
- package/lib/preview-server.js +164 -0
- package/lib/router.js +165 -0
- package/lib/scaffold.js +315 -0
- package/lib/skill.js +174 -0
- package/lib/splice.js +219 -0
- package/lib/state.js +88 -0
- package/lib/surgical.js +369 -0
- package/lib/templates.js +400 -0
- package/lib/tools.js +955 -0
- package/lib/verify.js +828 -0
- package/lib/xmljs.js +112 -0
- package/lib/zips.js +117 -0
- package/package.json +81 -0
- package/scripts/audit-manual-facts.mjs +294 -0
- package/scripts/audit-template-fidelity.mjs +162 -0
- package/scripts/bench-m2.mjs +48 -0
- package/scripts/build-templates.mjs +63 -0
- package/scripts/build.mjs +14 -0
- package/scripts/build.sh +7 -0
- package/scripts/check-lib-freshness.mjs +70 -0
- package/scripts/check-preset.mjs +189 -0
- package/scripts/check-tracked-lf.mjs +84 -0
- package/scripts/dbg-import.mjs +13 -0
- package/scripts/dbg-media.mjs +10 -0
- package/scripts/dbg-tpl-inventory.mjs +23 -0
- package/scripts/dump-workflow-baseline.mjs +38 -0
- package/scripts/e2e-1.0.mjs +129 -0
- package/scripts/eval-skills-blind.mjs +91 -0
- package/scripts/eval-skills-score.mjs +129 -0
- package/scripts/export-orig-templates.ps1 +17 -0
- package/scripts/fixtures/workflow-baseline.json +300 -0
- package/scripts/fixtures.mjs +501 -0
- package/scripts/import-user-templates.mjs +60 -0
- package/scripts/install-desktop.mjs +312 -0
- package/scripts/install.mjs +209 -0
- package/scripts/migrate-templates-dualtrack.mjs +71 -0
- package/scripts/preflight-1.0.mjs +198 -0
- package/scripts/probe-asset-freshness.mjs +57 -0
- package/scripts/probe-effect-semantics.mjs +69 -0
- package/scripts/probe-reparse-safety.mjs +32 -0
- package/scripts/rebuild-templates-c11.mjs +69 -0
- package/scripts/regression-real.mjs +84 -0
- package/scripts/release-sync.mjs +425 -0
- package/scripts/shot-templates.mjs +49 -0
- package/scripts/smoke.mjs +2512 -0
- package/scripts/verify-bundle-install.mjs +281 -0
- package/scripts/verify-fresh-install.mjs +260 -0
- package/skills/ppt-studio-copy/SKILL.md +139 -0
- package/skills/ppt-studio-craft/SKILL.md +171 -0
- package/skills/ppt-studio-data/SKILL.md +173 -0
- package/skills/ppt-studio-manual/SKILL.md +104 -0
- package/templates/academic-white/deck.yaml +24 -0
- package/templates/academic-white/pages/_chart.yaml +43 -0
- package/templates/academic-white/pages/_compare.yaml +37 -0
- package/templates/academic-white/pages/_content.yaml +35 -0
- package/templates/academic-white/pages/_cover.yaml +28 -0
- package/templates/academic-white/pages/_ending.yaml +28 -0
- package/templates/academic-white/pages/_flow.yaml +61 -0
- package/templates/academic-white/preview.png +0 -0
- package/templates/academic-white/template.yaml +14 -0
- package/templates/business-blue/deck.yaml +28 -0
- package/templates/business-blue/pages/_chart.yaml +36 -0
- package/templates/business-blue/pages/_compare.yaml +45 -0
- package/templates/business-blue/pages/_content.yaml +80 -0
- package/templates/business-blue/pages/_cover.yaml +39 -0
- package/templates/business-blue/pages/_ending.yaml +32 -0
- package/templates/business-blue/pages/_flow.yaml +87 -0
- package/templates/business-blue/preview.png +0 -0
- package/templates/business-blue/template.yaml +14 -0
- package/templates/pitch-bold/deck.yaml +23 -0
- package/templates/pitch-bold/pages/_chart.yaml +32 -0
- package/templates/pitch-bold/pages/_compare.yaml +36 -0
- package/templates/pitch-bold/pages/_content.yaml +58 -0
- package/templates/pitch-bold/pages/_cover.yaml +31 -0
- package/templates/pitch-bold/pages/_ending.yaml +27 -0
- package/templates/pitch-bold/pages/_flow.yaml +46 -0
- package/templates/pitch-bold/preview.png +0 -0
- package/templates/pitch-bold/template.yaml +14 -0
- package/templates/tech-dark/deck.yaml +24 -0
- package/templates/tech-dark/pages/_chart.yaml +27 -0
- package/templates/tech-dark/pages/_compare.yaml +38 -0
- package/templates/tech-dark/pages/_content.yaml +51 -0
- package/templates/tech-dark/pages/_cover.yaml +30 -0
- package/templates/tech-dark/pages/_ending.yaml +25 -0
- package/templates/tech-dark/pages/_flow.yaml +80 -0
- package/templates/tech-dark/preview.png +0 -0
- package/templates/tech-dark/template.yaml +14 -0
package/lib/xmljs.js
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 极简 XML 解析(pptx 读取用):去命名空间的标签树。
|
|
3
|
+
* 仅支持我们 OOXML 子集所需:元素/属性/文本/子节点,忽略注释/CDATA 内容按文本。
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export function parseXml(xml) {
|
|
7
|
+
let i = 0
|
|
8
|
+
const root = { tag: '#root', attrs: {}, children: [], text: '' }
|
|
9
|
+
const stack = [root]
|
|
10
|
+
while (i < xml.length) {
|
|
11
|
+
const lt = xml.indexOf('<', i)
|
|
12
|
+
if (lt < 0) break
|
|
13
|
+
if (xml.startsWith('<?', lt) || xml.startsWith('<!--', lt) || xml.startsWith('<!', lt)) {
|
|
14
|
+
const end = xml.indexOf('>', lt)
|
|
15
|
+
i = end < 0 ? xml.length : end + 1
|
|
16
|
+
continue
|
|
17
|
+
}
|
|
18
|
+
// text
|
|
19
|
+
if (lt > i) appendText(stack[stack.length - 1], xml.slice(i, lt))
|
|
20
|
+
if (xml.startsWith('</', lt)) {
|
|
21
|
+
const end = xml.indexOf('>', lt)
|
|
22
|
+
stack.pop()
|
|
23
|
+
i = end < 0 ? xml.length : end + 1
|
|
24
|
+
continue
|
|
25
|
+
}
|
|
26
|
+
const close = xml.indexOf('>', lt)
|
|
27
|
+
const tagText = xml.slice(lt + 1, close).trim()
|
|
28
|
+
const selfClose = tagText.endsWith('/')
|
|
29
|
+
const body = selfClose ? tagText.slice(0, -1) : tagText
|
|
30
|
+
const spAt = body.search(/\s/)
|
|
31
|
+
const name = spAt < 0 ? body : body.slice(0, spAt)
|
|
32
|
+
// attrs:支持带空格/引号的值(如 typeface="Microsoft YaHei")——按 = 匹配,不按空格拆
|
|
33
|
+
const attrs = {}
|
|
34
|
+
const attrRe = /([^\s=]+)\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s]+))/g
|
|
35
|
+
let mm
|
|
36
|
+
let guard2 = 0
|
|
37
|
+
while ((mm = attrRe.exec(body)) !== null && guard2++ < 200) {
|
|
38
|
+
attrs[mm[1]] = mm[2] ?? mm[3] ?? mm[4]
|
|
39
|
+
}
|
|
40
|
+
const node = { tag: local(name), attrs, children: [], text: '' }
|
|
41
|
+
stack[stack.length - 1].children.push(node)
|
|
42
|
+
if (!selfClose) stack.push(node)
|
|
43
|
+
i = close + 1
|
|
44
|
+
}
|
|
45
|
+
return root
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// 实体解码(2026-09-14 修):此前只做转义(导出侧)、没有解码(导入侧),
|
|
49
|
+
// 于是导入含 & < > 的文本会拿到字面 "&"/"<"("R&D" → "R&D")。
|
|
50
|
+
// 只在**文本节点**解码:属性值保持原样,避免 splice/surgical 的结构性属性往返被改写。
|
|
51
|
+
const ENTITIES = { amp: '&', lt: '<', gt: '>', quot: '"', apos: "'" }
|
|
52
|
+
export function decodeEntities(s) {
|
|
53
|
+
return String(s).replace(/&(#[0-9]+|#x[0-9a-fA-F]+|[a-zA-Z]+);/g, (m, body) => {
|
|
54
|
+
if (body[0] === '#') {
|
|
55
|
+
const hex = body[1] === 'x' || body[1] === 'X'
|
|
56
|
+
const code = hex ? parseInt(body.slice(2), 16) : parseInt(body.slice(1), 10)
|
|
57
|
+
if (!Number.isFinite(code) || code <= 0 || code > 0x10ffff || (code >= 0xd800 && code <= 0xdfff)) return m
|
|
58
|
+
return String.fromCodePoint(code)
|
|
59
|
+
}
|
|
60
|
+
return ENTITIES[body] ?? m
|
|
61
|
+
})
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function appendText(node, s) {
|
|
65
|
+
if (s) node.text += decodeEntities(s)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function unquote(v) {
|
|
69
|
+
const s = v.trim()
|
|
70
|
+
if ((s.startsWith('"') && s.endsWith('"')) || (s.startsWith("'") && s.endsWith("'"))) return s.slice(1, -1)
|
|
71
|
+
return s
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function local(name) {
|
|
75
|
+
const idx = name.indexOf(':')
|
|
76
|
+
return idx >= 0 ? name.slice(idx + 1) : name
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function children(node, tag) {
|
|
80
|
+
return tag ? node.children.filter((c) => c.tag === tag) : node.children
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function first(node, tag) {
|
|
84
|
+
return node.children.find((c) => c.tag === tag)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function textOf(node) {
|
|
88
|
+
let out = node.text ?? ''
|
|
89
|
+
for (const c of node.children) {
|
|
90
|
+
if (c.tag === 't') out += c.text ?? ''
|
|
91
|
+
}
|
|
92
|
+
return out
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function allText(node) {
|
|
96
|
+
let out = ''
|
|
97
|
+
const walk = (n) => {
|
|
98
|
+
if (n.tag === 't') out += n.text ?? ''
|
|
99
|
+
for (const c of n.children) walk(c)
|
|
100
|
+
}
|
|
101
|
+
walk(node)
|
|
102
|
+
return out
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function firstByPath(node, path) {
|
|
106
|
+
let cur = node
|
|
107
|
+
for (const tag of path.split('/')) {
|
|
108
|
+
cur = first(cur, tag)
|
|
109
|
+
if (!cur) return undefined
|
|
110
|
+
}
|
|
111
|
+
return cur
|
|
112
|
+
}
|
package/lib/zips.js
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 极简 ZIP(OOXML)读写:store/deflate 双向,基于 Node 内置 zlib。
|
|
3
|
+
* 写入用 deflate(PowerPoint 可读);读取同时支持 store 与 deflate。
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import zlib from 'node:zlib'
|
|
7
|
+
|
|
8
|
+
// ── CRC32 ────────────────────────────────────────────────────────────────
|
|
9
|
+
const CRC_TABLE = (() => {
|
|
10
|
+
const t = new Uint32Array(256)
|
|
11
|
+
for (let n = 0; n < 256; n++) {
|
|
12
|
+
let c = n
|
|
13
|
+
for (let k = 0; k < 8; k++) c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1
|
|
14
|
+
t[n] = c >>> 0
|
|
15
|
+
}
|
|
16
|
+
return t
|
|
17
|
+
})()
|
|
18
|
+
export function crc32(buf) {
|
|
19
|
+
let c = 0xffffffff
|
|
20
|
+
for (let i = 0; i < buf.length; i++) c = CRC_TABLE[(c ^ buf[i]) & 0xff] ^ (c >>> 8)
|
|
21
|
+
return (c ^ 0xffffffff) >>> 0
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function u16(v) { return Buffer.from([v & 0xff, (v >>> 8) & 0xff]) }
|
|
25
|
+
function u32(v) { return Buffer.from([v & 0xff, (v >>> 8) & 0xff, (v >>> 16) & 0xff, (v >>> 24) & 0xff]) }
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* 写入 ZIP:entries = { name: Buffer|string }
|
|
29
|
+
*/
|
|
30
|
+
export function zipWrite(entries) {
|
|
31
|
+
const locals = []
|
|
32
|
+
const centrals = []
|
|
33
|
+
let offset = 0
|
|
34
|
+
for (const [name, content] of Object.entries(entries)) {
|
|
35
|
+
const data = Buffer.isBuffer(content) ? content : Buffer.from(String(content), 'utf8')
|
|
36
|
+
const nameBuf = Buffer.from(name, 'utf8')
|
|
37
|
+
const comp = zlib.deflateRawSync(data)
|
|
38
|
+
const crc = crc32(data)
|
|
39
|
+
const local = Buffer.concat([
|
|
40
|
+
u32(0x04034b50), u16(20), u16(0x0800), u16(8), u16(0), u16(0),
|
|
41
|
+
u32(crc), u32(comp.length), u32(data.length), u16(nameBuf.length), u16(0),
|
|
42
|
+
nameBuf, comp,
|
|
43
|
+
])
|
|
44
|
+
locals.push(local)
|
|
45
|
+
centrals.push(Buffer.concat([
|
|
46
|
+
u32(0x02014b50), u16(20), u16(20), u16(0x0800), u16(8), u16(0), u16(0),
|
|
47
|
+
u32(crc), u32(comp.length), u32(data.length), u16(nameBuf.length), u16(0), u16(0), u16(0), u16(0),
|
|
48
|
+
u32(0), u32(offset), nameBuf,
|
|
49
|
+
]))
|
|
50
|
+
offset += local.length
|
|
51
|
+
}
|
|
52
|
+
const central = Buffer.concat(centrals)
|
|
53
|
+
const eocd = Buffer.concat([
|
|
54
|
+
u32(0x06054b50), u16(0), u16(0), u16(Object.keys(entries).length), u16(Object.keys(entries).length),
|
|
55
|
+
u32(central.length), u32(offset), u16(0),
|
|
56
|
+
])
|
|
57
|
+
return Buffer.concat([...locals, central, eocd])
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** 读取 ZIP(支持 store/deflate;仅处理我们关心的条目)。返回 Map<name, Buffer> */
|
|
61
|
+
export function zipRead(buf) {
|
|
62
|
+
const out = new Map()
|
|
63
|
+
const eocd = findEocd(buf)
|
|
64
|
+
if (eocd < 0) throw new Error('not a zip file (no EOCD)')
|
|
65
|
+
const count = buf.readUInt16LE(eocd + 10)
|
|
66
|
+
let p = buf.readUInt32LE(eocd + 16) // central dir offset
|
|
67
|
+
for (let i = 0; i < count; i++) {
|
|
68
|
+
if (buf.readUInt32LE(p) !== 0x02014b50) break
|
|
69
|
+
const compMethod = buf.readUInt16LE(p + 10)
|
|
70
|
+
const compSize = buf.readUInt32LE(p + 20)
|
|
71
|
+
const nameLen = buf.readUInt16LE(p + 28)
|
|
72
|
+
const extraLen = buf.readUInt16LE(p + 30)
|
|
73
|
+
const commentLen = buf.readUInt16LE(p + 32)
|
|
74
|
+
const localOff = buf.readUInt32LE(p + 42)
|
|
75
|
+
const name = buf.subarray(p + 46, p + 46 + nameLen).toString('utf8')
|
|
76
|
+
p += 46 + nameLen + extraLen + commentLen
|
|
77
|
+
const lnameLen = buf.readUInt16LE(localOff + 26)
|
|
78
|
+
const lextraLen = buf.readUInt16LE(localOff + 28)
|
|
79
|
+
const dataStart = localOff + 30 + lnameLen + lextraLen
|
|
80
|
+
const data = buf.subarray(dataStart, dataStart + compSize)
|
|
81
|
+
out.set(name, compMethod === 0 ? data : zlib.inflateRawSync(data))
|
|
82
|
+
}
|
|
83
|
+
return out
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function findEocd(buf) {
|
|
87
|
+
for (let i = buf.length - 22; i >= 0; i--) {
|
|
88
|
+
if (buf.readUInt32LE(i) === 0x06054b50) return i
|
|
89
|
+
}
|
|
90
|
+
return -1
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** OOXML part 解码:兼容 UTF-16(WPS 常见)与 UTF-8 带/不带 BOM。 */
|
|
94
|
+
export function decodeXml(buf) {
|
|
95
|
+
if (buf.length >= 3 && buf[0] === 0xef && buf[1] === 0xbb && buf[2] === 0xbf) return buf.toString('utf8').slice(1)
|
|
96
|
+
if (buf.length >= 2 && buf[0] === 0xff && buf[1] === 0xfe) return buf.toString('utf16le')
|
|
97
|
+
if (buf.length >= 2 && buf[0] === 0xfe && buf[1] === 0xff) {
|
|
98
|
+
const s = buf.toString('utf16le').slice(1).split('')
|
|
99
|
+
const swapped = []
|
|
100
|
+
for (let i = 0; i + 1 < s.length; i += 2) {
|
|
101
|
+
swapped.push(s[i + 1])
|
|
102
|
+
swapped.push(s[i])
|
|
103
|
+
}
|
|
104
|
+
return swapped.join('')
|
|
105
|
+
}
|
|
106
|
+
return buf.toString('utf8')
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** OOXML 命名空间常量(用于 pptx 生成/解析) */
|
|
110
|
+
export const NS = {
|
|
111
|
+
o: 'http://schemas.openxmlformats.org/officeDocument/2006/relationships',
|
|
112
|
+
r: 'http://schemas.openxmlformats.org/officeDocument/2006/relationships',
|
|
113
|
+
a: 'http://schemas.openxmlformats.org/drawingml/2006/main',
|
|
114
|
+
p: 'http://schemas.openxmlformats.org/presentationml/2006/main',
|
|
115
|
+
ct: 'http://schemas.openxmlformats.org/package/2006/content-types',
|
|
116
|
+
rel: 'http://schemas.openxmlformats.org/package/2006/relationships',
|
|
117
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "dsh-ppt-studio",
|
|
3
|
+
"version": "1.1.0",
|
|
4
|
+
"description": "在 DeepSeek Harness 里做 PPT:说需求 → 逐页制作 → 导出可编辑的 .pptx。含布局检查、Office 真渲染复核,以及可扩展的本地模板库",
|
|
5
|
+
"//publish": "2026-09-18 定:发 npm,**无 scope** 包名 `dsh-ppt-studio`。**状态(2026-09-26):尚未发布** —— `npm login` 已通(`npm whoami` = `zbsph`)、包名未被占用(`npm view` 404)、构件已备好(发版那个 tgz,sha 与 Release 资产一致),但首次 `npm publish` 返回 **E403**:账号开了 2FA,普通登录凭据不能写入,需 `--otp=<6 位码>` 或勾了 Bypass 2FA 的 granular token。发布纪律:**用发版时那个 tgz**(`npm publish \"<.sync-state.json 里 localTgz 指向的文件>\"`),保证 registry 字节与 GitHub Release 资产**逐字节相同**;**别在 main 前进后跑裸 `npm publish`**(会按当前工作区重新打包)。成功后才做:核对 `npm view dsh-ppt-studio version dist.integrity`,并在隔离 DSH_HOME 里 `dsh plugin add dsh-ppt-studio` 真装一遍。见 docs/06 与 docs/03 的 1.1.0 条目。",
|
|
6
|
+
"repository": { "type": "git", "url": "git+https://github.com/zbsph/dsh-ppt-studio.git" },
|
|
7
|
+
"homepage": "https://github.com/zbsph/dsh-ppt-studio#readme",
|
|
8
|
+
"bugs": { "url": "https://github.com/zbsph/dsh-ppt-studio/issues" },
|
|
9
|
+
"keywords": ["dsh", "ppt", "pptx", "powerpoint", "slides", "deck", "presentation", "cordis", "plugin", "deepseek-harness"],
|
|
10
|
+
"publishConfig": { "access": "public" },
|
|
11
|
+
"type": "module",
|
|
12
|
+
"main": "./lib/index.js",
|
|
13
|
+
"//lib": "lib/ 是**提交**的构建产物(src 的纯拷贝):商店(创意工坊)对无 npm 包的条目生成的命令是 `dsh plugin add <repo URL>`,git 安装只拿得到已提交内容,lib 不入库则插件挂不上。守卫:npm run check:lib + smoke 断言。",
|
|
14
|
+
"//dsh": "profile bundle 声明(2026-09-14):让 `dsh plugin --profile <p> add <本包>` 装完即挂载——launcher 在 profile 里跑完 pnpm 后核对 dsh.profile.bundles,声明了 dsh.bundle 的依赖自动入栈。dsh.engines.dsh(2026-09-24,DSH 0.1.7 新增字段):本包从 1.0.7 起只支持 DSH >= 0.1.7-rc.1——不是挑剔版本,而是 0.1.7 把「预设 = 磁盘目录」换成了「预设 = 向 agentPresets 注册表声明」(旧版扫 <dshHome>/.agent-presets/;0.1.7 上游原文:the harness discovers no preset on disk),适配后的交付方式在 0.1.6 及以前没有任何效果。写上范围,让旧宿主上的用户拿到明确信号,而不是「装上却永远看不到那个预设」。",
|
|
15
|
+
"dsh": {
|
|
16
|
+
"engines": {
|
|
17
|
+
"dsh": ">=0.1.7-rc.1"
|
|
18
|
+
},
|
|
19
|
+
"bundle": {
|
|
20
|
+
"patch": "./cordis.patch.yml"
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
"exports": {
|
|
24
|
+
".": {
|
|
25
|
+
"default": "./lib/index.js"
|
|
26
|
+
},
|
|
27
|
+
"./package.json": "./package.json"
|
|
28
|
+
},
|
|
29
|
+
"files": [
|
|
30
|
+
"lib",
|
|
31
|
+
"vendor",
|
|
32
|
+
"templates",
|
|
33
|
+
"docs",
|
|
34
|
+
"skills",
|
|
35
|
+
"scripts",
|
|
36
|
+
"agent-presets",
|
|
37
|
+
"cordis.patch.yml",
|
|
38
|
+
"examples/smoke",
|
|
39
|
+
"examples/fx",
|
|
40
|
+
"README.md"
|
|
41
|
+
],
|
|
42
|
+
"license": "MIT",
|
|
43
|
+
"dependencies": {
|
|
44
|
+
"yaml": "^2.8.0"
|
|
45
|
+
},
|
|
46
|
+
"peerDependencies": {
|
|
47
|
+
"@deepseek-ai/cordis": ">=4.0.2 <5",
|
|
48
|
+
"@deepseek-ai/dsh-skill": ">=0.1.5-rc.2 <2",
|
|
49
|
+
"@deepseek-ai/dsh-tools": ">=0.1.5-rc.2 <2"
|
|
50
|
+
},
|
|
51
|
+
"peerDependenciesMeta": {
|
|
52
|
+
"@deepseek-ai/cordis": {
|
|
53
|
+
"optional": true
|
|
54
|
+
},
|
|
55
|
+
"@deepseek-ai/dsh-skill": {
|
|
56
|
+
"optional": true
|
|
57
|
+
},
|
|
58
|
+
"@deepseek-ai/dsh-tools": {
|
|
59
|
+
"optional": true
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
"scripts": {
|
|
63
|
+
"build": "bash scripts/build.sh",
|
|
64
|
+
"build:node": "node scripts/build.mjs",
|
|
65
|
+
"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",
|
|
67
|
+
"check:eol": "node scripts/check-tracked-lf.mjs",
|
|
68
|
+
"fresh": "node scripts/verify-fresh-install.mjs",
|
|
69
|
+
"test:real": "node scripts/regression-real.mjs",
|
|
70
|
+
"test:preflight": "node scripts/preflight-1.0.mjs",
|
|
71
|
+
"test:preset": "node scripts/check-preset.mjs",
|
|
72
|
+
"test:e2e": "node scripts/e2e-1.0.mjs",
|
|
73
|
+
"test:bundle": "node scripts/verify-bundle-install.mjs",
|
|
74
|
+
"check:lib": "node scripts/check-lib-freshness.mjs",
|
|
75
|
+
"audit:facts": "node scripts/audit-manual-facts.mjs",
|
|
76
|
+
"probe:effect": "node scripts/probe-effect-semantics.mjs",
|
|
77
|
+
"probe:reparse": "node scripts/probe-reparse-safety.mjs",
|
|
78
|
+
"probe:asset": "node scripts/probe-asset-freshness.mjs",
|
|
79
|
+
"eval:skills": "node scripts/eval-skills-score.mjs"
|
|
80
|
+
}
|
|
81
|
+
}
|
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 手册事实审计(evidence report)——「我改对了没有」的可复跑证据。
|
|
3
|
+
*
|
|
4
|
+
* 与 smoke 的分工:
|
|
5
|
+
* - smoke §37/§38(13 条)= **回归闸门**:谁改坏了当场红、必须修。
|
|
6
|
+
* - 本脚本 = **证据报告**:把每一条"手册 vs 源码"的事实逐条验一遍,打印 ✓/✗ + 源码锚点,
|
|
7
|
+
* 供人工复核与交接(`npm run audit:facts`)。不进门禁,失败退出码非 0 便于 CI/人工使用。
|
|
8
|
+
*
|
|
9
|
+
* 覆盖三类:
|
|
10
|
+
* ① 结构性事实(名字/集合/形状/阈值)——可机器判定;
|
|
11
|
+
* ② 运行时语义(某个写法到底会不会报错)——用真实引擎跑小夹具;
|
|
12
|
+
* ③ 文档自证(各处引用计数、纪律文档里的条数)——防"文档说自己有 13 条其实只有 9 条"。
|
|
13
|
+
*/
|
|
14
|
+
import { join, dirname } from 'node:path'
|
|
15
|
+
import { fileURLToPath } from 'node:url'
|
|
16
|
+
import { readFileSync, existsSync } from 'node:fs'
|
|
17
|
+
import { rm, mkdir, writeFile, readFile } from 'node:fs/promises'
|
|
18
|
+
import { listBundledSkills } from '../lib/skill.js'
|
|
19
|
+
import { SCHEMA_REF } from '../lib/scaffold.js'
|
|
20
|
+
import { resolveDeck } from '../lib/pptd/schema.js'
|
|
21
|
+
import { renderDeck } from '../lib/pptd/render-html.js'
|
|
22
|
+
import { exportPptx } from '../lib/pptd/export-pptx.js'
|
|
23
|
+
import { verifyDeck } from '../lib/verify.js'
|
|
24
|
+
import { chartSvg } from '../lib/pptd/svgCharts.js'
|
|
25
|
+
import { zipRead, decodeXml } from '../lib/zips.js'
|
|
26
|
+
import { readFileSync as rfs } from 'node:fs'
|
|
27
|
+
|
|
28
|
+
const root = join(dirname(fileURLToPath(import.meta.url)), '..')
|
|
29
|
+
const read = (rel) => readFileSync(join(root, rel), 'utf8')
|
|
30
|
+
const src = {
|
|
31
|
+
verify: read(join('src', 'verify.js')),
|
|
32
|
+
schema: read(join('src', 'pptd', 'schema.js')),
|
|
33
|
+
svgCharts: read(join('src', 'pptd', 'svgCharts.js')),
|
|
34
|
+
exportPptx: read(join('src', 'pptd', 'export-pptx.js')),
|
|
35
|
+
renderHtml: read(join('src', 'pptd', 'render-html.js')),
|
|
36
|
+
tools: read(join('src', 'tools.js')),
|
|
37
|
+
index: read(join('src', 'index.js')),
|
|
38
|
+
commands: read(join('src', 'commands.js')),
|
|
39
|
+
router: read(join('src', 'router.js')),
|
|
40
|
+
crosscheck: read(join('src', 'crosscheck.js')),
|
|
41
|
+
scaffold: read(join('src', 'scaffold.js')),
|
|
42
|
+
}
|
|
43
|
+
const skills = listBundledSkills().map((b) => ({ name: b.parsed.name, text: b.parsed.content, desc: b.parsed.description, when: b.parsed.whenToUse ?? '' }))
|
|
44
|
+
const bySkill = Object.fromEntries(skills.map((s) => [s.name, s.text]))
|
|
45
|
+
const all = skills.map((s) => s.text).join('\n')
|
|
46
|
+
const manual = bySkill['ppt-studio-manual'] ?? ''
|
|
47
|
+
|
|
48
|
+
let pass = 0
|
|
49
|
+
let fail = 0
|
|
50
|
+
const rows = []
|
|
51
|
+
const check = (claim, ok, evidence) => {
|
|
52
|
+
rows.push(`${ok ? '✓' : '✗'} ${claim}\n ↳ ${evidence}`)
|
|
53
|
+
ok ? pass++ : fail++
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// ── ① 结构性事实 ────────────────────────────────────────────────────────────
|
|
57
|
+
const registered = new Set()
|
|
58
|
+
for (const f of ['tools.js', 'index.js']) for (const m of read(join('src', f)).matchAll(/name: '(ppt_[a-z_]+)'/g)) registered.add(m[1])
|
|
59
|
+
const mentioned = new Set([...all.matchAll(/\b(ppt_[a-z_]+)\b/g)].map((m) => m[1]))
|
|
60
|
+
const ghosts = [...mentioned].filter((t) => !registered.has(t))
|
|
61
|
+
check('手册提到的 ppt_* 全部是已注册工具', ghosts.length === 0, ghosts.length ? `不存在:${ghosts.join('、')}` : `${mentioned.size} 个名字命中注册表(共 ${registered.size} 个工具)`)
|
|
62
|
+
|
|
63
|
+
const vLines = src.verify.split(/\r?\n/)
|
|
64
|
+
const codes = new Map()
|
|
65
|
+
vLines.forEach((line, i) => {
|
|
66
|
+
if (/\b(?:let|const|var)\s+code\s*=/.test(line)) return
|
|
67
|
+
for (const m of line.matchAll(/code\s*[:=]\s*'([a-z-]+)'/g)) {
|
|
68
|
+
const w = vLines.slice(Math.max(0, i - 3), i + 1).join('\n')
|
|
69
|
+
codes.set(m[1], /'error'/.test(w) ? 'error' : 'warn|suggest')
|
|
70
|
+
}
|
|
71
|
+
})
|
|
72
|
+
const errCodes = [...codes].filter(([, s]) => s === 'error').map(([c]) => c).sort()
|
|
73
|
+
const noDoc = errCodes.filter((c) => !manual.includes(c))
|
|
74
|
+
check('verify 的每个门禁错误码都写进了使用手册 §4', noDoc.length === 0, noDoc.length ? `缺:${noDoc.join('、')}` : `错误码 ${errCodes.join('/')}`)
|
|
75
|
+
const gateLine = manual.split('\n').find((l) => l.includes('ERROR 必须清零')) ?? ''
|
|
76
|
+
const gateNames = [...gateLine.matchAll(/`([a-z][a-z-]*)`/g)].map((m) => m[1])
|
|
77
|
+
const ghostGate = gateNames.filter((t) => !errCodes.includes(t))
|
|
78
|
+
check('手册门禁清单里的 code 名都是真实错误码(防写出源码里没有的名字)', ghostGate.length === 0 && gateNames.length > 0, ghostGate.length ? `幽灵 code:${ghostGate.join('、')}` : `清单 = ${gateNames.join('/')}`)
|
|
79
|
+
|
|
80
|
+
for (const c of ['density', 'hotspot', 'near-align']) {
|
|
81
|
+
const line = vLines.find((l) => l.includes(`code: '${c}'`)) ?? ''
|
|
82
|
+
check(`${c} 在源码里是 warning(手册不得归到 [·] 建议)`, /severity: 'warning'/.test(line), `verify.js: ${line.trim().slice(0, 90)}`)
|
|
83
|
+
}
|
|
84
|
+
const conformanceMode = src.verify.match(/severity: mode === 'strict' \? 'error' : 'warning',\s*\n?\s*code: 'theme-conformance'/)
|
|
85
|
+
check('theme-conformance:strict=error / suggest=warning / off=跳过(手册如此描述)', !!conformanceMode, 'verify.js themeConformance(): severity 随 mode 切换;无 colors 自动跳过')
|
|
86
|
+
|
|
87
|
+
const lineValidator = src.schema.match(/仅支持 2 点 \[\[x1,y1\],\[x2,y2\]\]/)
|
|
88
|
+
check('line 只支持两点(手册已写明,拆成多条 line)', !!lineValidator, `schema.js validateLine: ${(lineValidator?.[0] ?? '未找到').slice(0, 60)}`)
|
|
89
|
+
|
|
90
|
+
const tableKeys = src.schema.match(/table: \['cols', 'rows', 'header'\]/)
|
|
91
|
+
check('table 只有 cols/rows/header(手册"没有逐列对齐字段"成立)', !!tableKeys, `schema.js ELEMENT_KEYS: ${tableKeys?.[0] ?? '未找到'}`)
|
|
92
|
+
|
|
93
|
+
const chartKeys = src.schema.match(/chart: \['chart'\]/)
|
|
94
|
+
const chartNoLabels = !/legend|dataLabels|labels\s*:/.test(src.schema.match(/function validateChart[\s\S]*?\n\}/)?.[0] ?? '')
|
|
95
|
+
check('chart 没有分类名/数值/图例字段(手册"标签要自己补"成立)', !!chartKeys && chartNoLabels, `schema.js ELEMENT_KEYS.chart=${chartKeys?.[0] ?? '?'};validateChart 只校验 type/data.cols/data.rows/series`)
|
|
96
|
+
|
|
97
|
+
const trunc = Number((src.svgCharts.match(/c\.length\s*>\s*(\d+)/) ?? [])[1])
|
|
98
|
+
const claimedTrunc = [...all.matchAll(/超过\s*(\d+)\s*字截断/g)].map((m) => Number(m[1]))
|
|
99
|
+
check('预览图表分类名截断阈值 = 手册所写', trunc > 0 && claimedTrunc.every((n) => n === trunc), `svgCharts.js: c.length > ${trunc};手册声称 ${claimedTrunc.join(',') || '未写'}`)
|
|
100
|
+
|
|
101
|
+
const realCmds = new Set([...src.commands.matchAll(/cmd === '([a-z-]+)'/g)].map((m) => m[1]))
|
|
102
|
+
const mentionedCmds = new Set([...all.matchAll(/\/ppt\s+([a-z-]+)/g)].map((m) => m[1]))
|
|
103
|
+
const ghostCmds = [...mentionedCmds].filter((c) => !realCmds.has(c))
|
|
104
|
+
check('/ppt 子命令真实存在', ghostCmds.length === 0, ghostCmds.length ? `不存在:${ghostCmds.join('、')}` : `手册用到 ${[...mentionedCmds].join('/')};命令面共 ${realCmds.size} 个`)
|
|
105
|
+
|
|
106
|
+
const floorM = src.exportPptx.match(/Math\.max\((\d+),\s*Math\.round\(origSize \*\s*([\d.]+)\)\)/)
|
|
107
|
+
const floorPt = Number(floorM?.[1])
|
|
108
|
+
const floorPct = Math.round(Number(floorM?.[2]) * 100)
|
|
109
|
+
check('缩字下限常量 = 手册所写 max(6pt, 60%)', new RegExp(`max\\(${floorPt}pt, ${floorPct}% 原字号\\)`).test(manual), `export-pptx.js: Math.max(${floorPt}, Math.round(origSize * ${floorM?.[2]}))`)
|
|
110
|
+
|
|
111
|
+
check('缺省页面尺寸 960×540 / 1px=1pt(手册所写)', /size \?\? \{ width: 960, height: 540 \}/.test(src.verify) && /960x540/.test(src.schema), 'verify.js verifyDeck(): size ?? {width:960,height:540};schema.js 顶部注释')
|
|
112
|
+
|
|
113
|
+
// ── ② 运行时语义(真引擎小夹具)─────────────────────────────────────────────
|
|
114
|
+
const tmp = join(root, 'examples', 'smoke', '.tmp-audit-facts')
|
|
115
|
+
await rm(tmp, { recursive: true, force: true })
|
|
116
|
+
await mkdir(join(tmp, 'pages'), { recursive: true })
|
|
117
|
+
const deckYaml = (pages) => ['version: 1', 'title: "审计夹具"', 'size: [960, 540]', 'theme:',
|
|
118
|
+
' colors: {ink: "#111111", brand: "#2563EB"}',
|
|
119
|
+
' textStyles:', ' body: {fontSize: 14, color: "$ink"}', 'pages:', ...pages.map((p) => ` - pages/${p}.yaml`), ''].join('\n')
|
|
120
|
+
const page = (els, pageExtra = []) => ['pageType: content', ...pageExtra, 'elements:', ...els, ''].join('\n')
|
|
121
|
+
const box = 'bounds: [40, 40, 300, 80]'
|
|
122
|
+
|
|
123
|
+
// 夹具 A:文字压图表(含/不含 role 与声明)
|
|
124
|
+
const chartEl = (role, extra = []) => [' - elementId: chart', ' elementType: chart', ` ${box}`, ...(role ? [` role: ${role}`] : []),
|
|
125
|
+
' chart:', ' type: bar', ' data:', ' cols: [分类, 值]', ' rows: [[A, 1], [B, 2]]', ...extra]
|
|
126
|
+
const labelEl = [' - elementId: label', ' elementType: text', ' bounds: [60, 60, 200, 40]', ' content: {text: "压在图上的标签", style: "$body"}']
|
|
127
|
+
// 不压图表的第二个元素:单元素页不会产生美学建议(els.length < 2 直接 return),且压图会造出 content-collision
|
|
128
|
+
const sideEl = [' - elementId: side', ' elementType: text', ' bounds: [60, 320, 600, 40]', ' content: {text: "图旁的说明", style: "$body"}']
|
|
129
|
+
const writeDeck = async (dir, els, pageExtra = []) => {
|
|
130
|
+
await mkdir(join(dir, 'pages'), { recursive: true })
|
|
131
|
+
await writeFile(join(dir, 'deck.yaml'), deckYaml(['01']), 'utf8')
|
|
132
|
+
await writeFile(join(dir, 'pages', '01.yaml'), page(els, pageExtra), 'utf8')
|
|
133
|
+
}
|
|
134
|
+
const verifyDir = async (dir) => {
|
|
135
|
+
const ctx = await resolveDeck(dir)
|
|
136
|
+
const r = await renderDeck(ctx, {})
|
|
137
|
+
const v = verifyDeck(r.layout)
|
|
138
|
+
return { ctx, r, v, errors: v.text.split('\n').filter((l) => l.includes('[✗]')).length }
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const dirA = join(tmp, 'a-plain')
|
|
142
|
+
await writeDeck(dirA, [...labelEl, ...chartEl(null)])
|
|
143
|
+
const ra = await verifyDir(dirA)
|
|
144
|
+
check('运行时:无 role 的 chart 被 text 压住 → content-collision(内容互压)', /content-collision/.test(ra.v.text), `errors=${ra.errors};${(ra.v.text.match(/content-collision[^\n]*/) ?? [''])[0].slice(0, 100)}`)
|
|
145
|
+
|
|
146
|
+
const dirB = join(tmp, 'b-declared-plain')
|
|
147
|
+
await writeDeck(dirB, [...labelEl, ...chartEl(null)], ['expectedOverlaps:', ' - pair: [chart, label]'])
|
|
148
|
+
const rb = await verifyDir(dirB)
|
|
149
|
+
check('运行时:无 role 时"声明也不管用"(仍报 content-collision)——手册"不可声明豁免"成立', /content-collision/.test(rb.v.text) && rb.errors > 0, `errors=${rb.errors}`)
|
|
150
|
+
|
|
151
|
+
const dirC = join(tmp, 'c-background')
|
|
152
|
+
await writeDeck(dirC, [...labelEl, ...chartEl('background')], ['expectedOverlaps:', ' - pair: [chart, label]'])
|
|
153
|
+
const rc = await verifyDir(dirC)
|
|
154
|
+
check('运行时:chart role:background + 声明 → 0 错误("图当衬底"路径成立)', rc.errors === 0 && /预期重叠/.test(rc.v.text), `errors=${rc.errors};${(rc.v.text.match(/预期重叠[^\n]*/) ?? [''])[0].slice(0, 60)}`)
|
|
155
|
+
|
|
156
|
+
const dirD = join(tmp, 'd-decoration')
|
|
157
|
+
await writeDeck(dirD, [...labelEl, ...chartEl('decoration')])
|
|
158
|
+
const rd = await verifyDir(dirD)
|
|
159
|
+
check('运行时:chart role:decoration → 不再报重叠("纯装饰"路径成立,无需声明)', rd.errors === 0 && !/content-collision/.test(rd.v.text), `errors=${rd.errors}`)
|
|
160
|
+
|
|
161
|
+
const dirE = join(tmp, 'e-dec-outside')
|
|
162
|
+
await mkdir(join(dirE, 'pages'), { recursive: true })
|
|
163
|
+
await writeFile(join(dirE, 'deck.yaml'), ['version: 1', 'title: "出界夹具"', 'size: [960, 540]', 'theme:', ' colors: {ink: "#111111"}', ' safeArea: {top: 20, bottom: 20, left: 0, right: 0}', 'pages:', ' - pages/01.yaml', ''].join('\n'), 'utf8')
|
|
164
|
+
await writeFile(join(dirE, 'pages', '01.yaml'), page([' - elementId: deco', ' elementType: shape', ' kind: rect', ' role: decoration', ' bounds: [40, 4, 120, 12]', ' fill: "#111111"']), 'utf8')
|
|
165
|
+
const re = await verifyDir(dirE)
|
|
166
|
+
check('运行时:decoration 不豁免出界(仍报 out-of-safe-area)——手册与 C3 口径一致', /out-of-safe-area/.test(re.v.text), `errors=${re.errors};${(re.v.text.match(/out-of-safe-area[^\n]*/) ?? [''])[0].slice(0, 90)}`)
|
|
167
|
+
|
|
168
|
+
const dirF = join(tmp, 'f-bad-chart')
|
|
169
|
+
await writeDeck(dirF, [' - elementId: c', ' elementType: chart', ` ${box}`, ' chart:', ' type: bad', ' data: [{label: A, value: 1}]'])
|
|
170
|
+
let badChart = null
|
|
171
|
+
try { await resolveDeck(dirF) } catch (e) { badChart = e }
|
|
172
|
+
check('运行时:chart 写成 data:[{label,value}] / type:bad → schema 直接拒绝', !!badChart && /chart\.type|data\.cols/.test(badChart.messages?.join(';') ?? ''), (badChart?.messages ?? []).join('; ').slice(0, 140) || '竟然通过了')
|
|
173
|
+
|
|
174
|
+
const dirG = join(tmp, 'g-3pts-line')
|
|
175
|
+
await writeDeck(dirG, [' - elementId: poly', ' elementType: line', ' points: [[10, 10], [50, 30], [90, 10]]'])
|
|
176
|
+
let badLine = null
|
|
177
|
+
try { await resolveDeck(dirG) } catch (e) { badLine = e }
|
|
178
|
+
check('运行时:三点折线被拒(手册"每条只有两点"成立)', !!badLine && /2 点/.test(badLine.messages?.join(';') ?? ''), (badLine?.messages ?? []).join('; ').slice(0, 120) || '竟然通过了')
|
|
179
|
+
|
|
180
|
+
const dirH = join(tmp, 'h-notes')
|
|
181
|
+
await mkdir(join(dirH, 'pages'), { recursive: true })
|
|
182
|
+
await writeFile(join(dirH, 'deck.yaml'), deckYaml(['01']), 'utf8')
|
|
183
|
+
await writeFile(join(dirH, 'pages', '01.yaml'), ['pageType: content', 'notes: "这段讲稿应被写进 pptx 备注页"', 'elements:', ...labelEl, ''].join('\n'), 'utf8')
|
|
184
|
+
const rh = await verifyDir(dirH)
|
|
185
|
+
const expH = await exportPptx(rh.ctx, { out: 'out-notes.pptx', engine: 'pptd' })
|
|
186
|
+
const zipH = zipRead(await readFile(expH.file))
|
|
187
|
+
const hasNotesPart = [...zipH.keys()].some((k) => /^ppt\/notesSlides\/notesSlide\d+\.xml$/.test(k))
|
|
188
|
+
check('运行时:页面 notes 真的写进 pptx(备注页部件存在)', hasNotesPart, hasNotesPart ? '包内有 notesSlide 部件' : '包内没有 notesSlide')
|
|
189
|
+
const notesParityOk = expH.parity?.notesExp === 1 && expH.parity?.notesOut === 1 && expH.parity?.ok === true
|
|
190
|
+
check('运行时:导出 parity 自证备注(notesExp/notesOut 且 ok)', notesParityOk, `parity = ${JSON.stringify(expH.parity ?? {})}`)
|
|
191
|
+
const notesRelsH = decodeXml(zipH.get('ppt/notesSlides/_rels/notesSlide1.xml.rels') ?? Buffer.from(''))
|
|
192
|
+
check('运行时:notesSlide 自带 rels(→ notesMaster + 回指幻灯)——缺它真 PowerPoint 报"文件损坏"',
|
|
193
|
+
/notesMaster/.test(notesRelsH) && /Type="[^"]*\/slide"/.test(notesRelsH),
|
|
194
|
+
`rels: ${notesRelsH.slice(0, 120) || '(缺失)'}`)
|
|
195
|
+
check('运行时:presentation.xml 不含 notesMasterIdLst(实测该元素会让 PowerPoint 拒开)',
|
|
196
|
+
!/notesMasterIdLst/.test(decodeXml(zipH.get('ppt/presentation.xml'))), 'presentation.xml 中无 notesMasterIdLst')
|
|
197
|
+
const xmljsMod = await import('../lib/xmljs.js')
|
|
198
|
+
check('实体解码:</&/数字实体能解回字符(导入侧此前不解码)',
|
|
199
|
+
xmljsMod.parseXml('<a:t>R&D <x> A</a:t>').children[0].text === 'R&D <x> A',
|
|
200
|
+
`解码结果=${JSON.stringify(xmljsMod.parseXml('<a:t>R&D <x> A</a:t>').children[0].text)}`)
|
|
201
|
+
|
|
202
|
+
const dirI = join(tmp, 'i-chartref')
|
|
203
|
+
await writeDeck(dirI, [' - elementId: c', ' elementType: chart', ` ${box}`, ' chart:', ' type: bar', ' colors: ["$brand"]', ' data:', ' cols: [分类, 值]', ' rows: [[A, 1], [B, 2]]', ...sideEl])
|
|
204
|
+
const ri = await verifyDir(dirI)
|
|
205
|
+
// 必须走真实渲染产物:normalizePage() 会在 layout.js 里 resolveColor,直接调 chartSvg(原始 yaml) 会绕过它。
|
|
206
|
+
// (layout.json 快照只记 chartType、不记 colors,所以三层里可查的是"预览 HTML"与"导出 XML"两层。)
|
|
207
|
+
const htmlI = readFileSync(join(ri.r.outDir, ri.r.htmlFiles[0]), 'utf8')
|
|
208
|
+
const expI = await exportPptx(ri.ctx, { out: 'out-ref.pptx', engine: 'pptd' })
|
|
209
|
+
const xmlI = zipRead(await readFile(expI.file)).get('ppt/slides/slide1.xml').toString('utf8')
|
|
210
|
+
check('运行时:chart colors 用 $themeRef($brand)在预览与成品两层都被解析为 hex',
|
|
211
|
+
!htmlI.includes('$brand') && !/val="\$brand"/.test(xmlI),
|
|
212
|
+
`预览 HTML ${htmlI.includes('$brand') ? '出现字面 $brand ✗' : '已解析 ✓'};导出 XML ${/val="\$brand"/.test(xmlI) ? '仍是 $brand ✗' : '已解析 ✓'}`)
|
|
213
|
+
|
|
214
|
+
const themeChartColors = /aesthetic-theme[^\n]*颜色/.test(ri.v.text)
|
|
215
|
+
check('运行时:chart 显式用主题内颜色($ref=$brand)→ 不出配色建议(不打扰)', !themeChartColors, themeChartColors ? (ri.v.text.match(/[^\n]*aesthetic-theme[^\n]*/) ?? [''])[0].slice(0, 110) : '无配色建议($brand ∈ theme.colors)')
|
|
216
|
+
|
|
217
|
+
const dirI2 = join(tmp, 'i2-chart-offtheme')
|
|
218
|
+
await writeDeck(dirI2, [' - elementId: c', ' elementType: chart', ` ${box}`, ' chart:', ' type: bar', ' colors: ["#FF00FF"]', ' data:', ' cols: [分类, 值]', ' rows: [[A, 1], [B, 2]]', ...sideEl])
|
|
219
|
+
const ri2 = await verifyDir(dirI2)
|
|
220
|
+
const offThemeSuggested = /aesthetic-theme[^\n]*#FF00FF/.test(ri2.v.text)
|
|
221
|
+
check('运行时:chart 显式写主题外颜色 → [·] 建议且**不新增错误**(图表配色刻意保持建议级)',
|
|
222
|
+
offThemeSuggested && ri2.errors === 0,
|
|
223
|
+
`建议${offThemeSuggested ? '命中' : '未命中'};errors=${ri2.errors}`)
|
|
224
|
+
|
|
225
|
+
// ── ③ 文档自证 ──────────────────────────────────────────────────────────────
|
|
226
|
+
const smokeSrc = read(join('scripts', 'smoke.mjs'))
|
|
227
|
+
const guardNames = [...smokeSrc.matchAll(/ok\('(手册 vs 源码|SCHEMA_REF 自洽|文档计数自证)[^']*'/g)].map((m) => m[0])
|
|
228
|
+
const docClaim = Number((read(join('docs', '05-迭代流程.md')).match(/已落成 \*\*(\d+) 条机器断言\*\*/) ?? [])[1])
|
|
229
|
+
check('docs/05 §0c 声称的断言条数 = smoke 里实际新增的手册事实断言条数', docClaim === guardNames.length, `docs 声称 ${docClaim} 条;smoke 实际 ${guardNames.length} 条`)
|
|
230
|
+
|
|
231
|
+
const refLines = SCHEMA_REF.split('\n')
|
|
232
|
+
const refBad = refLines.find((l) => /decoration/.test(l) && /豁免/.test(l) && /出界/.test(l) && !/不豁免出界/.test(l))
|
|
233
|
+
check('SCHEMA_REF 自洽:不声称 decoration 豁免出界', refBad === undefined && refLines.some((l) => /不豁免出界/.test(l)), refBad ? `仍写着:${refBad.trim().slice(0, 70)}` : '两处口径一致:只豁免重叠、不豁免出界')
|
|
234
|
+
|
|
235
|
+
const crossPageClaims = /跨页一致性没有断言|这些没有工具断言|工具基本不管/.test(bySkill['ppt-studio-craft'] ?? '')
|
|
236
|
+
check('craft 已写明"跨页一致性没有工具断言"(源码确实无跨页/字体家族检查)', crossPageClaims && !/fontFamily/.test(src.verify), `verify.js 中 fontFamily 出现 ${(src.verify.match(/fontFamily/g) ?? []).length} 次`)
|
|
237
|
+
|
|
238
|
+
// 2026-09-18 改判:表格有了自己的溢出断言(table-overflow),图表仍然没有。
|
|
239
|
+
// 这条审计的价值正在于此——加门禁时它当场抓出"手册还在说表格没有溢出断言"。
|
|
240
|
+
const tableOverflowClaim = /table-overflow/.test(bySkill['ppt-studio-data'] ?? '') && /不检查图表/.test(bySkill['ppt-studio-data'] ?? '')
|
|
241
|
+
const overflowTextAndTable = /el\.kind === 'text' \|\| el\.kind === 'table'/.test(src.verify)
|
|
242
|
+
&& /code: 'table-overflow'/.test(src.verify)
|
|
243
|
+
&& !/code: '(chart|image)-overflow'/.test(src.verify)
|
|
244
|
+
check('data 已写明"表格有溢出断言(table-overflow)、图表没有"(与源码一致)',
|
|
245
|
+
tableOverflowClaim && overflowTextAndTable, `verify.js: 溢出断言覆盖 text+table;chart 仍无`)
|
|
246
|
+
|
|
247
|
+
// 2026-09-18 重写:crosscheck 从"机器判定(跨页数字分组 + grounded/unmapped 伪状态)"改为
|
|
248
|
+
// "材料包 + 审阅协议(工具不判定)"。这条审计的价值仍在于**手册与源码不许各说各话**:
|
|
249
|
+
// 源码里不得残留状态机/分组,data 手册必须同步成"材料包"口径且不得再提 grounded/unmapped。
|
|
250
|
+
const packRewrite = /审阅材料包/.test(src.crosscheck)
|
|
251
|
+
&& /无法核实/.test(src.crosscheck) && /无来源支撑/.test(src.crosscheck)
|
|
252
|
+
&& /不要使用子代理/.test(src.crosscheck)
|
|
253
|
+
const noVerdictLogic = !/status: [^\n]*'grounded'/.test(src.crosscheck)
|
|
254
|
+
&& !/numMap/.test(src.crosscheck) && !/groups: groups/.test(src.crosscheck)
|
|
255
|
+
const dataSkillSynced = /ppt_crosscheck/.test(bySkill['ppt-studio-data'] ?? '')
|
|
256
|
+
&& /材料包/.test(bySkill['ppt-studio-data'] ?? '')
|
|
257
|
+
&& !/grounded|unmapped/.test(bySkill['ppt-studio-data'] ?? '')
|
|
258
|
+
check('data 对 crosscheck 的口径与源码一致(材料包+审阅协议;无 grounded/unmapped 伪状态、无数字分组)',
|
|
259
|
+
packRewrite && noVerdictLogic && dataSkillSynced,
|
|
260
|
+
`crosscheck.js:材料包=${packRewrite}|无状态机=${noVerdictLogic}|data 手册已同步=${dataSkillSynced}`)
|
|
261
|
+
|
|
262
|
+
const chartIssueScope = /allZero/.test(src.svgCharts) && !/fabricat/.test(src.svgCharts)
|
|
263
|
+
const chartIssueHonest = /没有任何工具会拦/.test(bySkill['ppt-studio-data'] ?? '')
|
|
264
|
+
check('data 对 chartIssue 的能力描述准确(只拦空数据/全零)', chartIssueScope && chartIssueHonest, 'svgCharts.js chartIssue(): rows 为空 / 全零或非有限值 → 其余不拦')
|
|
265
|
+
|
|
266
|
+
const notesHonest = /备注页/.test(bySkill['ppt-studio-copy'] ?? '') && /(导出|写进|随导出)/.test(bySkill['ppt-studio-copy'] ?? '')
|
|
267
|
+
check('copy §7 与导出器一致:讲稿会写进 pptx 备注页(双向断言,手册说反了会红)',
|
|
268
|
+
notesHonest && /notesSlides\//.test(src.exportPptx), `export-pptx.js 备注部件 ${(src.exportPptx.match(/notesSlides\//g) ?? []).length} 处;手册${notesHonest ? '说会导出' : '未说会导出'}`)
|
|
269
|
+
|
|
270
|
+
const presetDrift = existsSync(join(root, 'agent-presets', 'ppt', 'agent.cordis.yml'))
|
|
271
|
+
check('预设副本仍在(check-preset.mjs 负责逐行比对)', presetDrift, 'agent-presets/ppt/agent.cordis.yml 存在,`npm run test:preset` 比对随包 standard')
|
|
272
|
+
|
|
273
|
+
// 触发纪律(2026-09-15 用户反馈):答疑手册曾被"开工时"加载——因为它的 description/whenToUse
|
|
274
|
+
// 把"如何开始 / 模型不确定某一 DSL 写法"写成了触发条件(对正在做 PPT 的人是恒真条件)。
|
|
275
|
+
// 事实判定:答疑触发面只认"用户提问",制作三本不把读者引向答疑手册,工作流提示段显式分行。
|
|
276
|
+
const manParsed = skills.find((s) => s.name === 'ppt-studio-manual') ?? { desc: '', when: '' }
|
|
277
|
+
const qaDesc = /用户问|提问/.test(manParsed.desc)
|
|
278
|
+
const qaWhen = /提问|用户问/.test(manParsed.when)
|
|
279
|
+
const negated = /不要加载/.test(manParsed.desc) && /不要加载/.test(manParsed.when)
|
|
280
|
+
const selfTrigger = /模型不确定/.test(manParsed.desc + manParsed.when) || /不确定某一/.test(manParsed.desc + manParsed.when)
|
|
281
|
+
check('答疑手册触发纪律:只由"用户提问"触发(description/whenToUse 双处声明 + "制作中不要加载"否定句)',
|
|
282
|
+
qaDesc && qaWhen && negated && !selfTrigger,
|
|
283
|
+
`提问触发=${qaDesc}/${qaWhen}|否定句=${negated}|恒真自触发措辞残留=${selfTrigger}`)
|
|
284
|
+
|
|
285
|
+
const crossPtr = ['ppt-studio-craft', 'ppt-studio-data', 'ppt-studio-copy'].filter((n) => (bySkill[n] ?? '').includes('ppt-studio-manual'))
|
|
286
|
+
const wfSplit = /答疑手册/.test(src.router) && /不要加载/.test(src.router)
|
|
287
|
+
check('答疑手册分层落地:工作流提示段显式分行(答疑手册 vs 制作手册),制作三本正文零指向答疑手册',
|
|
288
|
+
wfSplit && crossPtr.length === 0, `工作流含分行=${wfSplit};三本指向=${crossPtr.join('、') || '无'}`)
|
|
289
|
+
|
|
290
|
+
await rm(tmp, { recursive: true, force: true })
|
|
291
|
+
|
|
292
|
+
console.log(rows.join('\n'))
|
|
293
|
+
console.log(`\n==== 手册事实审计:${pass} 通过 / ${fail} 失败(共 ${pass + fail} 条,逐条附源码锚点)====`)
|
|
294
|
+
process.exit(fail > 0 ? 1 : 0)
|