@deckflow/deckuse-pptx 1.0.0 → 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.
Files changed (79) hide show
  1. package/LICENSE +662 -0
  2. package/dist/.tsbuildinfo +1 -1
  3. package/dist/adapter.d.ts +70 -3
  4. package/dist/adapter.d.ts.map +1 -1
  5. package/dist/adapter.js +519 -86
  6. package/dist/adapter.js.map +1 -1
  7. package/dist/addressing.d.ts +45 -0
  8. package/dist/addressing.d.ts.map +1 -0
  9. package/dist/addressing.js +401 -0
  10. package/dist/addressing.js.map +1 -0
  11. package/dist/chart-classify.d.ts +10 -0
  12. package/dist/chart-classify.d.ts.map +1 -0
  13. package/dist/chart-classify.js +54 -0
  14. package/dist/chart-classify.js.map +1 -0
  15. package/dist/chart.d.ts +33 -0
  16. package/dist/chart.d.ts.map +1 -0
  17. package/dist/chart.js +492 -0
  18. package/dist/chart.js.map +1 -0
  19. package/dist/edition.d.ts +20 -0
  20. package/dist/edition.d.ts.map +1 -0
  21. package/dist/edition.js +46 -0
  22. package/dist/edition.js.map +1 -0
  23. package/dist/elements.d.ts.map +1 -1
  24. package/dist/elements.js +61 -46
  25. package/dist/elements.js.map +1 -1
  26. package/dist/hyperlink.d.ts +7 -0
  27. package/dist/hyperlink.d.ts.map +1 -0
  28. package/dist/hyperlink.js +83 -0
  29. package/dist/hyperlink.js.map +1 -0
  30. package/dist/index-sync.d.ts +7 -0
  31. package/dist/index-sync.d.ts.map +1 -0
  32. package/dist/index-sync.js +24 -0
  33. package/dist/index-sync.js.map +1 -0
  34. package/dist/index.d.ts +2 -0
  35. package/dist/index.d.ts.map +1 -1
  36. package/dist/index.js +2 -0
  37. package/dist/index.js.map +1 -1
  38. package/dist/indexer.d.ts +4 -0
  39. package/dist/indexer.d.ts.map +1 -1
  40. package/dist/indexer.js +105 -27
  41. package/dist/indexer.js.map +1 -1
  42. package/dist/media.d.ts +15 -0
  43. package/dist/media.d.ts.map +1 -0
  44. package/dist/media.js +141 -0
  45. package/dist/media.js.map +1 -0
  46. package/dist/mutations.d.ts +8 -2
  47. package/dist/mutations.d.ts.map +1 -1
  48. package/dist/mutations.js +695 -72
  49. package/dist/mutations.js.map +1 -1
  50. package/dist/picture.d.ts.map +1 -1
  51. package/dist/picture.js +1 -3
  52. package/dist/picture.js.map +1 -1
  53. package/dist/properties.d.ts +6 -1
  54. package/dist/properties.d.ts.map +1 -1
  55. package/dist/properties.js +159 -10
  56. package/dist/properties.js.map +1 -1
  57. package/dist/resolve-properties.d.ts +17 -0
  58. package/dist/resolve-properties.d.ts.map +1 -0
  59. package/dist/resolve-properties.js +545 -0
  60. package/dist/resolve-properties.js.map +1 -0
  61. package/dist/slides.d.ts +3 -1
  62. package/dist/slides.d.ts.map +1 -1
  63. package/dist/slides.js +77 -4
  64. package/dist/slides.js.map +1 -1
  65. package/dist/table.d.ts +15 -0
  66. package/dist/table.d.ts.map +1 -0
  67. package/dist/table.js +456 -0
  68. package/dist/table.js.map +1 -0
  69. package/dist/types.d.ts +2 -1
  70. package/dist/types.d.ts.map +1 -1
  71. package/dist/workspace.d.ts +9 -9
  72. package/dist/workspace.d.ts.map +1 -1
  73. package/dist/workspace.js +67 -47
  74. package/dist/workspace.js.map +1 -1
  75. package/dist/xml.d.ts +7 -0
  76. package/dist/xml.d.ts.map +1 -1
  77. package/dist/xml.js +86 -13
  78. package/dist/xml.js.map +1 -1
  79. package/package.json +11 -3
