slidemind 0.3.20 → 0.4.1

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 (187) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/.codex-plugin/plugin.json +1 -1
  3. package/AGENTS.md +592 -16
  4. package/LICENSE +4 -0
  5. package/README.md +203 -8
  6. package/THIRD-PARTY-NOTICES.md +201 -0
  7. package/bin/slidemind.mjs +50 -2
  8. package/package.json +5 -3
  9. package/skills/create-slides/SKILL.md +484 -886
  10. package/skills/search/SKILL.md +46 -23
  11. package/src/bridge/build-and-push.mjs +548 -73
  12. package/src/bridge/bundle.mjs +100 -31
  13. package/src/bridge/capture.mjs +14 -17
  14. package/src/bridge/client/brands.mjs +157 -18
  15. package/src/bridge/client/engine-routing.mjs +48 -13
  16. package/src/bridge/client/engine-search.mjs +24 -1
  17. package/src/bridge/client/render.mjs +27 -1
  18. package/src/bridge/client/transport.mjs +499 -23
  19. package/src/bridge/client.mjs +10 -0
  20. package/src/bridge/config.mjs +67 -6
  21. package/src/bridge/engine-download.mjs +0 -10
  22. package/src/bridge/graph-auth.mjs +55 -3
  23. package/src/bridge/image-intent-envelope.mjs +239 -0
  24. package/src/bridge/package-preview.mjs +67 -0
  25. package/src/bridge/sharepoint.mjs +914 -0
  26. package/src/builders/asset-fetch.mjs +203 -0
  27. package/src/builders/block-dividers.mjs +83 -0
  28. package/src/builders/build.mjs +171 -87
  29. package/src/builders/card-height.mjs +294 -0
  30. package/src/builders/chart-hole.mjs +183 -0
  31. package/src/builders/chart-jsx.mjs +410 -0
  32. package/src/builders/chart-labels.mjs +326 -0
  33. package/src/builders/concept-geometry.mjs +370 -0
  34. package/src/builders/concept-intent.mjs +200 -0
  35. package/src/builders/concept-layout.mjs +79 -0
  36. package/src/builders/concept-lines.mjs +509 -0
  37. package/src/builders/concept-lists.mjs +270 -0
  38. package/src/builders/concept-marks.mjs +84 -0
  39. package/src/builders/concept-relationships.mjs +12 -0
  40. package/src/builders/concept-spec.mjs +645 -0
  41. package/src/builders/concept-text.mjs +140 -0
  42. package/src/builders/concept-waves.mjs +57 -0
  43. package/src/builders/copy-fit.mjs +139 -0
  44. package/src/builders/design-tokens.mjs +347 -0
  45. package/src/builders/draft-check.mjs +552 -340
  46. package/src/builders/draft-lines.mjs +369 -0
  47. package/src/builders/edit-route.mjs +29 -0
  48. package/src/builders/figure-sketch.mjs +375 -0
  49. package/src/builders/fold-sets.mjs +639 -0
  50. package/src/builders/gate-server.mjs +9 -4
  51. package/src/builders/gate-session.js +273 -12
  52. package/src/builders/gate-shell.js +1494 -0
  53. package/src/builders/image-intent-check.mjs +308 -0
  54. package/src/builders/image-use-reconcile.mjs +107 -0
  55. package/src/builders/kit-helpers.mjs +121 -0
  56. package/src/builders/layout-names.mjs +32 -0
  57. package/src/builders/legacy-reference-uses.mjs +179 -0
  58. package/src/builders/line-fit.mjs +224 -0
  59. package/src/builders/line-native.mjs +895 -0
  60. package/src/builders/line-relations.mjs +230 -0
  61. package/src/builders/lint-fix.mjs +117 -9
  62. package/src/builders/lint.mjs +25 -20
  63. package/src/builders/list-jsx.mjs +88 -0
  64. package/src/builders/placement.mjs +133 -0
  65. package/src/builders/plan-checks.mjs +135 -0
  66. package/src/builders/plan-comments.mjs +19 -0
  67. package/src/builders/plan-handlers.mjs +95 -0
  68. package/src/builders/plan-markdown-edit.mjs +3 -3
  69. package/src/builders/plan-overlay.js +1530 -1608
  70. package/src/builders/plan.mjs +689 -186
  71. package/src/builders/preset-definitions.json +1 -0
  72. package/src/builders/preset-eval.mjs +244 -0
  73. package/src/builders/preview.html +51 -120
  74. package/src/builders/red-thread-backbone.mjs +135 -36
  75. package/src/builders/repair.mjs +205 -0
  76. package/src/builders/repeat-groups.mjs +540 -0
  77. package/src/builders/resolve-skill.mjs +5 -4
  78. package/src/builders/review-overlay.js +296 -293
  79. package/src/builders/review.mjs +305 -84
  80. package/src/builders/rule-shift.mjs +96 -0
  81. package/src/builders/seat.mjs +354 -0
  82. package/src/builders/settle-driver.mjs +236 -0
  83. package/src/builders/settle.mjs +1448 -0
  84. package/src/builders/slide-prep.mjs +67 -0
  85. package/src/builders/spec-assets.mjs +236 -0
  86. package/src/builders/spec-match.mjs +56 -0
  87. package/src/builders/stack.mjs +413 -0
  88. package/src/builders/storyboard-markdown.mjs +70 -27
  89. package/src/builders/storyboard-schema.mjs +54 -114
  90. package/src/builders/text-blocks.mjs +212 -0
  91. package/src/builders/text-card-fit.mjs +552 -0
  92. package/src/builders/text-stack.mjs +276 -0
  93. package/src/builders/transcribe.mjs +1069 -0
  94. package/src/cli/assets.mjs +164 -0
  95. package/src/cli/common.mjs +173 -12
  96. package/src/cli/concept-spec-support.mjs +76 -0
  97. package/src/cli/concepts.mjs +1002 -103
  98. package/src/cli/deck.mjs +401 -65
  99. package/src/cli/diagnose.mjs +4 -2
  100. package/src/cli/engine-helpers.mjs +17 -10
  101. package/src/cli/engine-results.mjs +76 -3
  102. package/src/cli/help.mjs +232 -28
  103. package/src/cli/install.mjs +9 -0
  104. package/src/cli/maintenance.mjs +104 -0
  105. package/src/cli/read.mjs +2 -1
  106. package/src/cli/ready.mjs +6 -3
  107. package/src/cli/record.mjs +522 -0
  108. package/src/cli/remote.mjs +818 -0
  109. package/src/cli/revision.mjs +686 -0
  110. package/src/cli/search.mjs +30 -11
  111. package/src/cli/session.mjs +56 -13
  112. package/src/cli/setup.mjs +46 -2
  113. package/src/cli/status.mjs +142 -33
  114. package/src/cli/tasks.mjs +246 -0
  115. package/src/cli/transcribe.mjs +854 -0
  116. package/src/cli/workspace-ui.mjs +260 -0
  117. package/src/cli/workspace.mjs +212 -34
  118. package/src/install-channel.mjs +64 -7
  119. package/src/lib/adopt.mjs +346 -25
  120. package/src/lib/concept-spec-binding.mjs +96 -0
  121. package/src/lib/conversions.mjs +70 -6
  122. package/src/lib/convert-folded.mjs +107 -0
  123. package/src/lib/deck-paths.mjs +418 -0
  124. package/src/lib/durable-ids.mjs +47 -0
  125. package/src/lib/edit-operations.mjs +316 -0
  126. package/src/lib/edit-state.mjs +1244 -0
  127. package/src/lib/edit-transitions.mjs +120 -0
  128. package/src/lib/extensions.mjs +501 -0
  129. package/src/lib/image-assets.mjs +465 -0
  130. package/src/lib/image-derivatives.mjs +234 -0
  131. package/src/lib/image-intent-fingerprint.mjs +70 -0
  132. package/src/lib/image-intent-tags.mjs +176 -0
  133. package/src/lib/image-intent.mjs +802 -0
  134. package/src/lib/image-use-overrides.mjs +279 -0
  135. package/src/lib/inline-components.mjs +613 -0
  136. package/src/lib/lint-converted.mjs +68 -4
  137. package/src/lib/merge-json.mjs +163 -0
  138. package/src/lib/overwrite-guard.mjs +3 -3
  139. package/src/lib/presentation-context.mjs +531 -0
  140. package/src/lib/preservation-tags.mjs +157 -0
  141. package/src/lib/preservation-verdict.mjs +1342 -0
  142. package/src/lib/publish.mjs +1557 -0
  143. package/src/lib/rebase.mjs +945 -0
  144. package/src/lib/reconcile-ack.mjs +4 -6
  145. package/src/lib/reconcile.mjs +62 -24
  146. package/src/lib/record-client.mjs +175 -0
  147. package/src/lib/record-state.mjs +151 -0
  148. package/src/lib/remote.mjs +661 -0
  149. package/src/lib/revision.mjs +1210 -0
  150. package/src/lib/slide-guids.mjs +4 -6
  151. package/src/lib/slide-preservation.mjs +423 -0
  152. package/src/lib/slide-selection.mjs +9 -0
  153. package/src/lib/workspace-transaction.mjs +117 -0
  154. package/src/pptx/content-digest.mjs +334 -0
  155. package/src/pptx/deck-assemble.mjs +1278 -0
  156. package/src/pptx/fill/engine.mjs +2 -3
  157. package/src/pptx/fill/freeform.mjs +6 -3
  158. package/src/pptx/ooxml.mjs +20 -0
  159. package/src/pptx/package-evidence.mjs +383 -0
  160. package/src/pptx/tags.mjs +163 -11
  161. package/src/update-check.mjs +22 -4
  162. package/src/workspace/adapters.mjs +222 -0
  163. package/src/workspace/auth.mjs +256 -0
  164. package/src/workspace/autostart.mjs +204 -0
  165. package/src/workspace/daemon.mjs +360 -0
  166. package/src/workspace/discovery.mjs +180 -0
  167. package/src/workspace/edit.mjs +167 -0
  168. package/src/workspace/events.mjs +52 -0
  169. package/src/workspace/gate-client.mjs +290 -0
  170. package/src/workspace/handback.mjs +73 -0
  171. package/src/workspace/lifecycle.mjs +635 -0
  172. package/src/workspace/operations.mjs +200 -0
  173. package/src/workspace/pick-folder.mjs +152 -0
  174. package/src/workspace/registry.mjs +359 -0
  175. package/src/workspace/remote-links.mjs +397 -0
  176. package/src/workspace/remote-status.mjs +359 -0
  177. package/src/workspace/routes.mjs +1331 -0
  178. package/src/workspace/server.mjs +159 -0
  179. package/src/workspace/sync.mjs +1119 -0
  180. package/src/workspace/task-actions.mjs +215 -0
  181. package/src/workspace/tasks.mjs +507 -0
  182. package/src/workspace/ui/app.js +1886 -0
  183. package/src/workspace/ui/index.html +141 -0
  184. package/src/workspace/ui/plan-bridge.js +120 -0
  185. package/src/workspace/ui/sharepoint.svg +59 -0
  186. package/src/workspace/ui/styles.css +423 -0
  187. package/src/builders/visual-families.mjs +0 -46
