@pptx-studio/writer 0.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.
@@ -0,0 +1,818 @@
1
+ import { PartStore, ReadZipOptions, ZipArchive } from "@pptx-studio/opc";
2
+ import { Report, RuleId } from "@pptx-studio/validate";
3
+ import { XmlParseLimits } from "@pptx-studio/xml";
4
+ //#region src/errors.d.ts
5
+ /**
6
+ * The writer's own failures.
7
+ *
8
+ * There are only three, and the smallness is deliberate. Almost everything that
9
+ * can go wrong on an export already has an owner: `OpcError` for the container
10
+ * (a part with no content type, a relationship pointing at nothing), and
11
+ * `ValidateError` for the twenty-nine rules. Re-wrapping either would throw away
12
+ * the detail a caller needs - `ValidateError` carries the whole `Report` on its
13
+ * `detail` - in exchange for a uniform class name nobody dispatches on.
14
+ *
15
+ * So this file holds what is genuinely the writer's: a hook that threw, a
16
+ * garbage collection that could not be shown to be safe, and the one assertion
17
+ * the writer makes on its own output.
18
+ */
19
+ declare const WRITER_ERROR_CODES: readonly ["ERR_PREPARE_FAILED", "ERR_COLLECTION_UNSAFE", "ERR_PRESERVATION_BROKEN"];
20
+ type WriterErrorCode = (typeof WRITER_ERROR_CODES)[number];
21
+ interface WriterErrorDetail {
22
+ /** The prepare hook that threw, for `ERR_PREPARE_FAILED`. */
23
+ readonly hook?: string;
24
+ /** The part the failure is about, when it is about one. */
25
+ readonly part?: string;
26
+ /** Relationship parts that would not parse, for `ERR_COLLECTION_UNSAFE`. */
27
+ readonly blockedBy?: readonly string[];
28
+ }
29
+ declare class WriterError extends Error {
30
+ readonly name = "WriterError";
31
+ readonly code: WriterErrorCode;
32
+ readonly detail: WriterErrorDetail;
33
+ constructor(code: WriterErrorCode, message: string, detail?: WriterErrorDetail, options?: {
34
+ cause?: unknown;
35
+ });
36
+ }
37
+ declare function isWriterError(value: unknown): value is WriterError;
38
+ //#endregion
39
+ //#region src/gc/reachability.d.ts
40
+ /**
41
+ * Which parts anything can still get to, and - the part that matters - whether
42
+ * we are sure.
43
+ *
44
+ * `@pptx-studio/census` has a walk of its own and this is deliberately not it.
45
+ * The two answer the same question with opposite failure modes, and the census
46
+ * says so in its own comment: it swallows a relationship part it cannot parse
47
+ * and carries on, because a census must never refuse. That is exactly right for
48
+ * a report and exactly wrong here. A swallowed parse error means some edges are
49
+ * invisible, invisible edges make their targets look unreferenced, and
50
+ * unreferenced is what this walk is used to justify deleting. The census's
51
+ * worst case is an inaccurate line of output; ours is a user's image gone.
52
+ *
53
+ * So every failure is recorded rather than skipped, and `complete` is false if
54
+ * there was one. `collect.ts` does nothing at all unless it is true.
55
+ *
56
+ * ## Relationship parts are reachable when their source is
57
+ *
58
+ * Nothing ever *relates* to a `.rels` - the spec forbids it and PowerPoint
59
+ * refuses a package that tries, which is one of the assertions in `PartStore`.
60
+ * They are found by naming convention instead. Walking edges alone would
61
+ * therefore report every relationship part in the package as unreachable, which
62
+ * is true of the graph and false of the container. They are marked alongside
63
+ * their source, so that "unreachable" keeps meaning "nothing can find this".
64
+ */
65
+ interface Reachability {
66
+ /** Normalised names of every part something can still get to, including `.rels` parts. */
67
+ readonly reachable: ReadonlySet<string>;
68
+ /**
69
+ * False when at least one relationship part could not be read.
70
+ *
71
+ * A caller that deletes on the strength of an incomplete walk is deleting on
72
+ * the strength of a parse error.
73
+ */
74
+ readonly complete: boolean;
75
+ /** The relationship parts that would not parse, and the root if it was unusable. */
76
+ readonly blockedBy: readonly string[];
77
+ }
78
+ /** Every part reachable by following relationships from the package root. */
79
+ declare function reachableParts(store: PartStore): Reachability;
80
+ /**
81
+ * Parts nothing in the package can get to.
82
+ *
83
+ * Relationship parts are excluded by construction rather than by filter - see
84
+ * the header. `[Content_Types].xml` never appears because it is not a part.
85
+ */
86
+ declare function orphanedParts(store: PartStore, walk: Reachability): string[];
87
+ //#endregion
88
+ //#region src/gc/collect.d.ts
89
+ /**
90
+ * Mark and sweep, for media.
91
+ *
92
+ * ## What it collects, and why so little
93
+ *
94
+ * The sweep set is `/ppt/media/` and nothing else. Not layouts, not masters,
95
+ * not themes, not `ppt/embeddings/*.bin`, whatever the relationship graph says
96
+ * about them. PowerPoint keeps slide layouts that no slide uses - that is how
97
+ * the layout picker has anything to offer - so a package where an unused layout
98
+ * has been tidied away is a package where "change layout" has quietly lost
99
+ * options the user had a moment ago. There is no upside to weigh against that:
100
+ * the layout part is a few kilobytes and the media is the megabytes.
101
+ *
102
+ * ## Where the walk is rooted is the same rule, made twice
103
+ *
104
+ * The obvious implementation asks "which images do the slides use?" and roots
105
+ * the walk at the slide list. It is wrong, and wrong in the way that only shows
106
+ * up weeks later: a layout no slide currently uses still has its background
107
+ * picture, and switching a slide onto that layout has to still work. Rooting at
108
+ * the package root instead gets this right with no special case - layouts hang
109
+ * off the master, the master off `presentation.xml` - so every image a layout
110
+ * or master references is live whether or not a slide has ever used it.
111
+ *
112
+ * ## It cannot break a relationship
113
+ *
114
+ * Worth stating as a property rather than a hope, because it is what makes this
115
+ * safe to run without supervision. A part is collected only when **no**
116
+ * relationship anywhere in the package resolves to it. So there is no edge left
117
+ * to dangle - and the case that kills naive implementations, two slides sharing
118
+ * one image with one of them deleted, cannot arise: the surviving slide's
119
+ * relationship still resolves and still marks the image live.
120
+ *
121
+ * The corollary is a division of labour. This collects nothing until something
122
+ * else has removed the relationship, and removing it is part of deleting a
123
+ * picture, which is a document operation and belongs to Phase 5's command
124
+ * layer rather than to the writer.
125
+ *
126
+ * ## By default it collects only what we orphaned
127
+ *
128
+ * The question `@pptx-studio/validate` asks about findings, asked about parts:
129
+ * not "is this unreferenced" but "did *we* leave it unreferenced". A deck can
130
+ * arrive with an orphaned image - decks that have been through several editors
131
+ * often do - and collecting it would mean that merely opening and saving a file
132
+ * changes it. That breaks the property the whole of Phase 1 exists to
133
+ * establish, and it breaks it in the direction where nobody notices for months.
134
+ *
135
+ * So the default policy differences the reachability of the package we are
136
+ * about to write against the package as it was opened, and collects only what
137
+ * moved from reachable to unreachable. With no baseline there is no "we", and
138
+ * it collects nothing.
139
+ *
140
+ * Measured, for scale: across the fifty-two committed decks there are 1474
141
+ * parts, 24 of them under `/ppt/media/`, and every part of every deck is
142
+ * reachable from the package root. On this corpus the two policies are the same
143
+ * policy. The difference is a promise about the decks we have not seen.
144
+ */
145
+ type CollectionPolicy =
146
+ /** Only parts this session left unreferenced. The default. */
147
+ 'orphaned-here' |
148
+ /** Every unreferenced part in the sweep set, however it got that way. */
149
+ 'every-orphan' |
150
+ /** Collect nothing. */
151
+ 'none';
152
+ /** `/ppt/media/...` - the default sweep set. */
153
+ declare function isMediaPart(partName: string): boolean;
154
+ interface CollectOptions {
155
+ /** The package about to be written. */
156
+ readonly store: PartStore;
157
+ /** The package as it was opened. Required by `orphaned-here`, ignored otherwise. */
158
+ readonly baseline?: PartStore | null;
159
+ /** Defaults to `orphaned-here`. */
160
+ readonly policy?: CollectionPolicy;
161
+ /**
162
+ * Which parts the sweep may touch at all. Defaults to `isMediaPart`.
163
+ *
164
+ * Sub-phase 8.7 will pass one of its own, to collect a `ppt/fonts/*.fntdata`
165
+ * whose typeface no run uses any more - PowerPoint requires every listed
166
+ * embedded font to be in use. Anything wider than that wants the header above
167
+ * read first.
168
+ */
169
+ readonly sweepable?: (partName: string) => boolean;
170
+ }
171
+ /** An orphan the sweep declined to collect, and why. */
172
+ interface KeptPart {
173
+ readonly part: string;
174
+ readonly why: string;
175
+ }
176
+ interface CollectionPlan {
177
+ /** Parts removed, in the order they were found. */
178
+ readonly collect: readonly string[];
179
+ /** Orphans left in place, each with a reason. Empty is the normal case. */
180
+ readonly kept: readonly KeptPart[];
181
+ /**
182
+ * False when the relationship graph could not be walked completely.
183
+ *
184
+ * A caller that deletes on the strength of an incomplete walk is deleting on
185
+ * the strength of a parse error.
186
+ */
187
+ readonly safe: boolean;
188
+ /** Relationship parts that would not parse. Empty when `safe`. */
189
+ readonly blockedBy: readonly string[];
190
+ }
191
+ /**
192
+ * Plan and apply in one pass.
193
+ *
194
+ * Not split into a pure plan and a separate apply, and the reason is the fixed
195
+ * point below: removing a part can orphan the parts *it* referenced, and the
196
+ * only way to observe that is to remove and look again. A pure planner would
197
+ * have to simulate the store to find the second round, which is a second
198
+ * implementation of the store.
199
+ *
200
+ * Never throws. A package too broken to reason about yields a plan that
201
+ * collects nothing and says why - refusing would make a deck with one
202
+ * unparseable `.rels` permanently unsaveable, and that `.rels` may well have
203
+ * arrived that way. `collectGarbage` is the variant that refuses, for callers
204
+ * who asked for a sweep and would misread silence as "nothing to do".
205
+ */
206
+ declare function planCollection(options: CollectOptions): CollectionPlan;
207
+ /**
208
+ * Collect, and refuse rather than guess.
209
+ *
210
+ * The entry point for a deliberate clean-up: a user asking for one, or a
211
+ * command that knows it has just orphaned something. It throws when the graph
212
+ * could not be walked completely, because a caller who asked for a sweep and
213
+ * got silence would reasonably conclude there had been nothing to sweep.
214
+ *
215
+ * `exportPackage` deliberately does not use this. An export that failed because
216
+ * some unrelated `.rels` will not parse would be refusing to save a file over a
217
+ * defect it did not cause and cannot fix.
218
+ */
219
+ declare function collectGarbage(options: CollectOptions): CollectionPlan;
220
+ //#endregion
221
+ //#region src/export/prepare.d.ts
222
+ /**
223
+ * Work that has to happen at save time, contributed by whoever owns it.
224
+ *
225
+ * Several later sub-phases need to touch the package on the way out, and each
226
+ * of them is somebody else's subject:
227
+ *
228
+ * - **8.7, embedded fonts.** Six artifacts that must all be present or
229
+ * PowerPoint reports a problem: the `fntdata` parts, the `fntdata` content-type
230
+ * `Default`, the font relationships on `presentation.xml`, the
231
+ * `p:embeddedFont` entries, their PANOSE and charset attributes, and
232
+ * `@embedTrueTypeFonts`. Plus a collection pass, because PowerPoint requires
233
+ * every listed typeface to actually be used.
234
+ * - **3.4, autofit.** A `txBody` whose text changed needs its computed
235
+ * `@fontScale` and `@lnSpcReduction` written back - and one whose text did
236
+ * not must be left exactly alone, or a view-only deck stops round-tripping.
237
+ * - **10.8, media.** The `p14:media` and `a:videoFile` relationships are a
238
+ * deliberate pair in the file and a duplicate on the way out.
239
+ *
240
+ * None of that is the writer's subject. Written inline it would become a list
241
+ * of special cases in `export.ts` that grows by one every phase, that the
242
+ * writer's own tests would have to know about, and that could not be tested
243
+ * without standing up a font stack or a text engine. As hooks, each lands in
244
+ * the package that owns it, ships with the sub-phase that needs it, and this
245
+ * package keeps knowing nothing about fonts.
246
+ *
247
+ * ## Where they run, and why there
248
+ *
249
+ * Before collection and before validation, both deliberately.
250
+ *
251
+ * Before collection, because a hook is exactly the thing that changes what is
252
+ * referenced - 8.7 both adds font parts and drops the ones nothing uses - and a
253
+ * sweep that ran first would be answering a question about the wrong package.
254
+ *
255
+ * Before validation, because a hook writes markup, and markup this session
256
+ * wrote is precisely what the twenty-nine rules exist to check. A hook that ran
257
+ * after validation would be the one part of an export nothing checked, which is
258
+ * the opposite of how it should be: it is newly synthesized markup, the
259
+ * riskiest kind there is.
260
+ */
261
+ interface PrepareContext {
262
+ /** The package being exported. Hooks mutate this. */
263
+ readonly store: PartStore;
264
+ /** The package as it was opened, when there is one. Read only; never mutate it. */
265
+ readonly baseline: PartStore | null;
266
+ /**
267
+ * Record something worth reporting.
268
+ *
269
+ * Surfaced on `ExportResult.prepared`. A hook that did nothing should say
270
+ * nothing - the common case for every hook on most exports is silence, and a
271
+ * note per hook per save would bury the one that mattered.
272
+ */
273
+ note(message: string): void;
274
+ }
275
+ interface PrepareHook {
276
+ /** Short and stable. It appears in `ExportResult` and in the error if this hook throws. */
277
+ readonly name: string;
278
+ run(context: PrepareContext): void;
279
+ }
280
+ interface PrepareRecord {
281
+ readonly hook: string;
282
+ readonly notes: readonly string[];
283
+ }
284
+ /**
285
+ * Run the hooks in the order given.
286
+ *
287
+ * The order is the caller's, not ours, and it is not inferred from anything.
288
+ * Some pairs genuinely depend on each other - autofit must settle before a font
289
+ * pass decides which typefaces are used - and a writer that sorted hooks by
290
+ * some notion of priority would be guessing at a dependency the caller knows
291
+ * for certain.
292
+ *
293
+ * A hook that throws stops the export. That is the point: the alternative is
294
+ * handing over a package with five of the six font artifacts in it, which
295
+ * PowerPoint reports as a problem with the content and no way to find out
296
+ * which.
297
+ */
298
+ declare function runPrepare(hooks: readonly PrepareHook[], store: PartStore, baseline: PartStore | null): PrepareRecord[];
299
+ //#endregion
300
+ //#region src/export/preserve.d.ts
301
+ /**
302
+ * The one assertion the writer makes about its own output.
303
+ *
304
+ * A part nobody edited must come out with the same bytes it went in with. That
305
+ * is the first architectural bet stated as a testable sentence, and it is worth
306
+ * a check of its own even though `V027` is nominally about the same thing,
307
+ * because the two are looking at different objects.
308
+ *
309
+ * `V027` asks the **store**: it compares what the store would hand you for a
310
+ * part against what the baseline store would hand you, and for an untouched
311
+ * part both answers come from the same archive. So it can catch a part that was
312
+ * replaced without being marked edited, and it cannot - even in principle -
313
+ * catch a writer that re-serialised a clean part on the way out, because by the
314
+ * time the bytes exist the rule has already run against a store that knows
315
+ * nothing about them.
316
+ *
317
+ * This check reads the archive we actually emitted. It compares the **stored**
318
+ * bytes, still compressed, against the stored bytes of the source archive:
319
+ * same DEFLATE stream, same CRC, same declared sizes. Comparing compressed
320
+ * bytes is both cheaper - nothing is inflated on either side - and stricter,
321
+ * because two different DEFLATE encodings of identical content would pass an
322
+ * inflate-and-compare and fail this. Stricter is what is wanted: any difference
323
+ * at all means something re-compressed, and re-compressing is the failure.
324
+ *
325
+ * It needs the bytes the package was opened from. Without them there is nothing
326
+ * to compare against and the check reports itself as not run, which is not the
327
+ * same as reporting that it passed.
328
+ */
329
+ interface PreservationCheck {
330
+ /** Entries compared and found identical. */
331
+ readonly checked: number;
332
+ /** Entries deliberately not compared, because this session wrote them. */
333
+ readonly rewritten: number;
334
+ /** Why the check did not run, or `null` when it did. */
335
+ readonly skipped: string | null;
336
+ }
337
+ /**
338
+ * Compare the emitted archive against the one it came from.
339
+ *
340
+ * Throws `ERR_PRESERVATION_BROKEN` on the first difference, naming the part.
341
+ * There is no "report and continue" mode: one part quietly re-serialised is the
342
+ * whole category of failure this package is built to make impossible, and every
343
+ * later one is likely the same cause.
344
+ */
345
+ declare function assertPreserved(output: Uint8Array, original: Uint8Array | undefined, rewritten: ReadonlySet<string>, zip?: ReadZipOptions): PreservationCheck;
346
+ //#endregion
347
+ //#region src/export/export.d.ts
348
+ /**
349
+ * Handing the package back.
350
+ *
351
+ * Four things happen, in an order that is load-bearing rather than tidy:
352
+ *
353
+ * 1. **Prepare hooks**, so that whatever later phases owe the file is in it.
354
+ * 2. **Collect**, because a hook is the thing most likely to have orphaned
355
+ * something.
356
+ * 3. **Write**, which streams every untouched part still compressed.
357
+ * 4. **Check**, twice: that nothing we did not edit changed, and that the
358
+ * twenty-nine rules pass. Only then do the bytes leave the function.
359
+ *
360
+ * The check comes after the write and not before it, which is the one ordering
361
+ * that might look backwards. It is the right way round because a check that
362
+ * runs first is checking an intention, and what a user opens is an archive. All
363
+ * three of `V003`, the duplicate half of `V002`, and the preservation check
364
+ * below are questions about the emitted container that no amount of inspecting
365
+ * the store can answer. Bytes are produced, then judged, then either returned
366
+ * or thrown away - never returned unjudged.
367
+ */
368
+ interface ExportOptions {
369
+ /** The package to write, with whatever edits have been made to it. */
370
+ readonly store: PartStore;
371
+ /**
372
+ * The package as it was opened.
373
+ *
374
+ * Optional, and the three things it costs to leave out are worth knowing
375
+ * before deciding to. Without it the three preservation rules are skipped
376
+ * rather than passed; the media sweep collects nothing, because there is no
377
+ * way to tell what this session orphaned; and `origin` is unavailable, so
378
+ * every fatal finding counts as one we introduced. `openPackage` produces a
379
+ * correct one in a line.
380
+ */
381
+ readonly baseline?: PartStore;
382
+ /** The archive the baseline was opened from. Needed by `V003` and the preservation check. */
383
+ readonly baselineBytes?: Uint8Array;
384
+ /** Run in the order given, before everything else. See `prepare.ts`. */
385
+ readonly prepare?: readonly PrepareHook[];
386
+ /** Media garbage collection. Defaults to `orphaned-here`. See `collect.ts`. */
387
+ readonly collect?: CollectionPolicy;
388
+ /** Which parts the sweep may touch. Defaults to `/ppt/media/`. */
389
+ readonly sweepable?: (partName: string) => boolean;
390
+ /**
391
+ * Set false to write without the firewall.
392
+ *
393
+ * There is no legitimate production use. It exists so that a test can build a
394
+ * deliberately broken package and look at it, and so that `cli bisect` in 1.5
395
+ * can emit the intermediate packages whose whole purpose is to be rejected.
396
+ */
397
+ readonly validate?: boolean;
398
+ /** A subset of rules to run. Defaults to all twenty-nine. */
399
+ readonly rules?: readonly RuleId[];
400
+ /** Set false to skip comparing the output against the source archive. */
401
+ readonly verifyPreservation?: boolean;
402
+ /** Passed through to `PartStore.write`. */
403
+ readonly normalizeEntryOrder?: boolean;
404
+ /** Passed through to `PartStore.write`. */
405
+ readonly deflateLevel?: number;
406
+ /** Passed to `readZip` wherever this package re-opens an archive. */
407
+ readonly zip?: ReadZipOptions;
408
+ }
409
+ interface ExportResult {
410
+ /** The package. Only ever returned when every check above passed. */
411
+ readonly bytes: Uint8Array;
412
+ /** `null` when validation was turned off. */
413
+ readonly report: Report | null;
414
+ /** Hooks that had something to say. Silent hooks are not listed. */
415
+ readonly prepared: readonly PrepareRecord[];
416
+ readonly collection: CollectionPlan;
417
+ /** Parts serialised afresh. Empty on a no-op export - that is the point. */
418
+ readonly rewritten: readonly string[];
419
+ /** Parts streamed out of the source archive without being decompressed. */
420
+ readonly streamed: number;
421
+ readonly preservation: PreservationCheck;
422
+ }
423
+ /**
424
+ * A package and the untouched twin every check compares it against.
425
+ *
426
+ * Two independent `PartStore.open` calls over the same bytes. Independent
427
+ * matters: the baseline has to keep answering questions about the file as it
428
+ * arrived no matter what happens to the other one, and sharing anything mutable
429
+ * between them would make it a record of the present rather than the past.
430
+ *
431
+ * Neither call inflates anything but `[Content_Types].xml`, so the second is
432
+ * two central-directory parses and a small XML read. They do carry separate
433
+ * inflation budgets, which means an adversarial archive gets two budgets rather
434
+ * than one; that is the correct trade at the size these budgets are set to, and
435
+ * it is written down here so it is a decision rather than an oversight.
436
+ */
437
+ interface OpenPackage {
438
+ readonly store: PartStore;
439
+ readonly baseline: PartStore;
440
+ readonly baselineBytes: Uint8Array;
441
+ }
442
+ /** Open a package for editing, with the baseline an export will need. */
443
+ declare function openPackage(bytes: Uint8Array, options?: ReadZipOptions): OpenPackage;
444
+ declare function exportPackage(options: ExportOptions): ExportResult;
445
+ //#endregion
446
+ //#region src/oracle/bisect.d.ts
447
+ /**
448
+ * Narrowing a package PowerPoint refuses down to the change that causes it.
449
+ *
450
+ * ## Why this is the debugger for this project
451
+ *
452
+ * PowerPoint emits no diagnostic log. A file it will not open produces one
453
+ * sentence - "PowerPoint found a problem with content in deck.pptx" - naming no
454
+ * part, no element and no reason, and a file it *repairs* produces a sentence
455
+ * that does not even say what was repaired. `@pptx-studio/validate` covers the
456
+ * twenty-nine failures we know how to describe; this covers the rest, which is
457
+ * the interesting ones. The plan's own caveat: passing the rules is necessary
458
+ * and not sufficient, because PowerPoint rejects some schema-legal markup for
459
+ * reasons nobody has written down.
460
+ *
461
+ * So the only way to find out what a refusal is about is to ask PowerPoint
462
+ * again with less of the change present, and keep asking. That is delta
463
+ * debugging: Zeller and Hildebrandt's `ddmin` over a set of changes
464
+ * (*Simplifying and Isolating Failure-Inducing Input*, IEEE TSE 2002), applied
465
+ * level by level down a tree, which is Misherghi and Su's HDD (*HDD:
466
+ * Hierarchical Delta Debugging*, ICSE 2006).
467
+ *
468
+ * ## This is the isolation problem, not the simplification one
469
+ *
470
+ * The distinction is Zeller's and it decides the whole design. Simplification
471
+ * starts from one failing input and cuts it down; it has to guess what a
472
+ * smaller input looks like, and most of its guesses are not even well formed.
473
+ * Isolation starts from a **passing** configuration and a **failing** one and
474
+ * reduces the *difference* between them.
475
+ *
476
+ * We are always in the second case. A deck that PowerPoint repairs came from a
477
+ * deck that it opened, and we have both. So the atoms here are not elements of
478
+ * the document: they are the **changes** between the two packages, and a
479
+ * configuration is a subset of them applied to the original. That buys three
480
+ * things a simplifier cannot have:
481
+ *
482
+ * - Every configuration is well formed by construction. The splice unit is a
483
+ * whole node replaced by the whole node the other package has in that
484
+ * position, so no configuration can invent unbalanced markup.
485
+ * - The empty configuration is known to pass and the full one to fail, which
486
+ * is exactly `ddmin`'s precondition - and both are *checked* here rather
487
+ * than assumed, because when they do not hold the reason is the most useful
488
+ * thing this command can say.
489
+ * - The answer is "these two changes, out of ninety-one", which is a sentence
490
+ * about your edit rather than about PowerPoint's file format.
491
+ *
492
+ * ## Entries, not parts
493
+ *
494
+ * The delta is taken over **ZIP entries**, not over `PartStore` parts, and the
495
+ * difference is not academic: `[Content_Types].xml` is not a part. It is a
496
+ * package-level stream, `PartStore.partNames` deliberately excludes it, and a
497
+ * missing `<Default Extension="fntdata"/>` in it is the canonical cause of
498
+ * "PowerPoint found a problem with content" - the one this project's own plan
499
+ * cites. A bisector that could only vary parts would be blind to the single
500
+ * best-documented repair prompt there is.
501
+ *
502
+ * Working at the entry level also means nothing has to know what an entry
503
+ * holds. Anything that parses as XML is descended into - parts, `.rels`,
504
+ * `docProps` and the content-type stream alike - and anything that does not is
505
+ * one atom, whether it is a PNG, an OLE2 compound file, or markup too damaged
506
+ * to read. There is no content-type table to keep in step.
507
+ */
508
+ interface Span {
509
+ readonly start: number;
510
+ readonly end: number;
511
+ }
512
+ type ChangeKind =
513
+ /** The entry exists only in the broken package. */
514
+ 'entry-added' |
515
+ /** The entry exists only in the original. */
516
+ 'entry-removed' |
517
+ /** A whole entry. Binary, or markup whose two sides do not line up. */
518
+ 'entry' |
519
+ /** One element and everything under it. */
520
+ 'element' |
521
+ /** One text, CDATA, comment, PI or declaration node. */
522
+ 'node' |
523
+ /**
524
+ * A run of children replaced by another run of a different length.
525
+ *
526
+ * What an insertion or a deletion looks like. `was` empty means children were
527
+ * added; `text` empty means they were taken away.
528
+ */
529
+ 'children' |
530
+ /** One element's start tag: its name, its attributes and their spacing. */
531
+ 'tag' |
532
+ /** One attribute, including the whitespace in front of it. */
533
+ 'attribute';
534
+ interface Change {
535
+ readonly id: number;
536
+ /** ZIP entry name, without a leading slash: `ppt/slides/slide1.xml`. */
537
+ readonly entry: string;
538
+ readonly kind: ChangeKind;
539
+ /**
540
+ * Where in the entry, for a person to read.
541
+ *
542
+ * An XPath-ish path for markup - `/p:sld/p:cSld/p:spTree/p:sp[3]/@name` -
543
+ * built from qualified names exactly as the file writes them, because a path
544
+ * that renamed prefixes could not be pasted back into a search.
545
+ */
546
+ readonly where: string;
547
+ /** Distance from the entry, so a report can indent. */
548
+ readonly depth: number;
549
+ /** Where this sits in the **original** entry's text. Null for a whole entry. */
550
+ readonly span: Span | null;
551
+ /** What the broken package has there. Null when the entry is only original. */
552
+ readonly text: string | null;
553
+ /** What the original has there. Null when the entry is only in the broken one. */
554
+ readonly was: string | null;
555
+ /**
556
+ * Finer changes that together reproduce this one exactly.
557
+ *
558
+ * The exactness is what makes descending free: the regions between the
559
+ * children are the regions that compared equal, so applying every child is
560
+ * the same package as applying the parent. HDD relies on it to move down a
561
+ * level without spending a run to re-establish that the configuration fails.
562
+ */
563
+ readonly children: readonly Change[];
564
+ }
565
+ /** Every change in the forest, parents before children. */
566
+ declare function flattenChanges(changes: readonly Change[]): Change[];
567
+ /** The leaves: the finest changes this delta can express. */
568
+ declare function leafChanges(changes: readonly Change[]): Change[];
569
+ /**
570
+ * What the oracle says about one candidate package.
571
+ *
572
+ * Three values rather than two, and the third is Zeller's. `unresolved` is a
573
+ * run that answered neither question - PowerPoint timed out, the harness would
574
+ * not start, a subprocess was killed. Treating it as "passes" would let the
575
+ * reducer discard the change that matters and then blame an innocent one;
576
+ * treating it as "fails" would let it keep everything. It is counted and
577
+ * reported instead, and never reduced towards.
578
+ */
579
+ type Verdict = 'fails' | 'passes' | 'unresolved';
580
+ /**
581
+ * Does this package still show the problem?
582
+ *
583
+ * Synchronous on purpose. Every oracle we have is either pure computation - the
584
+ * twenty-nine rules, the round-trip comparison - or a subprocess the caller
585
+ * blocks on anyway, and `spawnSync` blocks perfectly well. Making the reducer
586
+ * asynchronous to serve a caller that does not exist would put a Promise in the
587
+ * middle of a browser package for nothing.
588
+ */
589
+ type Oracle = (bytes: Uint8Array, label: string) => Verdict;
590
+ interface RunEvent {
591
+ readonly run: number;
592
+ readonly applied: number;
593
+ readonly total: number;
594
+ readonly label: string;
595
+ }
596
+ interface BisectOptions {
597
+ readonly oracle: Oracle;
598
+ /**
599
+ * Ceiling on oracle runs.
600
+ *
601
+ * `ddmin` is quadratic in the worst case and the interesting oracle takes
602
+ * about a second and a half, so a run against a large delta can take a long
603
+ * time. When the ceiling is hit the result carries `exhausted: true` and the
604
+ * smallest failing configuration found so far - never a truncated answer
605
+ * presented as a minimal one.
606
+ */
607
+ readonly maxRuns?: number;
608
+ /** Called before each oracle run, for a progress line. */
609
+ readonly onRun?: (run: RunEvent) => void;
610
+ readonly zip?: ReadZipOptions;
611
+ /**
612
+ * Ceiling on the size of the delta tree.
613
+ *
614
+ * Two packages that share nothing produce a tree the size of the document.
615
+ * Past this, subtrees stop being decomposed and stay atomic, which costs
616
+ * precision and not correctness; `truncated` says when it happened.
617
+ */
618
+ readonly maxChanges?: number;
619
+ }
620
+ type BisectOutcome =
621
+ /** A minimal failing set was found. */
622
+ 'localized' |
623
+ /** The two packages hold the same entries, byte for byte. */
624
+ 'identical' |
625
+ /** The broken package passes the oracle. There is nothing to look for. */
626
+ 'broken-passes' |
627
+ /** The original fails too, so the cause is not in the difference. */
628
+ 'original-fails';
629
+ interface BisectResult {
630
+ readonly outcome: BisectOutcome;
631
+ /** The whole delta, as a forest. One root per differing entry. */
632
+ readonly changes: readonly Change[];
633
+ /** The 1-minimal failing subset: drop any one of these and it passes. */
634
+ readonly minimal: readonly Change[];
635
+ /** The smallest package still showing the problem. Ready to open, or to keep. */
636
+ readonly bytes: Uint8Array;
637
+ /** Oracle runs actually performed. */
638
+ readonly runs: number;
639
+ /** Configurations answered from the cache instead of the oracle. */
640
+ readonly cached: number;
641
+ /** Runs that answered neither question. */
642
+ readonly unresolved: number;
643
+ /** True when `maxRuns` stopped the reduction early. */
644
+ readonly exhausted: boolean;
645
+ /** True when `maxChanges` stopped the delta being decomposed further. */
646
+ readonly truncated: boolean;
647
+ }
648
+ /** The forest of changes between two packages. One root per differing entry. */
649
+ declare function collectDelta(original: ZipArchive, broken: ZipArchive, maxChanges?: number): {
650
+ changes: Change[];
651
+ text: Map<string, string>;
652
+ truncated: boolean;
653
+ };
654
+ declare function bisectPackages(originalBytes: Uint8Array, brokenBytes: Uint8Array, options: BisectOptions): BisectResult;
655
+ /** One line naming a change, without its text. */
656
+ declare function describeChange(change: Change): string;
657
+ /** `2 change(s) in 1 entry(s), from a delta of 91, in 37 oracle run(s)`. */
658
+ declare function summarizeBisect(result: BisectResult): string;
659
+ //#endregion
660
+ //#region src/oracle/roundtrip.d.ts
661
+ /**
662
+ * Is the deck we wrote the deck we read?
663
+ *
664
+ * ## Why this is not a byte comparison
665
+ *
666
+ * The plan is explicit that raw ZIP byte equality is the wrong test and says
667
+ * why in one line: entry order, deflate level, DOS timestamps and attribute
668
+ * order all legitimately differ, so a byte-equality round-trip test is red on
669
+ * day one and disabled on day two. A test nobody trusts is worse than no test,
670
+ * because it also stops anyone writing the one that would have worked.
671
+ *
672
+ * `assertPreserved` in this same package *does* compare bytes, and the two are
673
+ * not in competition. It asks a narrow question - did the entries we said we
674
+ * would not touch come out with the identical stored bytes - and it is the
675
+ * right question for a no-op export. This module asks the wide one: for a
676
+ * package we edited, or one a different producer wrote, do the two archives
677
+ * hold the same document? That question has to survive an entry moving, a rId
678
+ * being renumbered, and a `Default` becoming an `Override`, none of which
679
+ * changes anything a reader can observe.
680
+ *
681
+ * ## Three comparisons, because a package has three kinds of content
682
+ *
683
+ * **XML parts** compare by canonical form - see `canonicalXml` in
684
+ * `@pptx-studio/xml` for what it normalises and, more importantly, what it
685
+ * refuses to. Whitespace and prefixes are meaning, not formatting.
686
+ *
687
+ * **Relationship parts** compare as a graph with the ids treated as opaque
688
+ * labels. Two `.rels` that disagree only about whether the theme is `rId1` or
689
+ * `rId7` describe the same package, and PowerPoint renumbers on every save, so
690
+ * a comparator that could not see past that would be useless the first time it
691
+ * was pointed at a file PowerPoint had written. Ids are matched by what they
692
+ * point at, and the resulting mapping is then applied to the referring markup:
693
+ * an `r:embed="rId3"` on one side and `r:embed="rId7"` on the other are the
694
+ * same reference exactly when rId3 and rId7 resolve to the same part.
695
+ *
696
+ * That relabelling is keyed on the **namespace** of the attribute rather than
697
+ * on a list of names. Every attribute in the relationships namespace is a
698
+ * relationship reference - `r:id`, `r:embed`, `r:link`, `r:pict`, `r:dm`,
699
+ * `r:lo`, `r:qs`, `r:cs` - and a list would be a list that is one entry short
700
+ * the first time we meet a part type nobody thought about.
701
+ *
702
+ * **Everything else** compares by SHA-256. The bytes are in hand on both sides
703
+ * so the digest is not needed to decide equality; it is needed to *report*, and
704
+ * to give `cli bisect` in 1.5 something to name a part by. CRC-32, which this
705
+ * package already has for the archive, is a 32-bit error-detecting code and not
706
+ * evidence that two files are the same.
707
+ *
708
+ * ## What it does not compare, and why that is not a hole
709
+ *
710
+ * `[Content_Types].xml` is never compared as a document. It is a map, and the
711
+ * same map has many spellings: a `Default` for an extension and an `Override`
712
+ * on every part with that extension mean the identical thing, and PowerPoint
713
+ * chooses between them differently from us. What is compared instead is the
714
+ * resolved content type of every part, which is the only thing the map is for.
715
+ */
716
+ type DifferenceKind =
717
+ /** A part is in the original and not in what was written. */
718
+ 'part-removed' |
719
+ /** A part is in what was written and not in the original. */
720
+ 'part-added' |
721
+ /** Both have the part; the content types disagree. */
722
+ 'content-type' |
723
+ /** Both have the part; the canonical XML disagrees. */
724
+ 'xml' |
725
+ /** Both have the part; the bytes disagree. */
726
+ 'binary' |
727
+ /** The relationship graphs of one source part disagree. */
728
+ 'relationship' |
729
+ /** A part could not be read on one side or the other, so nothing can be said. */
730
+ 'unreadable';
731
+ interface Difference {
732
+ readonly kind: DifferenceKind;
733
+ /** The part it is about. For a relationship difference, the *source* part. */
734
+ readonly part: string;
735
+ readonly detail: string;
736
+ }
737
+ type CompareHow = 'xml' | 'binary' | 'relationships';
738
+ interface PartComparison {
739
+ readonly part: string;
740
+ readonly how: CompareHow;
741
+ readonly same: boolean;
742
+ /**
743
+ * The digest of the compared form on the original side: the canonical XML for
744
+ * an XML part, the bytes for anything else, and the relationship graph with
745
+ * the ids left out for a `.rels`.
746
+ *
747
+ * Two parts with the same digest are the same part in the sense this module
748
+ * means. That is the value worth writing into a report.
749
+ */
750
+ readonly digest: string;
751
+ /** The same digest on the written side. Equal to `digest` whenever `same`. */
752
+ readonly writtenDigest: string;
753
+ }
754
+ /** One relationship id that means the same thing under a different name. */
755
+ interface Relabelling {
756
+ /** The part whose `.rels` this is - `/` for the package root. */
757
+ readonly source: string;
758
+ readonly from: string;
759
+ readonly to: string;
760
+ /** What both ids point at, which is why they are the same label. */
761
+ readonly target: string;
762
+ }
763
+ interface RoundTripReport {
764
+ readonly ok: boolean;
765
+ readonly parts: readonly PartComparison[];
766
+ readonly differences: readonly Difference[];
767
+ /**
768
+ * Ids that moved. Empty for anything this package wrote, and the interesting
769
+ * field when the comparison is against a file PowerPoint saved.
770
+ */
771
+ readonly relabelled: readonly Relabelling[];
772
+ readonly counts: {
773
+ readonly xml: number;
774
+ readonly binary: number;
775
+ readonly relationships: number;
776
+ readonly same: number;
777
+ };
778
+ }
779
+ interface CompareOptions {
780
+ /** Passed to `parseXml` for both sides. */
781
+ readonly limits?: XmlParseLimits;
782
+ }
783
+ /**
784
+ * Compare two packages.
785
+ *
786
+ * Never throws. Every failure - a part that will not parse, a `.rels` that will
787
+ * not read - is a difference with a reason, because the caller of a comparison
788
+ * wants the whole list and not the first item on it.
789
+ */
790
+ declare function comparePackages(original: PartStore, written: PartStore, options?: CompareOptions): RoundTripReport;
791
+ /** A one-line summary, for a CLI and for a test failure message. */
792
+ declare function summarizeRoundTrip(report: RoundTripReport): string;
793
+ /**
794
+ * Read a package, write it back, and compare the two.
795
+ *
796
+ * The whole of sub-phase 1.4 in one call, and the shape `cli roundtrip` and the
797
+ * corpus gate both want. Every option `exportPackage` takes is accepted, so a
798
+ * caller can round-trip through a different deflate level or a normalised entry
799
+ * order - which is the point of a comparison that is not byte equality, and
800
+ * worth having a test do deliberately.
801
+ *
802
+ * Throws whatever `exportPackage` throws. A package the firewall refuses is not
803
+ * a package with a round-trip difference; it is one that never got written, and
804
+ * flattening the two into one verdict would lose the distinction exactly where
805
+ * it matters.
806
+ */
807
+ declare function roundTripPackage(bytes: Uint8Array, options?: RoundTripOptions): RoundTripResult;
808
+ interface RoundTripOptions extends Omit<ExportOptions, 'store' | 'baseline' | 'baselineBytes'>, CompareOptions {}
809
+ interface RoundTripResult {
810
+ readonly exported: ExportResult;
811
+ /** The package as re-opened from the bytes that were written. */
812
+ readonly written: PartStore;
813
+ readonly comparison: RoundTripReport;
814
+ readonly ok: boolean;
815
+ }
816
+ //#endregion
817
+ export { type BisectOptions, type BisectOutcome, type BisectResult, type Change, type ChangeKind, type CollectOptions, type CollectionPlan, type CollectionPolicy, type CompareHow, type CompareOptions, type Difference, type DifferenceKind, type ExportOptions, type ExportResult, type KeptPart, type OpenPackage, type Oracle, type PartComparison, type PrepareContext, type PrepareHook, type PrepareRecord, type PreservationCheck, type Reachability, type Relabelling, type RoundTripOptions, type RoundTripReport, type RoundTripResult, type RunEvent, type Span, type Verdict, WRITER_ERROR_CODES, WriterError, type WriterErrorCode, type WriterErrorDetail, assertPreserved, bisectPackages, collectDelta, collectGarbage, comparePackages, describeChange, exportPackage, flattenChanges, isMediaPart, isWriterError, leafChanges, openPackage, orphanedParts, planCollection, reachableParts, roundTripPackage, runPrepare, summarizeBisect, summarizeRoundTrip };
818
+ //# sourceMappingURL=index.d.ts.map