dsh-ppt 0.6.0 → 0.8.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.
@@ -1,184 +1,184 @@
1
- #! /usr/bin/env node
2
- /**
3
- * build-deck.mjs —— dsh-ppt 的跨 harness 裸 CLI。
4
- *
5
- * DSH 插件内请优先使用 ppt_create 工具;把 skills/dsh-ppt 目录复制到
6
- * Claude Code / Cursor / Gemini CLI / Codex 等 agent 时,用本脚本生成三件套。
7
- *
8
- * 示例:
9
- * node build-deck.mjs --title "产品发布" --content "deck.md" --theme data --lang zh --out dist/deck
10
- * node build-deck.mjs --title "Pitch" --content "一句话介绍我们的产品。" --theme velvet --lang bilingual
11
- * node build-deck.mjs --list-themes
12
- */
13
-
14
- import { existsSync, readFileSync } from 'node:fs'
15
- import { resolve as resolvePath } from 'node:path'
16
- import { pathToFileURL } from 'node:url'
17
- import { listThemes, THEME_IDS, DEFAULT_THEME } from './deck-core.mjs'
18
- import { buildDeckAsync, listTemplates, editDeck, undoDeck, checkDeckAsync, renderDeck } from './deck-advanced.mjs'
19
-
20
- const HELP = `dsh-ppt —— 一句话 / 一篇文档 → HTML 放映 + PPTX 导出
21
-
22
- 用法:
23
- node build-deck.mjs --title <标题> --content <Markdown 或文件路径> [选项]
24
-
25
- 选项:
26
- --title <text> 演示文稿标题(必填)
27
- --content <text|@path> Markdown 正文;或文件路径(自动读取);或 @- 从 stdin 读
28
- --slides <json> 结构化 slides JSON(可选,与 --content 二选一)
29
- --template <id> weekly / defense / project / pitch;仅模板会生成草稿
30
- --brand <json|@path> 品牌颜色、字体、Logo、页脚
31
- --edit <deck.json> 修改指定页,配合 --edits <json|@path>
32
- --undo <deck.json> 撤销最近一次修改
33
- --check <deck.json> 查看质量报告和页码 / ID
34
- --render <deck.pptx> 用可选官方引擎生成逐页 PNG / PDF
35
- --format <png|pdf|both> 渲染格式,默认 png
36
- --width <n> PNG 目标宽度,默认 1440
37
- --timeout <ms> 渲染整体期限,默认 120000 毫秒
38
- --render-receipt <path> 配合 --check 核验当前渲染收据
39
- --revision <n> 编辑 / 撤销预期修订号,防止覆盖他人修改
40
- --list-templates 列出场景结构参考
41
- --theme <id> 视觉主题:${THEME_IDS.join(' / ')}(默认 ${DEFAULT_THEME})
42
- --lang <id> 界面语言:zh / en / bilingual(默认 zh)
43
- --motion <on|off> 页间转场与要点入场动画(默认 on;off 产出纯静态)
44
- --out <dir> 输出目录(默认当前目录)
45
- --file <name> 文件名前缀(默认取标题)
46
- --overwrite 显式覆盖同名三件套(默认自动追加 -1/-2…)
47
- --list-themes 列出内置主题
48
- --help 显示本帮助
49
-
50
- 输出:
51
- <file>.html 独立网页放映(无外链,可直接双击打开)
52
- <file>.pptx 可编辑 PPTX(16:9)
53
- <file>.json deck manifest
54
- `
55
-
56
- export function parseArgv(argv) {
57
- const args = { content: '', slides: null, lang: 'zh', theme: undefined, motion: undefined, out: '.', file: '', title: '', overwrite: false }
58
- const positional = []
59
- for (let i = 0; i < argv.length; i += 1) {
60
- const raw = String(argv[i])
61
- const eq = raw.indexOf('=')
62
- const key = eq >= 0 ? raw.slice(0, eq) : raw
63
- const inline = eq >= 0 ? raw.slice(eq + 1) : null
64
- const nextValue = () => {
65
- if (inline !== null) return inline
66
- if (i + 1 >= argv.length) throw new Error('dsh-ppt CLI:' + key + ' 需要一个值')
67
- i += 1
68
- return String(argv[i])
69
- }
70
- if (key === '--help' || key === '-h') args.help = true
71
- else if (key === '--list-themes') args.listThemes = true
72
- else if (key === '--list-templates') args.listTemplates = true
73
- else if (key === '--overwrite') args.overwrite = true
74
- else if (key === '--title') args.title = nextValue()
75
- else if (key === '--content') args.content = nextValue()
76
- else if (key === '--slides') args.slides = readJson(nextValue())
77
- else if (key === '--template') args.template = nextValue()
78
- else if (key === '--brand') args.brand = readJson(nextValue())
79
- else if (key === '--edit') args.edit = nextValue()
80
- else if (key === '--edits') args.edits = readJson(nextValue())
81
- else if (key === '--undo') args.undo = nextValue()
82
- else if (key === '--check') args.check = nextValue()
83
- else if (key === '--render') args.render = nextValue()
84
- else if (key === '--format') args.format = nextValue()
85
- else if (key === '--width') args.width = Number(nextValue())
86
- else if (key === '--timeout') args.timeoutMs = Number(nextValue())
87
- else if (key === '--render-receipt') args.renderReceipt = nextValue()
88
- else if (key === '--revision') args.revision = Number(nextValue())
89
- else if (key === '--theme') args.theme = nextValue()
90
- else if (key === '--lang') args.lang = nextValue()
91
- else if (key === '--motion') args.motion = nextValue()
92
- else if (key === '--out') { args.out = nextValue(); args.explicitOut = true }
93
- else if (key === '--file') args.file = nextValue()
94
- else positional.push(raw)
95
- }
96
- args.positional = positional
97
- return args
98
- }
99
-
100
- function readJson(value) { return JSON.parse(value.startsWith('@') ? readFileSync(resolvePath(value.slice(1)), 'utf8') : value) }
101
-
102
- async function readStdin(stream) {
103
- let text = ''
104
- stream.setEncoding('utf8')
105
- for await (const chunk of stream) text += chunk
106
- return text
107
- }
108
-
109
- export async function main(argv = process.argv.slice(2), io = {}) {
110
- const log = io.log ?? ((message) => console.log(message))
111
- const args = parseArgv(argv)
112
-
113
- if (args.help) {
114
- log(HELP.trim())
115
- return 0
116
- }
117
- if (args.listThemes) {
118
- for (const theme of listThemes('zh')) {
119
- log('- ' + theme.id.padEnd(8) + theme.name + '|' + theme.mood + '|适合:' + theme.bestFor)
120
- }
121
- return 0
122
- }
123
- if (args.listTemplates) { log(JSON.stringify({ templates: listTemplates(args.lang) }, null, 2)); return 0 }
124
- if ([args.edit, args.undo, args.check, args.render].filter(Boolean).length > 1) throw new Error('dsh-ppt CLI:--edit / --undo / --check / --render 只能选择一项')
125
- if (args.renderReceipt !== undefined && !args.check) throw new Error('dsh-ppt CLI:--render-receipt 需配合 --check')
126
- if (!args.render && [args.format, args.width, args.timeoutMs].some(v => v !== undefined)) throw new Error('dsh-ppt CLI:--format / --width / --timeout 需配合 --render')
127
- if (args.check) {
128
- const report = await checkDeckAsync({ deckPath: args.check, renderReceipt: args.renderReceipt })
129
- log(JSON.stringify(report, null, 2)); return report.ok && (args.renderReceipt === undefined || report.render?.status === 'rendered') ? 0 : 1
130
- }
131
- if (args.render) {
132
- const rendered = await (io.renderDeck ?? renderDeck)({ pptxPath: args.render, ...(args.explicitOut ? { outputDir: args.out } : {}), format: args.format, width: args.width, timeoutMs: args.timeoutMs })
133
- log(JSON.stringify(rendered, null, 2)); return rendered.ok ? 0 : 1
134
- }
135
- if (args.edit || args.undo) {
136
- const result = args.edit ? await editDeck({ deckPath: args.edit, edits: args.edits, brand: args.brand, expectedRevision: args.revision })
137
- : await undoDeck({ deckPath: args.undo, expectedRevision: args.revision })
138
- log(JSON.stringify(result, null, 2)); return 0
139
- }
140
-
141
- let content = String(args.content ?? '')
142
- let slides = args.slides
143
- if (content === '@-') {
144
- content = await readStdin(io.stdin ?? process.stdin)
145
- } else if (content.startsWith('@')) {
146
- content = readFileSync(resolvePath(content.slice(1)), 'utf8')
147
- } else if (content.trim() !== '' && existsSync(resolvePath(content.trim()))) {
148
- // README / SKILL.md 的示例直接传文件路径(不带 @)时也按文件读取
149
- content = readFileSync(resolvePath(content.trim()), 'utf8')
150
- } else if (content.trim() === '' && slides === null && !args.template) {
151
- throw new Error('dsh-ppt CLI:--content 不能为空(或用 --slides 传结构化幻灯片)')
152
- }
153
-
154
- const options = {
155
- title: args.title,
156
- content,
157
- slides,
158
- theme: args.theme,
159
- lang: args.lang,
160
- motion: args.motion,
161
- outputDir: resolvePath(args.out || '.'),
162
- fileName: args.file,
163
- overwrite: args.overwrite,
164
- template: args.template,
165
- brand: args.brand,
166
- }
167
- const result = await buildDeckAsync(options)
168
- log('dsh-ppt 已生成 ' + result.slideCount + ' 页演示文稿:')
169
- log(' HTML 放映:' + result.htmlPath)
170
- log(' PPTX 导出:' + result.pptxPath)
171
- log(' Manifest :' + result.jsonPath)
172
- if (result.deliveryStatus === 'draft') log(' 草稿:还有待填写内容,不可作为成品交付。')
173
- log(' 质量检查:' + result.quality.errorCount + ' 个错误 / ' + result.quality.warningCount + ' 个提醒;修订号 ' + result.revision)
174
- return 0
175
- }
176
-
177
- if (import.meta.url === pathToFileURL(process.argv[1] ?? '').href) {
178
- main().then((code) => {
179
- process.exitCode = code ?? 0
180
- }).catch((err) => {
181
- console.error('[dsh-ppt] ' + (err instanceof Error ? err.message : String(err)))
182
- process.exitCode = 1
183
- })
184
- }
1
+ #! /usr/bin/env node
2
+ /**
3
+ * build-deck.mjs —— dsh-ppt 的跨 harness 裸 CLI。
4
+ *
5
+ * DSH 插件内请优先使用 ppt_create 工具;把 skills/dsh-ppt 目录复制到
6
+ * Claude Code / Cursor / Gemini CLI / Codex 等 agent 时,用本脚本生成三件套。
7
+ *
8
+ * 示例:
9
+ * node build-deck.mjs --title "产品发布" --content "deck.md" --theme data --lang zh --out dist/deck
10
+ * node build-deck.mjs --title "Pitch" --content "一句话介绍我们的产品。" --theme velvet --lang bilingual
11
+ * node build-deck.mjs --list-themes
12
+ */
13
+
14
+ import { existsSync, readFileSync } from 'node:fs'
15
+ import { resolve as resolvePath } from 'node:path'
16
+ import { pathToFileURL } from 'node:url'
17
+ import { listThemes, THEME_IDS, DEFAULT_THEME } from './deck-core.mjs'
18
+ import { buildDeckAsync, listTemplates, editDeck, undoDeck, checkDeckAsync, renderDeck } from './deck-advanced.mjs'
19
+
20
+ const HELP = `dsh-ppt —— 一句话 / 一篇文档 → HTML 放映 + PPTX 导出
21
+
22
+ 用法:
23
+ node build-deck.mjs --title <标题> --content <Markdown 或文件路径> [选项]
24
+
25
+ 选项:
26
+ --title <text> 演示文稿标题(必填)
27
+ --content <text|@path> Markdown 正文;或文件路径(自动读取);或 @- 从 stdin 读
28
+ --slides <json> 结构化 slides JSON(可选,与 --content 二选一)
29
+ --template <id> weekly / defense / project / pitch;仅模板会生成草稿
30
+ --brand <json|@path> 品牌颜色、字体、Logo、页脚
31
+ --edit <deck.json> 修改指定页,配合 --edits <json|@path>
32
+ --undo <deck.json> 撤销最近一次修改
33
+ --check <deck.json> 查看质量报告和页码 / ID
34
+ --render <deck.pptx> 用可选官方引擎生成逐页 PNG / PDF
35
+ --format <png|pdf|both> 渲染格式,默认 png
36
+ --width <n> PNG 目标宽度,默认 1440
37
+ --timeout <ms> 渲染整体期限,默认 120000 毫秒
38
+ --render-receipt <path> 配合 --check 核验当前渲染收据
39
+ --revision <n> 编辑 / 撤销预期修订号,防止覆盖他人修改
40
+ --list-templates 列出场景结构参考
41
+ --theme <id> 视觉主题:${THEME_IDS.join(' / ')}(默认 ${DEFAULT_THEME})
42
+ --lang <id> 界面语言:zh / en / bilingual(默认 zh)
43
+ --motion <on|off> 页间转场与要点入场动画(默认 on;off 产出纯静态)
44
+ --out <dir> 输出目录(默认当前目录)
45
+ --file <name> 文件名前缀(默认取标题)
46
+ --overwrite 显式覆盖同名三件套(默认自动追加 -1/-2…)
47
+ --list-themes 列出内置主题
48
+ --help 显示本帮助
49
+
50
+ 输出:
51
+ <file>.html 独立网页放映(无外链,可直接双击打开)
52
+ <file>.pptx 可编辑 PPTX(16:9)
53
+ <file>.json deck manifest
54
+ `
55
+
56
+ export function parseArgv(argv) {
57
+ const args = { content: '', slides: null, lang: 'zh', theme: undefined, motion: undefined, out: '.', file: '', title: '', overwrite: false }
58
+ const positional = []
59
+ for (let i = 0; i < argv.length; i += 1) {
60
+ const raw = String(argv[i])
61
+ const eq = raw.indexOf('=')
62
+ const key = eq >= 0 ? raw.slice(0, eq) : raw
63
+ const inline = eq >= 0 ? raw.slice(eq + 1) : null
64
+ const nextValue = () => {
65
+ if (inline !== null) return inline
66
+ if (i + 1 >= argv.length) throw new Error('dsh-ppt CLI:' + key + ' 需要一个值')
67
+ i += 1
68
+ return String(argv[i])
69
+ }
70
+ if (key === '--help' || key === '-h') args.help = true
71
+ else if (key === '--list-themes') args.listThemes = true
72
+ else if (key === '--list-templates') args.listTemplates = true
73
+ else if (key === '--overwrite') args.overwrite = true
74
+ else if (key === '--title') args.title = nextValue()
75
+ else if (key === '--content') args.content = nextValue()
76
+ else if (key === '--slides') args.slides = readJson(nextValue())
77
+ else if (key === '--template') args.template = nextValue()
78
+ else if (key === '--brand') args.brand = readJson(nextValue())
79
+ else if (key === '--edit') args.edit = nextValue()
80
+ else if (key === '--edits') args.edits = readJson(nextValue())
81
+ else if (key === '--undo') args.undo = nextValue()
82
+ else if (key === '--check') args.check = nextValue()
83
+ else if (key === '--render') args.render = nextValue()
84
+ else if (key === '--format') args.format = nextValue()
85
+ else if (key === '--width') args.width = Number(nextValue())
86
+ else if (key === '--timeout') args.timeoutMs = Number(nextValue())
87
+ else if (key === '--render-receipt') args.renderReceipt = nextValue()
88
+ else if (key === '--revision') args.revision = Number(nextValue())
89
+ else if (key === '--theme') args.theme = nextValue()
90
+ else if (key === '--lang') args.lang = nextValue()
91
+ else if (key === '--motion') args.motion = nextValue()
92
+ else if (key === '--out') { args.out = nextValue(); args.explicitOut = true }
93
+ else if (key === '--file') args.file = nextValue()
94
+ else positional.push(raw)
95
+ }
96
+ args.positional = positional
97
+ return args
98
+ }
99
+
100
+ function readJson(value) { return JSON.parse(value.startsWith('@') ? readFileSync(resolvePath(value.slice(1)), 'utf8') : value) }
101
+
102
+ async function readStdin(stream) {
103
+ let text = ''
104
+ stream.setEncoding('utf8')
105
+ for await (const chunk of stream) text += chunk
106
+ return text
107
+ }
108
+
109
+ export async function main(argv = process.argv.slice(2), io = {}) {
110
+ const log = io.log ?? ((message) => console.log(message))
111
+ const args = parseArgv(argv)
112
+
113
+ if (args.help) {
114
+ log(HELP.trim())
115
+ return 0
116
+ }
117
+ if (args.listThemes) {
118
+ for (const theme of listThemes('zh')) {
119
+ log('- ' + theme.id.padEnd(8) + theme.name + '|' + theme.mood + '|适合:' + theme.bestFor)
120
+ }
121
+ return 0
122
+ }
123
+ if (args.listTemplates) { log(JSON.stringify({ templates: listTemplates(args.lang) }, null, 2)); return 0 }
124
+ if ([args.edit, args.undo, args.check, args.render].filter(Boolean).length > 1) throw new Error('dsh-ppt CLI:--edit / --undo / --check / --render 只能选择一项')
125
+ if (args.renderReceipt !== undefined && !args.check) throw new Error('dsh-ppt CLI:--render-receipt 需配合 --check')
126
+ if (!args.render && [args.format, args.width, args.timeoutMs].some(v => v !== undefined)) throw new Error('dsh-ppt CLI:--format / --width / --timeout 需配合 --render')
127
+ if (args.check) {
128
+ const report = await checkDeckAsync({ deckPath: args.check, renderReceipt: args.renderReceipt })
129
+ log(JSON.stringify(report, null, 2)); return report.ok && (args.renderReceipt === undefined || report.render?.status === 'rendered') ? 0 : 1
130
+ }
131
+ if (args.render) {
132
+ const rendered = await (io.renderDeck ?? renderDeck)({ pptxPath: args.render, ...(args.explicitOut ? { outputDir: args.out } : {}), format: args.format, width: args.width, timeoutMs: args.timeoutMs })
133
+ log(JSON.stringify(rendered, null, 2)); return rendered.ok ? 0 : 1
134
+ }
135
+ if (args.edit || args.undo) {
136
+ const result = args.edit ? await editDeck({ deckPath: args.edit, edits: args.edits, brand: args.brand, expectedRevision: args.revision })
137
+ : await undoDeck({ deckPath: args.undo, expectedRevision: args.revision })
138
+ log(JSON.stringify(result, null, 2)); return 0
139
+ }
140
+
141
+ let content = String(args.content ?? '')
142
+ let slides = args.slides
143
+ if (content === '@-') {
144
+ content = await readStdin(io.stdin ?? process.stdin)
145
+ } else if (content.startsWith('@')) {
146
+ content = readFileSync(resolvePath(content.slice(1)), 'utf8')
147
+ } else if (content.trim() !== '' && existsSync(resolvePath(content.trim()))) {
148
+ // README / SKILL.md 的示例直接传文件路径(不带 @)时也按文件读取
149
+ content = readFileSync(resolvePath(content.trim()), 'utf8')
150
+ } else if (content.trim() === '' && slides === null && !args.template) {
151
+ throw new Error('dsh-ppt CLI:--content 不能为空(或用 --slides 传结构化幻灯片)')
152
+ }
153
+
154
+ const options = {
155
+ title: args.title,
156
+ content,
157
+ slides,
158
+ theme: args.theme,
159
+ lang: args.lang,
160
+ motion: args.motion,
161
+ outputDir: resolvePath(args.out || '.'),
162
+ fileName: args.file,
163
+ overwrite: args.overwrite,
164
+ template: args.template,
165
+ brand: args.brand,
166
+ }
167
+ const result = await buildDeckAsync(options)
168
+ log('dsh-ppt 已生成 ' + result.slideCount + ' 页演示文稿:')
169
+ log(' HTML 放映:' + result.htmlPath)
170
+ log(' PPTX 导出:' + result.pptxPath)
171
+ log(' Manifest :' + result.jsonPath)
172
+ if (result.deliveryStatus === 'draft') log(' 草稿:还有待填写内容,不可作为成品交付。')
173
+ log(' 质量检查:' + result.quality.errorCount + ' 个错误 / ' + result.quality.warningCount + ' 个提醒;修订号 ' + result.revision)
174
+ return 0
175
+ }
176
+
177
+ if (import.meta.url === pathToFileURL(process.argv[1] ?? '').href) {
178
+ main().then((code) => {
179
+ process.exitCode = code ?? 0
180
+ }).catch((err) => {
181
+ console.error('[dsh-ppt] ' + (err instanceof Error ? err.message : String(err)))
182
+ process.exitCode = 1
183
+ })
184
+ }
@@ -312,13 +312,14 @@ async function generate(manifest, signal) {
312
312
  manifest.quality = checkManifest(manifest)
313
313
  if (manifest.quality.errorCount) fail(manifest.quality.issues.filter(i => i.severity === 'error').map(i => `第 ${i.slide} 页:${i.message}`).join(';'))
314
314
  const theme = brandedTheme(manifest), language = resolveLanguage(manifest.language)
315
- const json = documentBytes(manifest), html = renderHtml(manifest, theme, language)
315
+ const json = documentBytes(manifest)
316
316
  const pptx = await exportPptx(manifest, theme, language, signal)
317
317
  abort(signal)
318
- return { json, html, pptx }
318
+ return { json, pptx, manifest, theme, language }
319
319
  }