@@ -36,6 +36,7 @@ import {
36
36
  insertPictureAtGeom, findShape, esc, imageSize, cellText, blocks,
37
37
  shapeFillXml, shapeRunRPr, setShapeFillXml,
38
38
  } from "./xml.mjs";
39
+ import { deckPptxPath } from "../../lib/deck-paths.mjs";
39
40
 
40
41
  const enc = (s) => new TextEncoder().encode(s);
41
42
  const RESERVED_PROPS = new Set(["repeat", "tables", "patches", "highlight"]);
@@ -128,9 +129,7 @@ export async function buildFillTemplateDeck({ deckDir, skill, outPath, onLog })
128
129
  throw new Error(`integrity check failed:\n - ${integrity.join("\n - ")}`);
129
130
  }
130
131
 
131
- const finalOut = outPath
132
- ? path.resolve(outPath)
133
- : path.join(deckDir, `${path.basename(deckDir)}.pptx`);
132
+ const finalOut = outPath ? path.resolve(outPath) : deckPptxPath(deckDir);
134
133
  fs.writeFileSync(finalOut, zip(files));
135
134
  return { outPath: finalOut, slideCount: output.length, warnings };
136
135
  }
@@ -19,6 +19,7 @@ import { unzip, readText } from "../zip.mjs";
19
19
  import { slideOrderPaths } from "../ooxml.mjs";