package/dist/adapter.js CHANGED
@@ -1,23 +1,84 @@
1
- import { randomUUID } from 'node:crypto';
2
- import { copyFile, mkdir, readFile, rename } from 'node:fs/promises';
3
- import { basename, dirname, join, resolve } from 'node:path';
1
+ import { copyFile } from 'node:fs/promises';
2
+ import { resolve } from 'node:path';
4
3
  import { err, ok, } from '@deckflow/deckuse-core';
5
4
  import { OpcArchive } from '@deckflow/deckuse-opc';
6
- import { buildIndex, findIndexed, matchesSelector } from './indexer.js';
5
+ import { isIntegerRevision, nextRevision } from '@deckflow/deckuse-workspace';
6
+ import { cNvPrIdOf, resolveTarget, targetPathForItem, uidForItem, } from './addressing.js';
7
+ import { editionMetadata } from './edition.js';
8
+ import { buildIndex, findIndexed, matchesSelector, mergeSlides } from './indexer.js';
9
+ import { loadIndex } from './index-sync.js';
7
10
  import { mutate } from './mutations.js';
8
- import { deckuseDir, packagePath, persist, readIndex, readManifest, revision, } from './workspace.js';
9
- const VERSION = '0.3.0';
11
+ import { resolveProperties } from './resolve-properties.js';
12
+ import { initializeWorkspace, packagePath, persistWrite, readHistory, readManifest, revision, sourceDir, undoWrites, withWriteLock, } from './workspace.js';
13
+ const VERSION = '0.5.0';
14
+ const WRITE_TYPES = new Set([
15
+ 'setText',
16
+ 'replaceText',
17
+ 'setTransform',
18
+ 'setProperties',
19
+ 'set',
20
+ 'xfrmSet',
21
+ 'zMove',
22
+ 'add',
23
+ 'addSlide',
24
+ 'addShape',
25
+ 'remove',
26
+ 'replacePicture',
27
+ 'duplicate',
28
+ 'batch',
29
+ 'applyTransaction',
30
+ ]);
10
31
  export const pptxCapabilities = {
32
+ protocol: '2.0',
33
+ ...editionMetadata,
11
34
  slides: { add: true, duplicate: true, remove: true },
12
- elements: ['shape', 'textbox', 'connector', 'group', 'picture', 'table', 'chart'],
35
+ // Master/layout/theme: list + resolve only in community; writes gated in mutations.
36
+ masters: { list: true, edit: false },
37
+ layouts: { list: true, edit: false },
38
+ theme: { list: true, edit: false },
39
+ elements: [
40
+ 'shape',
41
+ 'textbox',
42
+ 'connector',
43
+ 'group',
44
+ 'picture',
45
+ 'video',
46
+ 'audio',
47
+ 'table',
48
+ 'chart',
49
+ ],
13
50
  pictureInput: ['base64', 'path'],
14
51
  picture: { replacePicture: true, cleanupUnreferencedMedia: true },
15
- chart: { title: true, seriesCache: true, embeddedWorkbook: false },
52
+ media: { video: true, audio: true, autoPoster: true },
53
+ chart: {
54
+ create: true,
55
+ families: ['bar', 'column', 'line', 'pie'],
56
+ basicOnly: true,
57
+ title: true,
58
+ seriesCache: true,
59
+ seriesColor: true,
60
+ textColor: true,
61
+ gapWidth: true,
62
+ showMajorGridlines: true,
63
+ fill: true,
64
+ embeddedWorkbook: false,
65
+ },
66
+ table: {
67
+ insertRow: true,
68
+ deleteRow: true,
69
+ insertColumn: true,
70
+ deleteColumn: true,
71
+ cellFill: true,
72
+ },
73
+ placeholder: { index: true, address: true, roleOnAdd: true },
74
+ hyperlink: { set: true, clear: true },
75
+ notes: { ensureOnWrite: true },
16
76
  properties: {
17
77
  text: true,
18
78
  name: true,
19
79
  srgbClr: true,
20
80
  fill: true,
81
+ fillTransparency: true,
21
82
  stroke: true,
22
83
  textColor: true,
23
84
  fontFamily: true,
@@ -26,6 +87,30 @@ export const pptxCapabilities = {
26
87
  italic: true,
27
88
  underline: true,
28
89
  hidden: true,
90
+ paragraphAlign: true,
91
+ paragraphLevel: true,
92
+ bullet: true,
93
+ hyperlink: true,
94
+ dotted: [
95
+ 'font.family',
96
+ 'font.size',
97
+ 'font.weight',
98
+ 'font.italic',
99
+ 'font.underline',
100
+ 'font.color',
101
+ 'fill.color',
102
+ 'fill.kind',
103
+ 'fill.transparency',
104
+ 'line.color',
105
+ 'line.kind',
106
+ 'line.width',
107
+ 'text.value',
108
+ 'visible',
109
+ 'paragraph.align',
110
+ 'paragraph.level',
111
+ 'bullet',
112
+ 'hyperlink',
113
+ ],
29
114
  aliases: {
30
115
  stroke: ['border', 'outline', 'line'],
31
116
  fontFamily: ['font', 'typeface'],
@@ -34,23 +119,206 @@ export const pptxCapabilities = {
34
119
  },
35
120
  },
36
121
  query: { matchAll: ['*', 'all'], textRegex: true, hasText: true },
37
- text: { setText: true, replaceText: true },
38
- commit: { overwrite: true },
122
+ text: { setText: true, replaceText: true, multiParagraph: true },
123
+ history: { undo: true },
124
+ addressing: { targetPath: true, uid: true, placeholder: true, notes: true },
125
+ resolve: { effective: true, direct: true, both: true, provenance: true },
39
126
  preservation: 'unknown parts and untouched XML are preserved; ZIP entries are recompressed',
40
127
  };
128
+ const openWorkspaceArchive = async (workspace) => OpcArchive.openDirectory(sourceDir(workspace));
129
+ const validateArchive = (archive) => {
130
+ const diagnostics = [];
131
+ for (const [source, rels] of archive.relationships)
132
+ for (const rel of rels)
133
+ if (!rel.external && rel.resolvedTarget && !archive.getPart(rel.resolvedTarget))
134
+ diagnostics.push({
135
+ severity: 'error',
136
+ code: 'BROKEN_RELATIONSHIP',
137
+ message: `Missing target ${rel.resolvedTarget}`,
138
+ details: { source, relationshipId: rel.id },
139
+ });
140
+ return diagnostics;
141
+ };
142
+ const slidesFromOutcome = (outcome) => outcome?.slides ?? [];
143
+ const assertExpectRevision = (command, current) => {
144
+ if (command.expectRevision !== undefined) {
145
+ const expected = String(command.expectRevision);
146
+ if (expected !== current && expected !== 'latest')
147
+ return err('TRANSACTION_CONFLICT', `Expected revision ${expected}, current ${current}`);
148
+ }
149
+ if (command.transactionId !== undefined &&
150
+ command.transactionId !== current &&
151
+ command.transactionId !== 'latest')
152
+ return err('TRANSACTION_CONFLICT', `Expected transactionId ${current}`);
153
+ return undefined;
154
+ };
155
+ const listResource = (index, resource, slide) => {
156
+ if (resource === 'slides') {
157
+ return index.elements
158
+ .filter((item) => item.kind === 'slide')
159
+ .map((item, i) => ({
160
+ target: `slide:${i + 1}`,
161
+ uid: uidForItem(item),
162
+ index: i + 1,
163
+ slideId: item.slideId,
164
+ name: item.name,
165
+ titlePreview: item.text?.slice(0, 80),
166
+ partUri: item.partUri,
167
+ }));
168
+ }
169
+ if (resource === 'shapes') {
170
+ if (slide === undefined)
171
+ return err('INVALID_COMMAND', 'list shapes requires --slide', [], {
172
+ hint: 'deckuse list shapes --slide <n> --json',
173
+ });
174
+ const slides = index.elements.filter((item) => item.kind === 'slide');
175
+ const slideItem = slides[slide - 1];
176
+ if (!slideItem)
177
+ return err('TARGET_NOT_FOUND', `slide:${slide} does not exist`, [], {
178
+ target: `slide:${slide}`,
179
+ });
180
+ return index.elements
181
+ .filter((item) => item.partUri === slideItem.partUri &&
182
+ !['slide', 'notes', 'master', 'layout', 'theme', 'tableCell'].includes(item.kind))
183
+ .map((item, z) => ({
184
+ target: targetPathForItem(index, item),
185
+ uid: uidForItem(item),
186
+ id: cNvPrIdOf(item),
187
+ name: item.name,
188
+ kind: item.kind,
189
+ role: item.payload?.['placeholder'] ?? item.payload?.['role'],
190
+ bbox: item.transform,
191
+ zOrder: z,
192
+ textPreview: item.text?.slice(0, 80),
193
+ parentId: item.parentId,
194
+ }));
195
+ }
196
+ if (resource === 'layouts' || resource === 'masters') {
197
+ const kind = resource === 'layouts' ? 'layout' : 'master';
198
+ return index.elements
199
+ .filter((item) => item.kind === kind)
200
+ .map((item) => ({
201
+ target: targetPathForItem(index, item),
202
+ uid: uidForItem(item),
203
+ name: item.partUri.split('/').pop()?.replace(/\.xml$/, ''),
204
+ partUri: item.partUri,
205
+ }));
206
+ }
207
+ return index.elements
208
+ .filter((item) => item.kind === 'theme')
209
+ .map((item) => ({
210
+ target: 'theme',
211
+ uid: uidForItem(item),
212
+ partUri: item.partUri,
213
+ colors: item.payload?.['colors'],
214
+ }));
215
+ };
216
+ const transactionOpsToAtomic = (ops, workspaceId, transactionId) => {
217
+ const commands = [];
218
+ for (const op of ops) {
219
+ const kind = String(op['op'] ?? op['type'] ?? '');
220
+ const base = {
221
+ version: '2.0',
222
+ workspaceId,
223
+ transactionId,
224
+ };
225
+ if (kind === 'add-shape' || kind === 'addShape') {
226
+ const shapeType = String(op['shapeType'] ?? op['type'] ?? 'text');
227
+ const allowed = new Set([
228
+ 'text',
229
+ 'rect',
230
+ 'rounded-rect',
231
+ 'ellipse',
232
+ 'line',
233
+ 'image',
234
+ 'group',
235
+ 'table',
236
+ 'chart',
237
+ 'video',
238
+ 'audio',
239
+ ]);
240
+ if (!allowed.has(shapeType))
241
+ return err('UNSUPPORTED_CAPABILITY', `Unsupported shape type: ${shapeType}`);
242
+ commands.push({
243
+ ...base,
244
+ type: 'addShape',
245
+ slide: Number(op['slide']),
246
+ shapeType: shapeType,
247
+ ...(typeof op['name'] === 'string' ? { name: op['name'] } : {}),
248
+ ...(typeof op['role'] === 'string' ? { role: op['role'] } : {}),
249
+ ...(typeof op['x'] === 'number' ? { x: op['x'] } : {}),
250
+ ...(typeof op['y'] === 'number' ? { y: op['y'] } : {}),
251
+ ...(typeof op['width'] === 'number' ? { width: op['width'] } : {}),
252
+ ...(typeof op['height'] === 'number' ? { height: op['height'] } : {}),
253
+ ...(typeof op['file'] === 'string' ? { file: op['file'] } : {}),
254
+ ...(typeof op['text'] === 'string' ? { text: op['text'] } : {}),
255
+ ...(Array.isArray(op['rows']) ? { rows: op['rows'] } : {}),
256
+ ...(typeof op['chartType'] === 'string'
257
+ ? { chartType: op['chartType'] }
258
+ : {}),
259
+ ...(op['data'] !== undefined && typeof op['data'] === 'object' && op['data'] !== null
260
+ ? {
261
+ data: op['data'],
262
+ }
263
+ : {}),
264
+ });
265
+ continue;
266
+ }
267
+ if (kind === 'set-text' || kind === 'setText') {
268
+ commands.push({
269
+ ...base,
270
+ type: 'setText',
271
+ target: String(op['target']),
272
+ value: String(op['value'] ?? op['text'] ?? ''),
273
+ });
274
+ continue;
275
+ }
276
+ if (kind === 'set') {
277
+ const { op: _op, type: _type, target, ...rest } = op;
278
+ const properties = op['properties'] ?? rest;
279
+ const cleaned = { ...properties };
280
+ delete cleaned['target'];
281
+ delete cleaned['op'];
282
+ delete cleaned['type'];
283
+ commands.push({
284
+ ...base,
285
+ type: 'set',
286
+ target: String(target),
287
+ properties: cleaned,
288
+ scope: 'local',
289
+ });
290
+ continue;
291
+ }
292
+ if (kind === 'add-slide' || kind === 'addSlide') {
293
+ commands.push({
294
+ ...base,
295
+ type: 'addSlide',
296
+ ...(typeof op['after'] === 'number' ? { after: op['after'] } : {}),
297
+ ...(typeof op['layout'] === 'string' ? { layout: op['layout'] } : {}),
298
+ ...(typeof op['name'] === 'string' ? { name: op['name'] } : {}),
299
+ });
300
+ continue;
301
+ }
302
+ if (kind === 'remove') {
303
+ commands.push({ ...base, type: 'remove', target: String(op['target']) });
304
+ continue;
305
+ }
306
+ return err('UNSUPPORTED_CAPABILITY', `Unsupported transaction op: ${kind || '(missing)'}`);
307
+ }
308
+ return ok(commands);
309
+ };
41
310
  export const pptxAdapter = {
42
311
  format: 'pptx',
43
312
  version: VERSION,
44
313
  async init(command) {
45
314
  try {
46
315
  const workspace = resolve(command.workspaceId);
47
- await mkdir(deckuseDir(workspace), { recursive: true });
48
316
  const archive = await OpcArchive.openFile(resolve(command.source));
49
317
  if (!archive.getPart('/ppt/presentation.xml'))
50
318
  return err('VALIDATION_FAILED', 'Not a PPTX presentation');
51
319
  const created = new Date().toISOString(), rev = revision();
52
320
  const manifest = {
53
- schemaVersion: '1.0',
321
+ schemaVersion: '2.0',
54
322
  workspaceId: workspace,
55
323
  format: 'pptx',
56
324
  source: resolve(command.source),
@@ -61,8 +329,15 @@ export const pptxAdapter = {
61
329
  files: [],
62
330
  metadata: { capabilities: pptxCapabilities },
63
331
  };
64
- const saved = await persist(workspace, archive, manifest, buildIndex(archive, workspace, rev), { type: 'init', source: command.source });
65
- return ok(saved);
332
+ const index = buildIndex(archive, workspace, rev);
333
+ const saved = await initializeWorkspace(workspace, archive, manifest, index);
334
+ return ok({
335
+ workspaceId: saved.workspaceId,
336
+ format: saved.format,
337
+ source: saved.source,
338
+ revision: saved.revision,
339
+ elementCount: index.elements.length,
340
+ });
66
341
  }
67
342
  catch (cause) {
68
343
  return err('IO_ERROR', cause instanceof Error ? cause.message : 'PPTX init failed');
@@ -71,15 +346,135 @@ export const pptxAdapter = {
71
346
  async execute(command) {
72
347
  const workspace = resolve(command.workspaceId);
73
348
  try {
74
- const manifest = await readManifest(workspace), archive = await OpcArchive.openFile(packagePath(workspace));
75
- let index;
76
- try {
77
- index = await readIndex(workspace);
349
+ if (command.type === 'undo') {
350
+ return await withWriteLock(workspace, async () => {
351
+ try {
352
+ const result = await undoWrites(workspace, command.steps);
353
+ return ok(result);
354
+ }
355
+ catch (cause) {
356
+ return err('VALIDATION_FAILED', cause instanceof Error ? cause.message : 'Undo failed');
357
+ }
358
+ });
359
+ }
360
+ if (command.type === 'history') {
361
+ const { records, total } = await readHistory(workspace, command.limit, command.offset);
362
+ const filtered = command.slide !== undefined
363
+ ? records.filter((record) => record.slides.includes(command.slide))
364
+ : records;
365
+ return ok({ records: filtered, total: command.slide !== undefined ? filtered.length : total });
78
366
  }
79
- catch {
80
- index = buildIndex(archive, manifest.workspaceId, manifest.revision);
367
+ const manifest = await readManifest(workspace);
368
+ if (manifest.schemaVersion !== '2.0' || !isIntegerRevision(manifest.revision)) {
369
+ if (command.type === 'status') {
370
+ return ok({
371
+ workspaceId: workspace,
372
+ format: manifest.format,
373
+ revision: manifest.revision,
374
+ schemaVersion: manifest.schemaVersion,
375
+ compatible: false,
376
+ hint: 'Re-run deckuse init to upgrade this workspace to protocol 2.0.',
377
+ capabilities: manifest.metadata?.['capabilities'],
378
+ });
379
+ }
380
+ }
381
+ const archive = await openWorkspaceArchive(workspace);
382
+ const index = await loadIndex(workspace, archive, manifest, { persist: true });
383
+ if (command.type === 'status') {
384
+ return ok({
385
+ workspaceId: workspace,
386
+ format: manifest.format,
387
+ revision: manifest.revision,
388
+ schemaVersion: manifest.schemaVersion,
389
+ compatible: true,
390
+ adapterVersion: manifest.adapterVersion,
391
+ source: manifest.source,
392
+ package: packagePath(workspace),
393
+ elementCount: index.elements.length,
394
+ capabilities: pptxCapabilities,
395
+ ...editionMetadata,
396
+ branch: 'main',
397
+ });
398
+ }
399
+ if (command.type === 'list') {
400
+ const listed = listResource(index, command.resource, command.slide);
401
+ if (listed && typeof listed === 'object' && 'ok' in listed && listed.ok === false)
402
+ return listed;
403
+ return ok({ resource: command.resource, items: listed });
404
+ }
405
+ if (command.type === 'get') {
406
+ const resolved = resolveTarget(index, command.target);
407
+ if (!resolved.ok)
408
+ return resolved;
409
+ const details = resolveProperties(archive, resolved.value, {
410
+ resolve: command.resolve,
411
+ ...(command.props ? { props: command.props } : {}),
412
+ provenance: command.provenance ?? true,
413
+ });
414
+ return ok(details);
415
+ }
416
+ if (command.type === 'search') {
417
+ const items = index.elements.filter((item) => {
418
+ if (command.kind === 'text') {
419
+ if (!item.text)
420
+ return false;
421
+ if (command.query && !item.text.toLowerCase().includes(command.query.toLowerCase()))
422
+ return false;
423
+ }
424
+ else {
425
+ if (command.name && item.name !== command.name)
426
+ return false;
427
+ if (command.query &&
428
+ !(item.name ?? '').toLowerCase().includes(command.query.toLowerCase()))
429
+ return false;
430
+ }
431
+ return true;
432
+ });
433
+ if (command.kind === 'shape' && command.name) {
434
+ const named = items.filter((item) => item.name === command.name);
435
+ if (named.length > 1)
436
+ return err('AMBIGUOUS_NAME', `Shape name is ambiguous: ${command.name}`);
437
+ }
438
+ return ok({
439
+ matches: items.slice(0, command.limit).map((item) => ({
440
+ target: targetPathForItem(index, item),
441
+ uid: uidForItem(item),
442
+ name: item.name,
443
+ kind: item.kind,
444
+ text: item.text?.slice(0, 200),
445
+ context: item.partUri,
446
+ })),
447
+ });
81
448
  }
82
449
  if (command.type === 'inspect') {
450
+ if (command.target) {
451
+ const resolved = resolveTarget(index, command.target);
452
+ if (!resolved.ok)
453
+ return resolved;
454
+ const details = resolveProperties(archive, resolved.value, {
455
+ resolve: 'both',
456
+ provenance: true,
457
+ });
458
+ return ok({
459
+ target: resolved.value.target,
460
+ uid: resolved.value.uid,
461
+ kind: resolved.value.item.kind,
462
+ name: resolved.value.item.name,
463
+ partUri: resolved.value.item.partUri,
464
+ transform: resolved.value.item.transform,
465
+ visualTree: command.visualTree
466
+ ? index.elements
467
+ .filter((item) => item.partUri === resolved.value.item.partUri)
468
+ .map((item) => ({
469
+ target: targetPathForItem(index, item),
470
+ kind: item.kind,
471
+ name: item.name,
472
+ }))
473
+ : undefined,
474
+ properties: details.properties,
475
+ warnings: details.warnings,
476
+ });
477
+ }
83
478
  if (command.ref) {
84
479
  const item = findIndexed(index, command.ref);
85
480
  return item ? ok(item) : err('ELEMENT_NOT_FOUND', 'Element reference was not found');
@@ -98,88 +493,126 @@ export const pptxAdapter = {
98
493
  return item
99
494
  ? ok({
100
495
  ref: item.ref,
496
+ target: targetPathForItem(index, item),
497
+ uid: uidForItem(item),
101
498
  slide: item.slideId,
102
499
  text: item.text ?? '',
103
500
  kind: item.kind,
104
501
  geometry: item.transform,
105
502
  location: item.location,
106
- payload: item.kind === 'chart' || item.kind === 'picture' ? item.payload : undefined,
503
+ payload: item.kind === 'chart' ||
504
+ item.kind === 'picture' ||
505
+ item.kind === 'video' ||
506
+ item.kind === 'audio'
507
+ ? item.payload
508
+ : undefined,
107
509
  })
108
510
  : err('ELEMENT_NOT_FOUND', 'Element reference was not found');
109
511
  }
110
512
  if (command.type === 'validate') {
111
- const diagnostics = [];
112
- for (const [source, rels] of archive.relationships)
113
- for (const rel of rels)
114
- if (!rel.external && rel.resolvedTarget && !archive.getPart(rel.resolvedTarget))
115
- diagnostics.push({
116
- severity: 'error',
117
- code: 'BROKEN_RELATIONSHIP',
118
- message: `Missing target ${rel.resolvedTarget}`,
119
- details: { source, relationshipId: rel.id },
120
- });
513
+ const diagnostics = validateArchive(archive);
121
514
  return diagnostics.length
122
515
  ? err('VALIDATION_FAILED', 'PPTX validation failed', diagnostics)
123
- : ok({ valid: true, revision: index.revision, capabilities: pptxCapabilities });
516
+ : ok({
517
+ valid: true,
518
+ revision: index.revision,
519
+ capabilities: pptxCapabilities,
520
+ });
124
521
  }
125
- if (command.type === 'commit') {
126
- if (command.transactionId !== manifest.revision && command.transactionId !== 'latest')
127
- return err('TRANSACTION_CONFLICT', `Expected transactionId ${manifest.revision}`);
128
- const destination = resolve(command.destination ??
129
- join(dirname(manifest.source), `${basename(manifest.source, '.pptx')}.deckuse.pptx`));
130
- if (!command.overwrite) {
131
- try {
132
- await readFile(destination);
133
- return err('IO_ERROR', `Destination exists: ${destination}`);
134
- }
135
- catch (error) {
136
- if (error.code !== 'ENOENT')
137
- throw error;
138
- }
139
- }
140
- await mkdir(dirname(destination), { recursive: true });
141
- const temp = `${destination}.${randomUUID()}.tmp`;
142
- await copyFile(packagePath(workspace), temp);
143
- await OpcArchive.openFile(temp);
144
- await rename(temp, destination);
145
- return ok({ destination, revision: manifest.revision });
522
+ if (command.type === 'export') {
523
+ if (command.revision !== undefined && String(command.revision) !== manifest.revision)
524
+ return err('UNSUPPORTED_CAPABILITY', 'Export of historical revisions is not available in Phase 1a', [], { hint: 'Omit --revision to export the current workspace package.' });
525
+ const output = resolve(command.output);
526
+ await copyFile(packagePath(workspace), output);
527
+ return ok({
528
+ output,
529
+ revision: manifest.revision,
530
+ });
146
531
  }
147
- if (command.transactionId !== manifest.revision && command.transactionId !== 'latest')
148
- return err('TRANSACTION_CONFLICT', `Expected transactionId ${manifest.revision}`);
149
- if (command.type === 'batch') {
150
- const working = await OpcArchive.open(await archive.toUint8Array()), results = [], diagnostics = [];
151
- for (const nested of command.commands) {
152
- const result = await mutate(nested, working, index);
153
- diagnostics.push(...result.diagnostics);
154
- if (!result.ok) {
155
- if (!command.atomic && results.length) {
156
- const rev = revision();
157
- await persist(workspace, working, manifest, buildIndex(working, manifest.workspaceId, rev), { ...command, partial: true, failed: nested.type });
158
- return err('VALIDATION_FAILED', 'Non-atomic batch partially applied', [
159
- ...diagnostics,
160
- {
161
- severity: 'warning',
162
- code: 'PARTIAL_BATCH_APPLIED',
163
- message: `${String(results.length)} operation(s) were committed before failure`,
164
- },
165
- ]);
166
- }
167
- return result;
532
+ if (!WRITE_TYPES.has(command.type))
533
+ return err('INVALID_COMMAND', `Unsupported command type: ${command.type}`);
534
+ const conflict = assertExpectRevision(command, manifest.revision);
535
+ if (conflict)
536
+ return conflict;
537
+ return await withWriteLock(workspace, async () => {
538
+ const currentManifest = await readManifest(workspace);
539
+ const locked = assertExpectRevision(command, currentManifest.revision);
540
+ if (locked)
541
+ return locked;
542
+ const working = await openWorkspaceArchive(workspace);
543
+ const currentIndex = await loadIndex(workspace, working, currentManifest);
544
+ const runBatch = async (nestedCommands, dryRun, operation) => {
545
+ const results = [];
546
+ const diagnostics = [];
547
+ const slidePages = [];
548
+ const changedTargets = [];
549
+ const changedParts = [];
550
+ for (const nested of nestedCommands) {
551
+ const result = await mutate(nested, working, currentIndex);
552
+ diagnostics.push(...result.diagnostics);
553
+ if (!result.ok)
554
+ return result;
555
+ results.push(result.value);
556
+ slidePages.push(...slidesFromOutcome(result.value));
557
+ if (result.value.changedTargets)
558
+ changedTargets.push(...result.value.changedTargets);
559
+ if (result.value.changedParts)
560
+ changedParts.push(...result.value.changedParts);
168
561
  }
169
- results.push(result.value);
562
+ const validation = validateArchive(working);
563
+ if (validation.length)
564
+ return err('VALIDATION_FAILED', 'PPTX validation failed', validation);
565
+ if (dryRun)
566
+ return ok({
567
+ results,
568
+ revision: currentManifest.revision,
569
+ dryRun: true,
570
+ changedTargets: [...new Set(changedTargets)],
571
+ changedParts: [...new Set(changedParts)],
572
+ affectedSlides: mergeSlides(slidePages),
573
+ }, diagnostics);
574
+ const rev = nextRevision(currentManifest.revision);
575
+ const nextIndex = buildIndex(working, currentManifest.workspaceId, rev);
576
+ const saved = await persistWrite(workspace, working, currentManifest, nextIndex, operation, mergeSlides(slidePages));
577
+ return ok({
578
+ results,
579
+ revision: saved.revision,
580
+ changedTargets: [...new Set(changedTargets)],
581
+ changedParts: [...new Set(changedParts)],
582
+ affectedSlides: mergeSlides(slidePages),
583
+ }, diagnostics);
584
+ };
585
+ if (command.type === 'applyTransaction') {
586
+ const mapped = transactionOpsToAtomic(command.operations, command.workspaceId, command.transactionId);
587
+ if (!mapped.ok)
588
+ return mapped;
589
+ return runBatch(mapped.value, command.dryRun, command);
590
+ }
591
+ if (command.type === 'batch') {
592
+ return runBatch(command.commands, command.dryRun, command);
170
593
  }
594
+ const result = await mutate(command, working, currentIndex);
595
+ if (!result.ok)
596
+ return result;
597
+ const validation = validateArchive(working);
598
+ if (validation.length)
599
+ return err('VALIDATION_FAILED', 'PPTX validation failed', validation);
171
600
  if (command.dryRun)
172
- return ok({ results, revision: manifest.revision, dryRun: true }, diagnostics);
173
- const rev = revision(), saved = await persist(workspace, working, manifest, buildIndex(working, manifest.workspaceId, rev), command);
174
- return ok({ results, revision: saved.revision }, diagnostics);
175
- }
176
- const working = await OpcArchive.open(await archive.toUint8Array()), result = await mutate(command, working, index);
177
- if (!result.ok)
178
- return result;
179
- if (command.dryRun)
180
- return ok({ ...result.value, revision: manifest.revision, dryRun: true }, result.diagnostics);
181
- const rev = revision(), saved = await persist(workspace, working, manifest, buildIndex(working, manifest.workspaceId, rev), command);
182
- return ok({ ...result.value, revision: saved.revision }, result.diagnostics);
601
+ return ok({
602
+ ...result.value,
603
+ revision: currentManifest.revision,
604
+ dryRun: true,
605
+ affectedSlides: slidesFromOutcome(result.value),
606
+ }, result.diagnostics);
607
+ const rev = nextRevision(currentManifest.revision);
608
+ const nextIndex = buildIndex(working, currentManifest.workspaceId, rev);
609
+ const saved = await persistWrite(workspace, working, currentManifest, nextIndex, command, slidesFromOutcome(result.value));
610
+ return ok({
611
+ ...result.value,
612
+ revision: saved.revision,
613
+ affectedSlides: slidesFromOutcome(result.value),
614
+ }, result.diagnostics);
615
+ });
183
616
  }
184
617
  catch (cause) {
185
618
  return err('IO_ERROR', cause instanceof Error ? cause.message : 'PPTX operation failed');