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
@@ -0,0 +1,234 @@
1
+ // What a source image needs before a renderer can ship it — recorded as data,
2
+ // never performed here.
3
+ //
4
+ // The CLI owns no decoder and no converter (design decision: "do not add an
5
+ // independent image-processing service", and this package has no image
6
+ // dependency at all). What it CAN do is read a header and say, deterministically,
7
+ // what the picture pipeline will have to do with those bytes:
8
+ //
9
+ // * a JPEG whose EXIF says it is rotated is presented rotated — a RENDERING
10
+ // transform, so the stored digest still hashes the raw imported bytes;
11
+ // * an animated source is presented as its first frame, with a static-output
12
+ // note, because a PPTX picture promises no animation;
13
+ // * a format the OOXML adapter cannot embed as-is (WebP, SVG) ships as a
14
+ // derivative while the source stays in the workspace, untouched.
15
+ //
16
+ // Each of those produces a DERIVATIVE RECORD — parent hash, format, transform
17
+ // version — which is the contract's evidence that the thing embedded in the
18
+ // .pptx descends from the registered source. The bytes of the derivative are
19
+ // produced at capture by the image decoration both clients run (the add-in's
20
+ // build-pptx/image-decorate.ts, served to the CLI in the capture engine): it
21
+ // asks `renderTransforms` below about the same bytes, so the declaration here
22
+ // and the picture that ships cannot disagree, and the export check accepts the
23
+ // derivative only by the parent digest the capture recorded for it.
24
+ //
25
+ // Mirrored byte-identically into the add-in (shared/plan-contract/lib), which
26
+ // renders with it and words the same notes from it.
27
+ //
28
+ // Every derivative is PNG. One rule instead of three: it is lossless (a JPEG
29
+ // re-encode for a rotation is not), it keeps alpha (a transparent PNG must never
30
+ // come back flattened), and every renderer on both sides embeds it natively.
31
+
32
+ /** Bumped when a transform's OUTPUT changes, so old derivatives are recognised as stale. */
33
+ export const IMAGE_TRANSFORM_VERSION = 1;
34
+
35
+ // What the OOXML adapter embeds without conversion; the add-in's image
36
+ // decoration and export check take their list from here. Anything else the
37
+ // contract supports — WebP — needs a derivative. An SVG ships as itself: the
38
+ // file rides in the picture's `asvg:svgBlip`, with the capture's raster as the
39
+ // fallback blip, so it needs none.
40
+ export const NATIVE_RASTER_MIME_TYPES = ["image/png", "image/jpeg", "image/gif"];
41
+ const SHIPS_AS_ITSELF = new Set([...NATIVE_RASTER_MIME_TYPES, "image/svg+xml"]);
42
+
43
+ export const DERIVATIVE_FORMAT = "image/png";
44
+
45
+ const startsWith = (buf, offset, bytes) =>
46
+ buf.length >= offset + bytes.length && bytes.every((b, i) => buf[offset + i] === b);
47
+
48
+ const ascii = (s) => [...s].map((c) => c.charCodeAt(0));
49
+
50
+ // ---------------------------------------------------------------------------
51
+ // EXIF orientation
52
+ // ---------------------------------------------------------------------------
53
+
54
+ // The Orientation tag out of a TIFF header that starts at `base` (just past
55
+ // "Exif\0\0"). Values 1..8; 5..8 are the ones that swap width and height.
56
+ function orientationFromTiff(buf, base) {
57
+ if (base + 8 > buf.length) return null;
58
+ const byteOrder = String.fromCharCode(buf[base], buf[base + 1]);
59
+ const le = byteOrder === "II";
60
+ if (!le && byteOrder !== "MM") return null;
61
+ const u16 = (o) => (le ? buf.readUInt16LE(o) : buf.readUInt16BE(o));
62
+ const u32 = (o) => (le ? buf.readUInt32LE(o) : buf.readUInt32BE(o));
63
+ if (u16(base + 2) !== 42) return null;
64
+ const ifd = base + u32(base + 4);
65
+ if (ifd + 2 > buf.length) return null;
66
+ const entries = u16(ifd);
67
+ for (let i = 0; i < entries; i++) {
68
+ const at = ifd + 2 + i * 12;
69
+ if (at + 12 > buf.length) return null;
70
+ if (u16(at) !== 0x0112) continue;
71
+ const value = u16(at + 8); // SHORT, so the value sits inline
72
+ return value >= 1 && value <= 8 ? value : null;
73
+ }
74
+ return null;
75
+ }
76
+
77
+ /**
78
+ * A JPEG's EXIF orientation (1..8), or null when it carries none.
79
+ *
80
+ * Walks the marker segments the same way `imageSizeFromBuffer` does, and stops
81
+ * at the start of scan data — EXIF never appears after it.
82
+ */
83
+ export function exifOrientation(buf) {
84
+ if (!buf || !startsWith(buf, 0, [0xff, 0xd8, 0xff])) return null;
85
+ let pos = 2;
86
+ while (pos + 4 <= buf.length) {
87
+ if (buf[pos] !== 0xff) {
88
+ pos++;
89
+ continue;
90
+ }
91
+ const marker = buf[pos + 1];
92
+ if (marker === 0xff) {
93
+ pos++; // fill byte
94
+ continue;
95
+ }
96
+ if (marker === 0xd8 || marker === 0x01 || (marker >= 0xd0 && marker <= 0xd7)) {
97
+ pos += 2; // standalone, no payload
98
+ continue;
99
+ }
100
+ if (marker === 0xda || marker === 0xd9) return null; // scan data / end
101
+ const length = buf.readUInt16BE(pos + 2);
102
+ if (length < 2) return null;
103
+ if (marker === 0xe1 && buf.length >= pos + 10 && buf.toString("latin1", pos + 4, pos + 10) === "Exif\0\0") {
104
+ return orientationFromTiff(buf, pos + 10);
105
+ }
106
+ pos += 2 + length;
107
+ }
108
+ return null;
109
+ }
110
+
111
+ // ---------------------------------------------------------------------------
112
+ // animation
113
+ // ---------------------------------------------------------------------------
114
+
115
+ /**
116
+ * Does this GIF carry more than one frame?
117
+ *
118
+ * Walks the block structure and stops at the second image descriptor — no
119
+ * decoding, so an unparseable stream answers "not animated" and is embedded as
120
+ * it stands rather than being declared a derivative on a guess.
121
+ */
122
+ export function gifIsAnimated(buf) {
123
+ if (!buf || !startsWith(buf, 0, ascii("GIF8")) || buf.length < 14) return false;
124
+ const flags = buf[10];
125
+ let pos = 13;
126
+ if (flags & 0x80) pos += 3 * 2 ** ((flags & 0x07) + 1); // global colour table
127
+ const skipSubBlocks = () => {
128
+ while (pos < buf.length) {
129
+ const size = buf[pos++];
130
+ if (size === 0) return true;
131
+ pos += size;
132
+ }
133
+ return false;
134
+ };
135
+ let frames = 0;
136
+ while (pos < buf.length) {
137
+ const block = buf[pos];
138
+ if (block === 0x3b) return false; // trailer
139
+ if (block === 0x21) {
140
+ pos += 2; // introducer + label
141
+ if (!skipSubBlocks()) return false;
142
+ continue;
143
+ }
144
+ if (block !== 0x2c) return false; // not a structure we can walk
145
+ if (++frames > 1) return true;
146
+ if (pos + 10 > buf.length) return false;
147
+ const local = buf[pos + 9];
148
+ pos += 10;
149
+ if (local & 0x80) pos += 3 * 2 ** ((local & 0x07) + 1); // local colour table
150
+ pos += 1; // LZW minimum code size
151
+ if (!skipSubBlocks()) return false;
152
+ }
153
+ return false;
154
+ }
155
+
156
+ // ---------------------------------------------------------------------------
157
+ // the plan
158
+ // ---------------------------------------------------------------------------
159
+
160
+ /**
161
+ * The transforms a renderer must apply to ship these bytes, in the order the
162
+ * plan names them: `exif-orientation` (a rotated JPEG ships upright),
163
+ * `first-frame` (an animated GIF ships its first frame), `format-conversion`
164
+ * (a format the adapter cannot embed). Empty when the source ships as itself.
165
+ * The one decision both the plan's notes and the capture's image decoration
166
+ * read.
167
+ *
168
+ * @param {string} mimeType the sniffed type of `bytes`
169
+ * @param {Buffer} bytes
170
+ */
171
+ export function renderTransforms(mimeType, bytes) {
172
+ const transforms = [];
173
+ if (mimeType === "image/jpeg") {
174
+ const orientation = exifOrientation(bytes);
175
+ if (orientation && orientation !== 1) transforms.push("exif-orientation");
176
+ } else if (mimeType === "image/gif" && gifIsAnimated(bytes)) {
177
+ transforms.push("first-frame");
178
+ }
179
+ if (!SHIPS_AS_ITSELF.has(mimeType)) transforms.push("format-conversion");
180
+ return transforms;
181
+ }
182
+
183
+ /**
184
+ * How `asset`'s bytes reach a slide: as themselves, or through a declared
185
+ * derivative.
186
+ *
187
+ * Returns `{native, presented, transforms, derivative, notes}`. `presented` is
188
+ * the asset's own size: the registry on both clients records the
189
+ * orientation-corrected size a viewer sees — the one a crop fraction and an
190
+ * `<img>` box are expressed in — while the digest hashes the raw bytes.
191
+ * `derivative` is null when the source ships untouched.
192
+ *
193
+ * @param {{sha256: string, mimeType: string, width: number, height: number}} asset
194
+ * @param {Buffer} bytes the asset's raw imported bytes
195
+ */
196
+ export function imageRenderPlan(asset, bytes) {
197
+ const transforms = renderTransforms(asset.mimeType, bytes);
198
+ const notes = [];
199
+ const presented = { width: asset.width, height: asset.height };
200
+
201
+ if (transforms.includes("exif-orientation")) {
202
+ const orientation = exifOrientation(bytes);
203
+ notes.push(
204
+ `EXIF orientation ${orientation}: the render ships the picture upright, as a PNG derivative. ` +
205
+ `The stored digest hashes the raw imported bytes, so it does not change.`,
206
+ );
207
+ }
208
+ if (transforms.includes("first-frame")) {
209
+ notes.push(
210
+ "Animated source: the slide shows its first frame, shipped as a PNG derivative. A PowerPoint picture " +
211
+ "is static here — the animation is not promised, and the full source stays in the workspace.",
212
+ );
213
+ }
214
+ if (transforms.includes("format-conversion")) {
215
+ notes.push(
216
+ `${asset.mimeType} cannot be embedded as it stands: the render ships a PNG derivative of it, ` +
217
+ `and the source is kept unchanged.`,
218
+ );
219
+ }
220
+
221
+ const derivative = transforms.length
222
+ ? {
223
+ parentSha256: asset.sha256,
224
+ parentMimeType: asset.mimeType,
225
+ format: DERIVATIVE_FORMAT,
226
+ transformVersion: IMAGE_TRANSFORM_VERSION,
227
+ transforms: [...transforms],
228
+ width: presented.width,
229
+ height: presented.height,
230
+ }
231
+ : null;
232
+
233
+ return { native: derivative === null, presented, transforms, derivative, notes };
234
+ }
@@ -0,0 +1,70 @@
1
+ // The SHA-256 half of the image-intent contract (design decision 6).
2
+ //
3
+ // Split out of `image-intent.mjs` for one reason: that module is concatenated
4
+ // into the plan gate's browser bundle and may not touch `node:crypto`. The
5
+ // projection — which fields count, in what order — lives there, in
6
+ // `imageIntentFingerprintInput()`; only the hashing lives here.
7
+ //
8
+ // `canonicalJson` is a line-for-line port of the add-in's
9
+ // `src/ui/src/node/agent/storage/canonical-hash.ts`, which the add-in side
10
+ // reuses as-is: recursively sorted object keys, array order preserved, UTF-8
11
+ // JSON, `undefined` object properties dropped, non-finite numbers and
12
+ // `undefined` array entries as `null`. The two implementations are held
13
+ // together by `test/fixtures/image-intent/contract.json`, which carries the
14
+ // expected canonical string AND digest for each case — not by shared code,
15
+ // since the CLI cannot import from the sibling repository.
16
+
17
+ import { createHash } from "node:crypto";
18
+
19
+ import { imageIntentFingerprintInput } from "./image-intent.mjs";
20
+
21
+ function normalize(value) {
22
+ if (value === null || typeof value === "string" || typeof value === "boolean") {
23
+ return value;
24
+ }
25
+ if (typeof value === "number") {
26
+ return Number.isFinite(value) ? value : null;
27
+ }
28
+ if (Array.isArray(value)) {
29
+ return value.map((entry) =>
30
+ entry === undefined || typeof entry === "function" ? null : normalize(entry),
31
+ );
32
+ }
33
+ if (typeof value === "object") {
34
+ const normalized = {};
35
+ for (const key of Object.keys(value).sort()) {
36
+ const entry = value[key];
37
+ if (entry === undefined || typeof entry === "function" || typeof entry === "symbol") {
38
+ continue;
39
+ }
40
+ normalized[key] = normalize(entry);
41
+ }
42
+ return normalized;
43
+ }
44
+ throw new TypeError("Value is not JSON-compatible");
45
+ }
46
+
47
+ /** Canonical JSON: sorted keys, preserved array order. */
48
+ export function canonicalJson(value) {
49
+ return JSON.stringify(normalize(value));
50
+ }
51
+
52
+ export function sha256Hex(value) {
53
+ return createHash("sha256").update(value).digest("hex");
54
+ }
55
+
56
+ export function canonicalSha256(value) {
57
+ return sha256Hex(canonicalJson(value));
58
+ }
59
+
60
+ /**
61
+ * The fingerprint a concept, spec or validation record is keyed by.
62
+ *
63
+ * Changing an instruction, a source's bytes, a crop or the set of uses changes
64
+ * it; renaming the file or moving the workspace does not.
65
+ *
66
+ * @param {{uses?: Array, assets?: Array, context?: unknown}} args
67
+ */
68
+ export function imageIntentFingerprint(args = {}) {
69
+ return canonicalSha256(imageIntentFingerprintInput(args));
70
+ }
@@ -0,0 +1,176 @@
1
+ // Image provenance through the deck-embedded source tags — one slide's
2
+ // approved image obligations, baked into the .pptx it was rendered into and
3
+ // read back out when that file is adopted again (design decision 8, A27).
4
+ //
5
+ // Why it rides the slide's own tags rather than a sidecar: the .pptx is the
6
+ // only thing that travels. A rendered deck is mailed, opened, saved and handed
7
+ // back, and `slidemind adopt` rebuilds a workspace from it alone. Everything
8
+ // else about a slide already survives that round trip — its jsx, its assets,
9
+ // its identity, its drift baseline. An obligation that did not would come back
10
+ // as a picture nobody could prove was the user's own.
11
+ //
12
+ // Two halves, both narrow:
13
+ //
14
+ // BAKE `imageIntentTagMeta` projects the slide's effective uses and the
15
+ // registry entries they name into `SRC_META.imageIntent`. The BYTES
16
+ // need no arrangement: the jsx references `./assets/local/<id>.<ext>`,
17
+ // so `referencedAssets` already embeds them within the tag budget.
18
+ // RESTORE `restoreImageIntentFromTags` puts both back — registry entries
19
+ // through `registerImageAssets` (identity preserved, bytes proved),
20
+ // uses through `setSlideImageUses` keyed by the slide's own GUID.
21
+ //
22
+ // The restore is deliberately conservative in three ways:
23
+ //
24
+ // * **A file with no `imageIntent` gains nothing.** A plain imported .pptx —
25
+ // one slidemind never rendered, or one from before this contract — is
26
+ // legacy and stays legacy. An obligation is never manufactured from a
27
+ // picture that happens to be on a slide (design decision 8, A27).
28
+ // * **Only a slide restored from its own proved-current source is restored at
29
+ // all.** A slide that had to be re-converted from its edited self is
30
+ // rebuilt content — adopt itself judged the embedded source stale — and its
31
+ // picture may have been moved, replaced or deleted in PowerPoint. Asserting
32
+ // the old obligation over it would refuse every later build for an edit
33
+ // nobody made to the picture. The bytes are still in the file; rebinding is
34
+ // a deliberate act, through the plan.
35
+ // * **Nothing is overwritten.** A known assetId with different bytes, a
36
+ // colliding useId, an asset whose bytes did not survive — each is reported
37
+ // and skipped. A partial restore says what it dropped.
38
+ //
39
+ // One known property, not a bug (confirmed with the add-in 2026-09-21): the
40
+ // add-in's own bake builds a FRESH meta from its own input (spec §5.3 write
41
+ // point 2 — a live insert or replace), so a slide re-pushed from the add-in
42
+ // comes back without this key. Its obligations are not lost — the add-in keeps
43
+ // its own record in its workspace, and the deck's registry and plan here are
44
+ // untouched — so the next CLI build re-writes the key from the registry rather
45
+ // than from the tags. What does not survive is that one slide's provenance
46
+ // INSIDE a .pptx pushed from the add-in and adopted without a CLI build in
47
+ // between; it adopts as a slide with a picture and no obligation, which is the
48
+ // legacy case and refuses nothing.
49
+
50
+ import {
51
+ findAssetById,
52
+ readImageAssetManifest,
53
+ registerImageAssets,
54
+ } from "./image-assets.mjs";
55
+ import { setSlideImageUses } from "./image-use-overrides.mjs";
56
+ import { IMAGE_INTENT_VERSION, validateSlideUses } from "./image-intent.mjs";
57
+ import { effectiveUsesForSlides } from "../bridge/image-intent-envelope.mjs";
58
+
59
+ export const IMAGE_INTENT_META_KEY = "imageIntent";
60
+
61
+ /**
62
+ * The `SRC_META.imageIntent` payload for each of `slides`, as
63
+ * `Map<slide, meta>` — a slide with no image use is absent, which keeps its
64
+ * tags byte-identical to what they were before this contract existed.
65
+ *
66
+ * Resolved for the WHOLE bake in one call on purpose: the effective-use lookup
67
+ * re-reads and re-hashes every slide file to map overrides onto slide numbers,
68
+ * so asking it per slide makes a deck pay for that once per slide.
69
+ *
70
+ * @param {string} deckDir the deck workspace
71
+ * @param {number[]} slides 1-based slide numbers (each file's NN- prefix)
72
+ */
73
+ export function imageIntentTagMetaForSlides(deckDir, slides, { planMarkdown = null } = {}) {
74
+ const out = new Map();
75
+ const wanted = (slides ?? []).filter((n) => Number.isInteger(n) && n > 0);
76
+ if (!wanted.length) return out;
77
+ let bySlide;
78
+ let manifest;
79
+ try {
80
+ bySlide = effectiveUsesForSlides(deckDir, wanted, { planMarkdown });
81
+ manifest = readImageAssetManifest(deckDir);
82
+ } catch {
83
+ // An unreadable registry is reported by the commands that need it (the
84
+ // draft check, the envelope build). A bake is bookkeeping: it must not be
85
+ // the thing that fails a render.
86
+ return out;
87
+ }
88
+ for (const [slide, uses] of bySlide) {
89
+ if (!uses.length) continue;
90
+ const assets = [];
91
+ for (const assetId of [...new Set(uses.map((u) => u.assetId))].sort()) {
92
+ const asset = findAssetById(manifest, assetId);
93
+ if (asset) assets.push(asset);
94
+ }
95
+ if (assets.length) out.set(slide, { v: IMAGE_INTENT_VERSION, uses, assets });
96
+ }
97
+ return out;
98
+ }
99
+
100
+ /** One slide's payload, or null. */
101
+ export function imageIntentTagMeta(deckDir, slide, opts = {}) {
102
+ return imageIntentTagMetaForSlides(deckDir, [slide], opts).get(slide) ?? null;
103
+ }
104
+
105
+ /**
106
+ * Restore the image provenance a set of adopted slides carried.
107
+ *
108
+ * @param {string} deckDir the deck being adopted into
109
+ * @param {Array<{slideId: string|null, intent: object|null, restoreUses: boolean,
110
+ * renames?: Map<string,string>}>} entries
111
+ * one per adopted slide. `renames` is adopt's asset-collision map
112
+ * (`assets/local/x.png` → the name actually written), so a restored entry
113
+ * points at the file that really landed.
114
+ * @returns {{assets: number, slides: number, notes: string[]}}
115
+ */
116
+ export function restoreImageIntentFromTags(deckDir, entries) {
117
+ const notes = [];
118
+ const wanted = (entries ?? []).filter((e) => e?.intent && Array.isArray(e.intent.assets));
119
+ if (!wanted.length) return { assets: 0, slides: 0, notes };
120
+
121
+ // Every slide's assets in one manifest write — a re-adopted deck has one
122
+ // registry, not one per slide.
123
+ const descriptors = new Map();
124
+ for (const entry of wanted) {
125
+ if (entry.intent.v !== IMAGE_INTENT_VERSION) {
126
+ notes.push(
127
+ `a slide records image intent version ${entry.intent.v} — this CLI speaks version ${IMAGE_INTENT_VERSION}; its obligations were not restored`,
128
+ );
129
+ entry.unsupported = true;
130
+ continue;
131
+ }
132
+ for (const asset of entry.intent.assets) {
133
+ if (!asset?.assetId) continue;
134
+ const placed = entry.renames?.get(asset.workspacePath);
135
+ descriptors.set(asset.assetId, placed ? { ...asset, workspacePath: placed } : asset);
136
+ }
137
+ }
138
+ const { added, reused, rejected } = registerImageAssets(deckDir, [...descriptors.values()]);
139
+ for (const bad of rejected) {
140
+ notes.push(`image source ${bad.assetId ?? "(unnamed)"} was not restored: ${bad.reason}`);
141
+ }
142
+ const usable = new Set([...added, ...reused]);
143
+
144
+ let slides = 0;
145
+ for (const entry of wanted) {
146
+ if (entry.unsupported || !entry.restoreUses) continue;
147
+ if (!entry.slideId) {
148
+ notes.push("a slide with recorded image obligations has no durable identity — its uses were not restored");
149
+ continue;
150
+ }
151
+ const uses = Array.isArray(entry.intent.uses) ? entry.intent.uses : [];
152
+ if (!uses.length) continue;
153
+ const missing = uses.filter((use) => !usable.has(use?.assetId));
154
+ if (missing.length) {
155
+ notes.push(
156
+ `slide ${entry.slideId}: its image obligation names ${missing[0]?.assetId ?? "an asset"}, which this deck does not have — the use was not restored`,
157
+ );
158
+ continue;
159
+ }
160
+ // Shape-check before writing: the record came out of an untrusted file.
161
+ const parsed = validateSlideUses(uses, {
162
+ assetIds: readImageAssetManifest(deckDir).assets.map((a) => a.assetId),
163
+ });
164
+ if (!parsed.value) {
165
+ notes.push(`slide ${entry.slideId}: ${parsed.issues[0]?.message ?? "invalid image uses"} — not restored`);
166
+ continue;
167
+ }
168
+ try {
169
+ setSlideImageUses(deckDir, entry.slideId, parsed.value);
170
+ slides += 1;
171
+ } catch (err) {
172
+ notes.push(`slide ${entry.slideId}: ${err?.message ?? err} — its image uses were not restored`);
173
+ }
174
+ }
175
+ return { assets: added.length, slides, notes };
176
+ }