20
20
  import { foldPickedSlides } from "../slide-swap.mjs";
21
21
  import { layoutNumber } from "./parse-slides.mjs";
22
+ import { deckBundleRefPath } from "../../bridge/bundle.mjs";
22
23
 
23
24
  const SIDE_CAR = ".freeform-renders.json";
24
25
 
@@ -107,7 +108,7 @@ export async function ensureFreeformRenders({
107
108
  // of the freshness key — a CD update re-renders the freeform slides.
108
109
  let etag = "";
109
110
  try {
110
- const ref = JSON.parse(fs.readFileSync(path.join(deckDir, ".bundle-ref.json"), "utf8"));
111
+ const ref = JSON.parse(fs.readFileSync(deckBundleRefPath(deckDir), "utf8"));
111
112
  etag = ref?.etag ?? "";
112
113
  } catch {
113
114
  etag = "";
@@ -133,9 +134,11 @@ export async function ensureFreeformRenders({
133
134
  if (fs.existsSync(assetsDir)) {
134
135
  fs.cpSync(assetsDir, path.join(tmp, "assets"), { recursive: true });
135
136
  }
136
- const refPath = path.join(deckDir, ".bundle-ref.json");
137
+ const refPath = deckBundleRefPath(deckDir);
137
138
  if (fs.existsSync(refPath)) {
138
- fs.copyFileSync(refPath, path.join(tmp, ".bundle-ref.json"));
139
+ const tmpRef = deckBundleRefPath(tmp);
140
+ fs.mkdirSync(path.dirname(tmpRef), { recursive: true });
141
+ fs.copyFileSync(refPath, tmpRef);
139
142
  }
140
143
  const outPath = path.join(tmp, "render.pptx");
141
144
  try {
@@ -145,6 +145,26 @@ function normalizeSegments(parts) {
145
145
  return out.join("/");
146
146
  }
147
147
 
148
+ // One `<p:tag .../>` element: the whole element, its attributes in group 1.
149
+ // Quote-aware on purpose: PowerPoint writes `>` UNESCAPED inside an attribute
150
+ // value (valid XML), and a `[^>]*` match stops there — the value is cut off
151
+ // and the tag silently lost. Observed on a Mac PowerPoint save (2026-10-09):
152
+ // every SLIDEMIND_PROV whose selector read `svg > <line>` came back unreadable.
153
+ // Both quote styles XML allows, and both spellings of an empty element
154
+ // (`<p:tag …/>`, `<p:tag …></p:tag>`), are read.
155
+ export const TAG_ELEMENT = String.raw`<p:tag\b((?:[^>"']|"[^"]*"|'[^']*')*?)(?:\/>|><\/p:tag>)`;
156
+
157
+ /** One attribute of a `<p:tag>`'s attribute string, in either quote style — undecoded. */
158
+ export function tagAttr(attrs, name) {
159
+ const m = new RegExp(`\\b${name}=(?:"([^"]*)"|'([^']*)')`).exec(String(attrs ?? ""));
160
+ return m ? (m[1] ?? m[2]) : undefined;
161
+ }
162
+
163
+ /** Every `<p:tag/>` element of a tag list, attributes in group 1. */
164
+ export function tagElements(tagXml) {
165
+ return String(tagXml ?? "").matchAll(new RegExp(TAG_ELEMENT, "g"));
166
+ }
167
+
148
168
  // Decode the five predefined XML entities plus numeric character references
149
169
  // (`&#233;`, `&#xE9;`). Ampersand is resolved LAST so a decoded `&` from a
150
170
  // numeric ref can't be re-read as the head of another entity.
@@ -0,0 +1,383 @@
1
+ // What a delivered package proves about one slide's objects.
2
+ //
3
+ // `src/lib/preservation-verdict.mjs` compares NORMALIZED projections keyed by
4
+ // element identity — deliberately, so that one pure module can serve both
5
+ // clients without importing either one's package machinery. It does not read
6
+ // packages. Nothing did, which is why the final delivery gate could never reach
7
+ // `passed`: a connector's endpoints and a native object's dependency closure
8
+ // only resolve against the delivered bytes, and no one produced that evidence.
9
+ //
10
+ // This module is that producer, on the CLI side. It takes an unzipped package
11
+ // and returns exactly what `comparePackageEvidence` and `preservationVerdict`
12
+ // consume:
13
+ //
14
+ // shapes { [elementId]: projection shape (+ native / chart) }
15
+ // resolvedTargets what this PACKAGE resolves — read the warning below
16
+ // tables per-element grid evidence (see the note below)
17
+ // connectors per-element { start, end } as each connector claims them
18
+ // natives per-element { kind, progId, parts, digests }
19
+ //
20
+ // ---------------------------------------------------------------------------
21
+ // WARNING — `resolvedTargets` speaks ONE namespace, and a requirement's
22
+ // `target` may be written in any of three
23
+ // ---------------------------------------------------------------------------
24
+ //
25
+ // A preservation requirement's `relationships[].target`
26
+ // (`src/lib/slide-preservation.mjs`) is minted in the namespace its `role`
27
+ // belongs to, and they are not interchangeable:
28
+ //
29
+ // role target is resolved by
30
+ // start / end an ELEMENT id on this slide this module
31
+ // package-part a WORKSPACE path the caller
32
+ // (`conversions/<stem>.slide.pptx`)
33
+ // native-slot an INDEX into the conversion the caller
34
+ // record's `preserved[]`
35
+ //
36
+ // `resolvedTargets` contains element ids and the package's own part paths. It
37
+ // does NOT contain workspace paths or slot indices, and it never will — this
38
+ // module reads one .pptx and knows nothing about a deck's conversion records.
39
+ //
40
+ // **The consequence, stated so nobody has to rediscover it: resolving a
41
+ // non-element target against this set produces a FALSE DELETION.** The
42
+ // obligation looks unmet, the slide looks broken, and the gate refuses a
43
+ // delivery that lost nothing. That is strictly worse than the `unknown` it
44
+ // replaced, and it reached `PRESERVATION FAILED` on a clean deck once already.
45
+ //
46
+ // A caller holding requirements of mixed roles must resolve each one in its own
47
+ // namespace and union the results — which is what `packageEvidenceFor` in the
48
+ // delivery gate does. There is deliberately no `targetsByRole` here: one
49
+ // consumer exists and it has solved this, and a second mechanism for one job
50
+ // would only be a second thing to keep in step. Add it when a second consumer
51
+ // makes the shape obvious.
52
+ //
53
+ // Four rules it exists to keep:
54
+ //
55
+ // * **One normalizer.** The units, their order and the MC-block rule come
56
+ // from `computeProjection`'s own `topLevelShapes`. A second split would
57
+ // drift, and a drifted split relabels every element on the slide.
58
+ // * **No ZIP byte streams, and no part PATHS either.** Package timestamps,
59
+ // relationship ids, part ordering and part NAMES all legitimately change —
60
+ // delivery renames an imported embedding whenever the name is taken. What
61
+ // is compared is the normalized projection, the RESOLVED targets of
62
+ // relationships, and, for an opaque payload, the digest of its bytes. A
63
+ // path-keyed comparison reports a relocated object as a deleted one.
64
+ // * **Identity, never count.** A native object is bound to the parts it
65
+ // depends on and a connector to the shape ids at its ends. Swapping either
66
+ // for something else leaves the counts identical, which is exactly how E11
67
+ // and E17 get through a check that only counts.
68
+ // * **A moved object is still that object.** E17's clause: an allowed
69
+ // position change must not lose the native or its closure. Geometry lives
70
+ // in its own scope, so moving a frame changes `geom` and nothing else —
71
+ // the closure digests are computed from the parts, not from the placement.
72
+
73
+ import { createHash } from "node:crypto";
74
+
75
+ import { relsFor, resolvePartPath, slideOrderPaths } from "./ooxml.mjs";
76
+ import { computeProjection, topLevelShapes } from "./tags.mjs";
77
+ import { readText } from "./zip.mjs";
78
+
79
+ const sha256 = (bytes) => createHash("sha256").update(bytes).digest("hex");
80
+
81
+ /** Relationship types whose target is a document of its own, not a payload. */
82
+ const NON_PAYLOAD_REL = /\/(slideLayout|slideMaster|notesSlide|theme|tags)$/;
83
+
84
+ // ---------------------------------------------------------------------------
85
+ // Identity
86
+ // ---------------------------------------------------------------------------
87
+
88
+ /**
89
+ * The `p:cNvPr/@id` of one top-level unit, as a string.
90
+ *
91
+ * The FIRST id in the unit's own XML: for an `mc:AlternateContent` block that
92
+ * is the `mc:Choice` branch's frame, which is the object itself — the
93
+ * `mc:Fallback` picture beside it carries a different id and is a preview of
94
+ * the same thing, never a second element. This is the same spelling the
95
+ * converted dialect uses (`data-sm-source-id={60}`) and the same one a
96
+ * preservation requirement for an imported slide carries, so a record, a source
97
+ * file and a package all name an element one way.
98
+ */
99
+ export function unitElementId(unitXml) {
100
+ const m = /<p:cNvPr\b[^>]*\bid="(\d+)"/.exec(String(unitXml ?? ""));
101
+ return m ? m[1] : null;
102
+ }
103
+
104
+ // ---------------------------------------------------------------------------
105
+ // Dependency closure
106
+ // ---------------------------------------------------------------------------
107
+
108
+ /** Every `r:id` / `r:embed` / `r:link` a unit's XML names. */
109
+ function relIdsIn(unitXml) {
110
+ const ids = new Set();
111
+ for (const m of String(unitXml ?? "").matchAll(/\br:(?:id|embed|link|pict|dm|lo|qs|cs)="([^"]+)"/g)) ids.add(m[1]);
112
+ return ids;
113
+ }
114
+
115
+ /**
116
+ * The parts reachable from `starts`, with each one's digest.
117
+ *
118
+ * Breadth-first over the package's own relationships, so a chart's embedded
119
+ * workbook and an OLE object's sidecars are in the closure even though the
120
+ * slide never names them directly. External targets are skipped: they are a
121
+ * URL, not bytes this package can lose. Layouts, masters, themes, notes and
122
+ * tag parts are skipped too — they are documents the whole deck shares, not
123
+ * this object's payload, and pulling them in would make every element's
124
+ * closure the entire deck.
125
+ *
126
+ * @returns {Record<string, string>} part path → sha256 of its bytes
127
+ */
128
+ export function dependencyDigests(files, starts) {
129
+ const digests = {};
130
+ const queue = [...starts];
131
+ const seen = new Set(starts);
132
+ while (queue.length) {
133
+ const part = queue.shift();
134
+ const bytes = files[part];
135
+ if (bytes === undefined) continue; // a missing part is reported by its absence
136
+ digests[part] = sha256(bytes);
137
+ const dir = part.slice(0, part.lastIndexOf("/"));
138
+ for (const rel of relsFor(files, part)) {
139
+ if (rel.targetMode === "External") continue;
140
+ if (NON_PAYLOAD_REL.test(rel.type ?? "")) continue;
141
+ const target = resolvePartPath(dir, rel.target);
142
+ if (!target || seen.has(target)) continue;
143
+ seen.add(target);
144
+ queue.push(target);
145
+ }
146
+ }
147
+ return digests;
148
+ }
149
+
150
+ /**
151
+ * Turn a path -> digest map into the map `comparePackageEvidence` compares:
152
+ * keyed by CONTENT, valued by how many parts carry those bytes.
153
+ *
154
+ * Not by path, and that is the whole point. Delivery renames parts — an
155
+ * imported OLE embedding comes back as `oleObject2.bin` when its neighbour
156
+ * already holds `oleObject1.bin` — so a path-keyed map reports a relocated
157
+ * object as a DELETED one. A false deletion is the worst direction for a
158
+ * preservation gate to be wrong in: it refuses a delivery that lost nothing.
159
+ * Bytes are what a native object's dependency actually is; where the package
160
+ * files them is the package's business.
161
+ *
162
+ * The value is the COUNT rather than the digest again, so that dropping one of
163
+ * two parts that happen to share bytes still moves the map. The readable
164
+ * path->digest mapping stays in `natives[elementId].digests`, because a human
165
+ * reading a failure needs the path and a comparison must not.
166
+ */
167
+ function contentKeyedParts(digests) {
168
+ const parts = {};
169
+ for (const sha of Object.values(digests)) {
170
+ const key = `sha256:${sha}`;
171
+ parts[key] = String((Number(parts[key] ?? 0)) + 1);
172
+ }
173
+ return parts;
174
+ }
175
+
176
+ // ---------------------------------------------------------------------------
177
+ // Native objects
178
+ // ---------------------------------------------------------------------------
179
+
180
+ /** Does this unit carry an opaque object the package must keep whole? */
181
+ export function nativeKindOf(unitXml) {
182
+ const xml = String(unitXml ?? "");
183
+ if (/<p:oleObj\b/.test(xml)) return "ole";
184
+ if (/<p:contentPart\b/.test(xml)) return "contentPart";
185
+ if (/graphicData[^>]*uri="[^"]*\/(diagram|smartArt)"/.test(xml)) return "diagram";
186
+ if (/<mc:AlternateContent\b/.test(xml)) return "alternateContent";
187
+ return null;
188
+ }
189
+
190
+ /** The `progId` an OLE object declares — its identity beyond its bytes. */
191
+ const progIdOf = (unitXml) => /\bprogId="([^"]*)"/.exec(String(unitXml ?? ""))?.[1] ?? null;
192
+
193
+ // ---------------------------------------------------------------------------
194
+ // Charts — data residue, not a re-serialization digest
195
+ // ---------------------------------------------------------------------------
196
+
197
+ /**
198
+ * A chart's DATA as the package carries it: every series' name, its category
199
+ * cache and its value cache, with the formula each came from.
200
+ *
201
+ * Deliberately not a digest of the chart part. PowerPoint re-serializes a chart
202
+ * on every save and materializes its own defaults into it, so an XML hash
203
+ * reports drift on a deck nobody touched (the note in `src/pptx/tags.mjs` says
204
+ * the same thing about why this repo has no ChartSpec hash). The cached values
205
+ * and the refs they came from are what a build must not silently lose, and they
206
+ * survive that round trip.
207
+ *
208
+ * This is NOT the add-in's `ProjectionShape.chart`, which is a hash over its
209
+ * canonical ChartSpec. Both answer "did this chart's data change"; they are not
210
+ * interchangeable, which is why a verdict is always computed from ONE
211
+ * composer's own before and after.
212
+ */
213
+ export function chartResidue(chartXml) {
214
+ const xml = String(chartXml ?? "");
215
+ const series = [];
216
+ for (const ser of xml.matchAll(/<c:ser\b[\s\S]*?<\/c:ser>/g)) {
217
+ const block = ser[0];
218
+ const cache = (section) => {
219
+ const found = block.match(new RegExp(`<c:${section}\\b[\\s\\S]*?</c:${section}>`));
220
+ if (!found) return null;
221
+ return {
222
+ f: /<c:f>([\s\S]*?)<\/c:f>/.exec(found[0])?.[1] ?? null,
223
+ pts: [...found[0].matchAll(/<c:pt\b[^>]*idx="(\d+)"[^>]*>\s*<c:v>([\s\S]*?)<\/c:v>/g)].map((m) => [
224
+ Number(m[1]),
225
+ m[2],
226
+ ]),
227
+ };
228
+ };
229
+ series.push({ idx: Number(/<c:idx\b[^>]*val="(\d+)"/.exec(block)?.[1] ?? 0), tx: cache("tx"), cat: cache("cat"), val: cache("val") });
230
+ }
231
+ series.sort((a, b) => a.idx - b.idx);
232
+ return series.length ? series : null;
233
+ }
234
+
235
+ // ---------------------------------------------------------------------------
236
+ // Tables
237
+ // ---------------------------------------------------------------------------
238
+
239
+ /**
240
+ * A table's GRID: how many rows and columns, and which cells are merge
241
+ * continuations. Cell TEXT is already in the projection's `text` scope
242
+ * (`parseTextRuns` walks `a:tbl`), so this is the part that text alone cannot
243
+ * prove — a merged cell whose neighbour was absorbed keeps every word.
244
+ *
245
+ * Sibling evidence, not a property scope: `PROPERTY_SCOPES` is a shared
246
+ * contract (`src/lib/edit-operations.mjs`) and a table grid is not one of the
247
+ * seven things a requirement can protect. A round-trip test asserts it
248
+ * directly; a row that genuinely disappears also loses its text, which the
249
+ * verdict does see.
250
+ */
251
+ export function tableGrid(unitXml) {
252
+ const tbl = String(unitXml ?? "").match(/<a:tbl\b[\s\S]*?<\/a:tbl>/)?.[0];
253
+ if (!tbl) return null;
254
+ const declared = (tbl.match(/<a:gridCol\b/g) ?? []).length;
255
+ const rows = [...tbl.matchAll(/<a:tr\b[\s\S]*?<\/a:tr>/g)].map((r) =>
256
+ [...r[0].matchAll(/<a:tc\b([^>]*)>/g)].map((c) => {
257
+ const attrs = c[1];
258
+ const span = /\bgridSpan="(\d+)"/.exec(attrs)?.[1];
259
+ const row = /\browSpan="(\d+)"/.exec(attrs)?.[1];
260
+ if (/\bhMerge="1"/.test(attrs)) return "h";
261
+ if (/\bvMerge="1"/.test(attrs)) return "v";
262
+ if (span || row) return `${span ?? 1}x${row ?? 1}`;
263
+ return ".";
264
+ }),
265
+ );
266
+ // A table without an `a:tblGrid` still has a column count — its widest row.
267
+ return { cols: declared || Math.max(0, ...rows.map((r) => r.length)), rows };
268
+ }
269
+
270
+ // ---------------------------------------------------------------------------
271
+ // The composer
272
+ // ---------------------------------------------------------------------------
273
+
274
+ /**
275
+ * Compose one slide's package evidence.
276
+ *
277
+ * @param {Record<string, Uint8Array>} files an unzipped package
278
+ * @param {{slidePart?: string|null}} [opts]
279
+ * @returns {{
280
+ * slidePart: string|null,
281
+ * shapes: Record<string, object>,
282
+ * resolvedTargets: string[],
283
+ * tables: Record<string, {cols: number, rows: string[][]}>,
284
+ * natives: Record<string, {kind: string, progId: string|null, parts: string[]}>,
285
+ * unidentified: number,
286
+ * }}
287
+ */
288
+ export function composeSlideEvidence(files, { slidePart = null } = {}) {
289
+ const part = slidePart ?? slideOrderPaths(files)[0] ?? null;
290
+ const empty = { slidePart: part, shapes: {}, resolvedTargets: [], tables: {}, natives: {}, connectors: {}, unidentified: 0 };
291
+ if (!part) return empty;
292
+ const xml = readText(files, part) ?? "";
293
+ const spTree = xml.match(/<p:spTree>([\s\S]*?)<\/p:spTree>/)?.[1] ?? "";
294
+ const units = topLevelShapes(spTree);
295
+ const { shapes: projected } = computeProjection(files, part);
296
+ if (units.length !== projected.length) return empty; // two readers disagreeing is not evidence
297
+
298
+ const dir = part.slice(0, part.lastIndexOf("/"));
299
+ const relsById = new Map();
300
+ const targets = new Set();
301
+ for (const rel of relsFor(files, part)) {
302
+ if (rel.targetMode === "External") continue;
303
+ const resolved = resolvePartPath(dir, rel.target);
304
+ if (rel.id) relsById.set(rel.id, { type: rel.type ?? "", target: resolved });
305
+ if (resolved && files[resolved] !== undefined) targets.add(resolved);
306
+ }
307
+
308
+ const out = { ...empty, shapes: {}, tables: {}, natives: {}, connectors: {} };
309
+ units.forEach((unit, i) => {
310
+ const elementId = unitElementId(unit.xml);
311
+ if (!elementId) {
312
+ out.unidentified += 1;
313
+ return;
314
+ }
315
+ // An id that appears twice is not an identity. Report the collision by
316
+ // dropping both rather than letting one silently answer for the other.
317
+ if (out.shapes[elementId]) {
318
+ // Both claimants lose their identity, so both are counted: one of them
319
+ // silently answering for the other is the failure mode this avoids.
320
+ delete out.shapes[elementId];
321
+ out.unidentified += 2;
322
+ return;
323
+ }
324
+ const shape = { ...projected[i] };
325
+
326
+ // A connector's ENDS, exactly as it claims them. These are the only element
327
+ // ids that become resolvable targets: a target is something something
328
+ // points AT, and adding every id on the slide would make "the end is still
329
+ // there" true however the connector was repointed — the same
330
+ // count-is-not-identity hole E11 walks through. `connectors` below carries
331
+ // the per-element pairing for a precise check.
332
+ const ends = [...unit.xml.matchAll(/<a:(stCxn|endCxn)\b[^>]*\bid="(\d+)"/g)].map((m) => [
333
+ m[1] === "stCxn" ? "start" : "end",
334
+ m[2],
335
+ ]);
336
+ if (ends.length) {
337
+ out.connectors[elementId] = Object.fromEntries(ends);
338
+ for (const [, id] of ends) targets.add(id);
339
+ }
340
+
341
+ // The parts this unit depends on, transitively.
342
+ const starts = [];
343
+ for (const id of relIdsIn(unit.xml)) {
344
+ const rel = relsById.get(id);
345
+ if (!rel?.target || NON_PAYLOAD_REL.test(rel.type)) continue;
346
+ starts.push(rel.target);
347
+ }
348
+ const digests = starts.length ? dependencyDigests(files, starts) : {};
349
+
350
+ const nativeKind = nativeKindOf(unit.xml);
351
+ if (nativeKind) {
352
+ // A native object's obligation IS its closure plus what it claims to be.
353
+ // `progId` rides in the map so a payload swapped for another application's
354
+ // object fails even when the byte count is identical.
355
+ const parts = contentKeyedParts(digests);
356
+ const progId = progIdOf(unit.xml);
357
+ if (progId) parts[`progId:${progId}`] = progId;
358
+ shape.native = { parts };
359
+ out.natives[elementId] = { kind: nativeKind, progId, parts: Object.keys(digests), digests };
360
+ } else if (Object.keys(digests).length) {
361
+ // An ordinary unit's payload (a picture's media, a chart's part) is still
362
+ // recorded, so a dropped dependency is visible even where the element
363
+ // itself survived.
364
+ shape.native = { parts: contentKeyedParts(digests) };
365
+ }
366
+
367
+ // A chart frame's data residue, hashed so the comparison stays a string
368
+ // equality like every other scope.
369
+ const chartRel = [...relIdsIn(unit.xml)].map((id) => relsById.get(id)).find((r) => r?.type.endsWith("/chart"));
370
+ if (chartRel?.target) {
371
+ const residue = chartResidue(readText(files, chartRel.target));
372
+ if (residue) shape.chart = sha256(JSON.stringify(residue));
373
+ }
374
+
375
+ const grid = tableGrid(unit.xml);
376
+ if (grid) out.tables[elementId] = grid;
377
+
378
+ out.shapes[elementId] = shape;
379
+ });
380
+
381
+ out.resolvedTargets = [...targets].sort();
382
+ return out;
383
+ }