320
320
  function commit(paths, data) {
321
- commitDeckArtifacts([{ path: paths.json, data: data.json, encoding: 'utf8' }, { path: paths.html, data: data.html, encoding: 'utf8' }, { path: paths.pptx, data: data.pptx }])
321
+ const html = renderHtml(data.manifest, data.theme, data.language, paths)
322
+ commitDeckArtifacts([{ path: paths.json, data: data.json, encoding: 'utf8' }, { path: paths.html, data: html, encoding: 'utf8' }, { path: paths.pptx, data: data.pptx }])
322
323
  }
323
324
 
324
325
  export async function buildDeckAsync(options = {}) {
@@ -369,6 +370,12 @@ function readDocument(pathInput, cwd = '.') {
369
370
  }
370
371
  return { manifest: document, path, fingerprint: hash(bytes) }
371
372
  }
373
+ /** Validated project snapshot for the operator workbench; no file is modified. */
374
+ export function readDeckProject(options = {}) {
375
+ abort(options.signal)
376
+ const { manifest, path } = readDocument(options.deckPath, options.cwd)
377
+ return { manifest: clone(manifest), path }
378
+ }
372
379
  function expected(manifest, revision) {
373
380
  if (revision !== undefined && (!Number.isInteger(revision) || revision !== manifest.revision)) fail(`修订号冲突:当前 revision=${manifest.revision},请重新读取工程后修改`)
374
381
  }
@@ -389,7 +396,7 @@ export async function editDeck(options = {}) {
389
396
  expected(current, options.expectedRevision)
390
397
  const next = clone(current)
391
398
  if (!Array.isArray(options.edits) || !options.edits.length) {
392
- if (options.brand === undefined) fail('请提供 edits 或 brand')
399
+ if (options.brand === undefined && options.order === undefined) fail('请提供 edits、order 或 brand')
393
400
  }
394
401
  const used = new Set(), allowed = new Set(['layout', 'title', 'subtitle', 'kicker', 'text', 'bullets', 'rows', 'notes', 'image', 'chart'])
395
402
  for (const edit of options.edits ?? []) {
@@ -412,6 +419,12 @@ export async function editDeck(options = {}) {
412
419
  const embedded = embedSlides(slides, next.assets, resolve(options.cwd || '.'))[0]
413
420
  next.slides[index] = { ...embedded, id: current.slides[index].id }
414
421
  }
422
+ if (options.order !== undefined) {
423
+ if (!Array.isArray(options.order) || options.order.length !== next.slides.length || options.order.some(id => typeof id !== 'string') || new Set(options.order).size !== next.slides.length) fail('order 必须包含每个页面 ID,且各出现一次')
424
+ const byId = new Map(next.slides.map(slide => [slide.id, slide]))
425
+ if (options.order.some(id => !byId.has(id))) fail('order 含有不属于本工程的页面 ID')
426
+ next.slides = options.order.map(id => byId.get(id))
427
+ }
415
428
  if (options.brand !== undefined) next.brand = normalizeBrand(options.brand === null ? null : { ...next.brand, ...options.brand,
416
429
  ...(options.brand?.logo === null ? { logo: undefined, logoAssetId: undefined } : {}) }, next.assets, resolve(options.cwd || '.'))
417
430
  if (JSON.stringify(snapshot(next)) === JSON.stringify(snapshot(current))) return { ...result(current, pathsFor(state.path)), changed: false }