slidemind 0.3.20 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (185) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/.codex-plugin/plugin.json +1 -1
  3. package/AGENTS.md +591 -15
  4. package/README.md +202 -7
  5. package/bin/slidemind.mjs +50 -2
  6. package/package.json +2 -2
  7. package/skills/create-slides/SKILL.md +484 -886
  8. package/skills/search/SKILL.md +46 -23
  9. package/src/bridge/build-and-push.mjs +548 -73
  10. package/src/bridge/bundle.mjs +100 -31
  11. package/src/bridge/capture.mjs +14 -17
  12. package/src/bridge/client/brands.mjs +157 -18
  13. package/src/bridge/client/engine-routing.mjs +48 -13
  14. package/src/bridge/client/engine-search.mjs +24 -1
  15. package/src/bridge/client/render.mjs +27 -1
  16. package/src/bridge/client/transport.mjs +499 -23
  17. package/src/bridge/client.mjs +10 -0
  18. package/src/bridge/config.mjs +67 -6
  19. package/src/bridge/engine-download.mjs +0 -10
  20. package/src/bridge/graph-auth.mjs +55 -3
  21. package/src/bridge/image-intent-envelope.mjs +239 -0
  22. package/src/bridge/package-preview.mjs +67 -0
  23. package/src/bridge/sharepoint.mjs +914 -0
  24. package/src/builders/asset-fetch.mjs +203 -0
  25. package/src/builders/block-dividers.mjs +83 -0
  26. package/src/builders/build.mjs +171 -87
  27. package/src/builders/card-height.mjs +294 -0
  28. package/src/builders/chart-hole.mjs +183 -0
  29. package/src/builders/chart-jsx.mjs +410 -0
  30. package/src/builders/chart-labels.mjs +326 -0
  31. package/src/builders/concept-geometry.mjs +370 -0
  32. package/src/builders/concept-intent.mjs +200 -0
  33. package/src/builders/concept-layout.mjs +79 -0
  34. package/src/builders/concept-lines.mjs +509 -0
  35. package/src/builders/concept-lists.mjs +270 -0
  36. package/src/builders/concept-marks.mjs +84 -0
  37. package/src/builders/concept-relationships.mjs +12 -0
  38. package/src/builders/concept-spec.mjs +645 -0
  39. package/src/builders/concept-text.mjs +140 -0
  40. package/src/builders/concept-waves.mjs +57 -0
  41. package/src/builders/copy-fit.mjs +139 -0
  42. package/src/builders/design-tokens.mjs +347 -0
  43. package/src/builders/draft-check.mjs +552 -340
  44. package/src/builders/draft-lines.mjs +369 -0
  45. package/src/builders/edit-route.mjs +29 -0
  46. package/src/builders/figure-sketch.mjs +375 -0
  47. package/src/builders/fold-sets.mjs +639 -0
  48. package/src/builders/gate-server.mjs +9 -4
  49. package/src/builders/gate-session.js +273 -12
  50. package/src/builders/gate-shell.js +1494 -0
  51. package/src/builders/image-intent-check.mjs +308 -0
  52. package/src/builders/image-use-reconcile.mjs +107 -0
  53. package/src/builders/kit-helpers.mjs +121 -0
  54. package/src/builders/layout-names.mjs +32 -0
  55. package/src/builders/legacy-reference-uses.mjs +179 -0
  56. package/src/builders/line-fit.mjs +224 -0
  57. package/src/builders/line-native.mjs +895 -0
  58. package/src/builders/line-relations.mjs +230 -0
  59. package/src/builders/lint-fix.mjs +117 -9
  60. package/src/builders/lint.mjs +25 -20
  61. package/src/builders/list-jsx.mjs +88 -0
  62. package/src/builders/placement.mjs +133 -0
  63. package/src/builders/plan-checks.mjs +135 -0
  64. package/src/builders/plan-comments.mjs +19 -0
  65. package/src/builders/plan-handlers.mjs +95 -0
  66. package/src/builders/plan-markdown-edit.mjs +3 -3
  67. package/src/builders/plan-overlay.js +1530 -1608
  68. package/src/builders/plan.mjs +689 -186
  69. package/src/builders/preset-definitions.json +1 -0
  70. package/src/builders/preset-eval.mjs +244 -0
  71. package/src/builders/preview.html +51 -120
  72. package/src/builders/red-thread-backbone.mjs +135 -36
  73. package/src/builders/repair.mjs +205 -0
  74. package/src/builders/repeat-groups.mjs +540 -0
  75. package/src/builders/resolve-skill.mjs +5 -4
  76. package/src/builders/review-overlay.js +296 -293
  77. package/src/builders/review.mjs +305 -84
  78. package/src/builders/rule-shift.mjs +96 -0
  79. package/src/builders/seat.mjs +354 -0
  80. package/src/builders/settle-driver.mjs +236 -0
  81. package/src/builders/settle.mjs +1448 -0
  82. package/src/builders/slide-prep.mjs +67 -0
  83. package/src/builders/spec-assets.mjs +236 -0
  84. package/src/builders/spec-match.mjs +56 -0
  85. package/src/builders/stack.mjs +413 -0
  86. package/src/builders/storyboard-markdown.mjs +70 -27
  87. package/src/builders/storyboard-schema.mjs +54 -114
  88. package/src/builders/text-blocks.mjs +212 -0
  89. package/src/builders/text-card-fit.mjs +552 -0
  90. package/src/builders/text-stack.mjs +276 -0
  91. package/src/builders/transcribe.mjs +1069 -0
  92. package/src/cli/assets.mjs +164 -0
  93. package/src/cli/common.mjs +173 -12
  94. package/src/cli/concept-spec-support.mjs +76 -0
  95. package/src/cli/concepts.mjs +1002 -103
  96. package/src/cli/deck.mjs +401 -65
  97. package/src/cli/diagnose.mjs +4 -2
  98. package/src/cli/engine-helpers.mjs +17 -10
  99. package/src/cli/engine-results.mjs +76 -3
  100. package/src/cli/help.mjs +232 -28
  101. package/src/cli/install.mjs +9 -0
  102. package/src/cli/maintenance.mjs +104 -0
  103. package/src/cli/read.mjs +2 -1
  104. package/src/cli/ready.mjs +6 -3
  105. package/src/cli/record.mjs +522 -0
  106. package/src/cli/remote.mjs +818 -0
  107. package/src/cli/revision.mjs +686 -0
  108. package/src/cli/search.mjs +30 -11
  109. package/src/cli/session.mjs +56 -13
  110. package/src/cli/setup.mjs +46 -2
  111. package/src/cli/status.mjs +142 -33
  112. package/src/cli/tasks.mjs +246 -0
  113. package/src/cli/transcribe.mjs +854 -0
  114. package/src/cli/workspace-ui.mjs +260 -0
  115. package/src/cli/workspace.mjs +212 -34
  116. package/src/install-channel.mjs +64 -7
  117. package/src/lib/adopt.mjs +346 -25
  118. package/src/lib/concept-spec-binding.mjs +96 -0
  119. package/src/lib/conversions.mjs +70 -6
  120. package/src/lib/convert-folded.mjs +107 -0
  121. package/src/lib/deck-paths.mjs +418 -0
  122. package/src/lib/durable-ids.mjs +47 -0
  123. package/src/lib/edit-operations.mjs +316 -0
  124. package/src/lib/edit-state.mjs +1244 -0
  125. package/src/lib/edit-transitions.mjs +120 -0
  126. package/src/lib/extensions.mjs +501 -0
  127. package/src/lib/image-assets.mjs +465 -0
  128. package/src/lib/image-derivatives.mjs +234 -0
  129. package/src/lib/image-intent-fingerprint.mjs +70 -0
  130. package/src/lib/image-intent-tags.mjs +176 -0
  131. package/src/lib/image-intent.mjs +802 -0
  132. package/src/lib/image-use-overrides.mjs +279 -0
  133. package/src/lib/inline-components.mjs +613 -0
  134. package/src/lib/lint-converted.mjs +68 -4
  135. package/src/lib/merge-json.mjs +163 -0
  136. package/src/lib/overwrite-guard.mjs +3 -3
  137. package/src/lib/presentation-context.mjs +531 -0
  138. package/src/lib/preservation-tags.mjs +157 -0
  139. package/src/lib/preservation-verdict.mjs +1342 -0
  140. package/src/lib/publish.mjs +1557 -0
  141. package/src/lib/rebase.mjs +945 -0
  142. package/src/lib/reconcile-ack.mjs +4 -6
  143. package/src/lib/reconcile.mjs +62 -24
  144. package/src/lib/record-client.mjs +175 -0
  145. package/src/lib/record-state.mjs +151 -0
  146. package/src/lib/remote.mjs +661 -0
  147. package/src/lib/revision.mjs +1210 -0
  148. package/src/lib/slide-guids.mjs +4 -6
  149. package/src/lib/slide-preservation.mjs +423 -0
  150. package/src/lib/slide-selection.mjs +9 -0
  151. package/src/lib/workspace-transaction.mjs +117 -0
  152. package/src/pptx/content-digest.mjs +334 -0
  153. package/src/pptx/deck-assemble.mjs +1278 -0
  154. package/src/pptx/fill/engine.mjs +2 -3
  155. package/src/pptx/fill/freeform.mjs +6 -3
  156. package/src/pptx/ooxml.mjs +20 -0
  157. package/src/pptx/package-evidence.mjs +383 -0
  158. package/src/pptx/tags.mjs +163 -11
  159. package/src/update-check.mjs +22 -4
  160. package/src/workspace/adapters.mjs +222 -0
  161. package/src/workspace/auth.mjs +256 -0
  162. package/src/workspace/autostart.mjs +204 -0
  163. package/src/workspace/daemon.mjs +360 -0
  164. package/src/workspace/discovery.mjs +180 -0
  165. package/src/workspace/edit.mjs +167 -0
  166. package/src/workspace/events.mjs +52 -0
  167. package/src/workspace/gate-client.mjs +290 -0
  168. package/src/workspace/handback.mjs +73 -0
  169. package/src/workspace/lifecycle.mjs +635 -0
  170. package/src/workspace/operations.mjs +200 -0
  171. package/src/workspace/pick-folder.mjs +152 -0
  172. package/src/workspace/registry.mjs +359 -0
  173. package/src/workspace/remote-links.mjs +397 -0
  174. package/src/workspace/remote-status.mjs +359 -0
  175. package/src/workspace/routes.mjs +1331 -0
  176. package/src/workspace/server.mjs +159 -0
  177. package/src/workspace/sync.mjs +1119 -0
  178. package/src/workspace/task-actions.mjs +215 -0
  179. package/src/workspace/tasks.mjs +507 -0
  180. package/src/workspace/ui/app.js +1886 -0
  181. package/src/workspace/ui/index.html +141 -0
  182. package/src/workspace/ui/plan-bridge.js +120 -0
  183. package/src/workspace/ui/sharepoint.svg +59 -0
  184. package/src/workspace/ui/styles.css +423 -0
  185. package/src/builders/visual-families.mjs +0 -46
@@ -0,0 +1,47 @@
1
+ // Durable identity for the things an edit operation names.
2
+ //
3
+ // SLIDE identity already exists and is NOT re-minted here: `mintSlideGuid()`
4
+ // (`src/pptx/tags.mjs`) mints `sm-<uuid>` and `src/lib/slide-guids.mjs` keeps a
5
+ // deck's sidecar of them across renames/reorders. A `slideKey` in the operation
6
+ // and preservation records IS one of those GUIDs (or, for a deck that predates
7
+ // them, whatever durable key the workspace already carries) — never a position.
8
+ //
9
+ // ELEMENT identity had no minter. Three flavours exist in the product already:
10
+ // concept-spec slugs (`stage-1-card`), the converted dialect's OOXML
11
+ // `data-sm-source-id={20}`, and ids an agent writes by hand. All three are
12
+ // durable because something outside the JSX remembers them, so this module
13
+ // only has to (a) mint one for a node the agent authors fresh and (b) say what
14
+ // counts as an id at all. Matching an unlabelled node back to a requirement is
15
+ // deliberately NOT here: the guide asks for ambiguity to be surfaced, and that
16
+ // comparison belongs to the preservation verdict (phase 5), not to id minting.
17
+
18
+ import { randomUUID } from "node:crypto";
19
+
20
+ /** Prefix of a freshly minted element id, so its provenance is readable. */
21
+ export const ELEMENT_ID_PREFIX = "sm-el-";
22
+
23
+ const MAX_ID_LENGTH = 128;
24
+
25
+ /**
26
+ * An id for a new agent-authored node. 48 bits of the uuid — short enough to
27
+ * read in a JSX attribute, and uniqueness is checked per slide by the
28
+ * preservation validator rather than assumed globally.
29
+ */
30
+ export function mintElementId() {
31
+ return `${ELEMENT_ID_PREFIX}${randomUUID().replace(/-/g, "").slice(0, 12)}`;
32
+ }
33
+
34
+ /** Was this id minted here (as opposed to a spec slug or a source id)? */
35
+ export function isMintedElementId(id) {
36
+ return typeof id === "string" && new RegExp(`^${ELEMENT_ID_PREFIX}[0-9a-f]{12}$`).test(id);
37
+ }
38
+
39
+ /**
40
+ * Anything usable as a durable identity: a non-empty, whitespace-free, bounded
41
+ * string. Deliberately permissive — the three existing flavours must all pass,
42
+ * and a shape the CLI has never seen (a pack's own ids) must not be refused by
43
+ * a validator that only had to check that an id is an id.
44
+ */
45
+ export function isDurableId(id) {
46
+ return typeof id === "string" && id.length > 0 && id.length <= MAX_ID_LENGTH && !/\s/.test(id);
47
+ }
@@ -0,0 +1,316 @@
1
+ // The edit-operation record — what an agent is allowed to change on this run.
2
+ //
3
+ // Contract from `openspec/changes/safeguard-slide-editing/implementation-guide.md`
4
+ // section 1. Persisted at `<deck>/.slidemind/state/edit-operations.json`; the
5
+ // add-in keeps the same record in its workspace persistence. This module is the
6
+ // SHARED definition of the shape and the only place that decides whether one is
7
+ // valid — it is mirrored byte-for-byte into the add-in
8
+ // (`scripts/sync-transcription-runtime.mjs`), so the two clients cannot drift
9
+ // into disagreeing about what a caller just handed them.
10
+ //
11
+ // Everything here is pure: no disk, no mutation of the argument, and no throw.
12
+ // A malformed record is a returned error list, because the callers (phase 3.2 /
13
+ // 3.3) must be able to refuse a write while leaving the bytes on disk exactly
14
+ // as they found them.
15
+
16
+ import { isDurableId } from "./durable-ids.mjs";
17
+
18
+ /** Sidecar name under the workspace's `state/`. */
19
+ export const EDIT_OPERATIONS_FILE = "edit-operations.json";
20
+
21
+ /** The only schema version this build understands. */
22
+ export const EDIT_OPERATION_VERSION = 1;
23
+
24
+ /**
25
+ * What the operation is for. The manager derives it from the request and the
26
+ * selected slide ids; a worker cannot upgrade one kind into another by editing
27
+ * a pragma (that gate is task 3.4 — this module only refuses a record whose
28
+ * kind and payload disagree).
29
+ */
30
+ export const OPERATION_KINDS = ["create", "targeted-edit", "repair", "redesign"];
31
+
32
+ export const OPERATION_STATUSES = ["prepared", "validated", "committed", "conflicted", "aborted"];
33
+
34
+ /** Where the source being edited came from, and therefore what it proves. */
35
+ export const TARGET_PROVENANCE = ["live", "saved", "local"];
36
+
37
+ export const CHANGE_ACTIONS = ["add", "update", "remove"];
38
+
39
+ /**
40
+ * The property vocabulary shared by an operation's declared changes and a
41
+ * preservation requirement's protected set. These are the structural
42
+ * projection's own fields (`computeProjection`, src/pptx/tags.mjs) plus
43
+ * `native` for an opaque object's package dependency closure — so a scope name
44
+ * always points at something the render evidence can actually compare, and a
45
+ * scope nobody can verify cannot be written into a record in the first place.
46
+ */
47
+ export const PROPERTY_SCOPES = ["geom", "text", "fill", "line", "media", "chart", "native"];
48
+
49
+ const SHA256_RE = /^[0-9a-f]{64}$/;
50
+
51
+ // --- error collection -------------------------------------------------------
52
+
53
+ const err = (out, path, code, message) => out.push({ path, code, message });
54
+
55
+ const isPlainObject = (v) => Boolean(v) && typeof v === "object" && !Array.isArray(v);
56
+
57
+ const isNonEmptyString = (v) => typeof v === "string" && v.trim().length > 0;
58
+
59
+ function checkVersion(record, path, out) {
60
+ if (record.version === EDIT_OPERATION_VERSION) return true;
61
+ err(
62
+ out,
63
+ `${path}.version`,
64
+ "unsupported_version",
65
+ `schema version ${JSON.stringify(record.version ?? null)} is not supported; ` +
66
+ `this build reads version ${EDIT_OPERATION_VERSION}. Leave the record untouched ` +
67
+ `and upgrade slidemind (\`slidemind update\`) before editing this deck.`,
68
+ );
69
+ return false;
70
+ }
71
+
72
+ function checkEnum(value, allowed, path, code, out) {
73
+ if (allowed.includes(value)) return true;
74
+ err(out, path, code, `${JSON.stringify(value ?? null)} is not one of ${allowed.join(" | ")}`);
75
+ return false;
76
+ }
77
+
78
+ // --- one target -------------------------------------------------------------
79
+
80
+ function validateTarget(target, kind, path, seenSlideKeys, out) {
81
+ if (!isPlainObject(target)) {
82
+ err(out, path, "invalid_record", "a target must be an object");
83
+ return;
84
+ }
85
+
86
+ if (!isDurableId(target.slideKey)) {
87
+ err(out, `${path}.slideKey`, "missing_field", "slideKey must be a durable slide identity, never a position");
88
+ } else if (seenSlideKeys.has(target.slideKey)) {
89
+ err(
90
+ out,
91
+ `${path}.slideKey`,
92
+ "duplicate_slide_key",
93
+ `slideKey ${target.slideKey} is targeted twice by one operation; a slide has exactly one scope per operation`,
94
+ );
95
+ } else {
96
+ seenSlideKeys.add(target.slideKey);
97
+ }
98
+
99
+ if (!isNonEmptyString(target.jsxPath)) {
100
+ err(out, `${path}.jsxPath`, "missing_field", "jsxPath must name the slide's current source file");
101
+ }
102
+ if (target.liveSlideId != null && !isNonEmptyString(target.liveSlideId)) {
103
+ err(out, `${path}.liveSlideId`, "invalid_value", "liveSlideId, when present, must be the Office id string");
104
+ }
105
+
106
+ const provenanceOk = checkEnum(target.provenance, TARGET_PROVENANCE, `${path}.provenance`, "invalid_value", out);
107
+
108
+ // A base revision is what makes an edit safe to publish. `create` has no
109
+ // base by definition, so demanding one there would be nonsense; every other
110
+ // kind is editing something that already exists and must name it exactly.
111
+ if (kind === "create") {
112
+ if (target.baseSourceHash != null) {
113
+ err(
114
+ out,
115
+ `${path}.baseSourceHash`,
116
+ "invalid_value",
117
+ "a create target has no base source; use targeted-edit or redesign to change existing source",
118
+ );
119
+ }
120
+ if (provenanceOk && target.provenance !== "local") {
121
+ err(out, `${path}.provenance`, "invalid_value", "a create target is authored locally; provenance must be local");
122
+ }
123
+ } else if (!SHA256_RE.test(String(target.baseSourceHash ?? ""))) {
124
+ err(
125
+ out,
126
+ `${path}.baseSourceHash`,
127
+ "missing_base_revision",
128
+ "baseSourceHash (sha256 of the exact source prepared for editing) is required to edit existing source",
129
+ );
130
+ }
131
+
132
+ // Provenance is a claim about freshness, and each claim has its own proof.
133
+ // Saved bytes never prove equality with unsaved live content (E08), so the
134
+ // two are recorded separately rather than collapsed into one flag.
135
+ if (target.provenance === "live" && !isNonEmptyString(target.baseLiveFingerprint)) {
136
+ err(
137
+ out,
138
+ `${path}.baseLiveFingerprint`,
139
+ "missing_base_revision",
140
+ "live provenance claims the live slide was verified; record the fingerprint it was verified against",
141
+ );
142
+ }
143
+ if (target.provenance === "saved" && !isNonEmptyString(target.sourceRevision)) {
144
+ err(
145
+ out,
146
+ `${path}.sourceRevision`,
147
+ "missing_base_revision",
148
+ "saved provenance must name the saved-file revision it read",
149
+ );
150
+ }
151
+
152
+ if (target.repairPolicy != null) {
153
+ if (!isPlainObject(target.repairPolicy)) {
154
+ err(out, `${path}.repairPolicy`, "invalid_value", "repairPolicy must be an object");
155
+ }
156
+ if (kind !== "repair") {
157
+ err(
158
+ out,
159
+ `${path}.repairPolicy`,
160
+ "invalid_value",
161
+ `a repair budget only applies to a repair operation, not to ${kind}`,
162
+ );
163
+ }
164
+ }
165
+
166
+ if (!Array.isArray(target.changes)) {
167
+ err(out, `${path}.changes`, "missing_field", "changes must be an array (empty is allowed)");
168
+ return;
169
+ }
170
+ const seenElements = new Set();
171
+ target.changes.forEach((change, i) => validateChange(change, `${path}.changes[${i}]`, seenElements, out));
172
+ }
173
+
174
+ function validateChange(change, path, seenElements, out) {
175
+ if (!isPlainObject(change)) {
176
+ err(out, path, "invalid_record", "a change must be an object");
177
+ return;
178
+ }
179
+ if (!isDurableId(change.elementId)) {
180
+ err(out, `${path}.elementId`, "missing_field", "elementId must be a durable element identity");
181
+ } else if (seenElements.has(change.elementId)) {
182
+ err(
183
+ out,
184
+ `${path}.elementId`,
185
+ "duplicate_element_id",
186
+ `element ${change.elementId} is declared twice on one target; state the whole intent for it once`,
187
+ );
188
+ } else {
189
+ seenElements.add(change.elementId);
190
+ }
191
+
192
+ checkEnum(change.action, CHANGE_ACTIONS, `${path}.action`, "invalid_value", out);
193
+
194
+ if (!Array.isArray(change.properties)) {
195
+ err(out, `${path}.properties`, "missing_field", "properties must be an array of property scopes");
196
+ return;
197
+ }
198
+ change.properties.forEach((scope, i) => {
199
+ if (!PROPERTY_SCOPES.includes(scope)) {
200
+ err(
201
+ out,
202
+ `${path}.properties[${i}]`,
203
+ "invalid_property_scope",
204
+ `${JSON.stringify(scope ?? null)} is not a known property scope (${PROPERTY_SCOPES.join(", ")})`,
205
+ );
206
+ }
207
+ });
208
+ if (change.action === "update" && change.properties.length === 0) {
209
+ err(out, `${path}.properties`, "missing_field", "an update must name the properties it is allowed to change");
210
+ }
211
+ }
212
+
213
+ // --- public surface ---------------------------------------------------------
214
+
215
+ /**
216
+ * Validate one operation record.
217
+ *
218
+ * @param {unknown} record
219
+ * @returns {{ ok: boolean, errors: Array<{path: string, code: string, message: string}> }}
220
+ */
221
+ export function validateEditOperation(record, { path = "operation" } = {}) {
222
+ const errors = [];
223
+ if (!isPlainObject(record)) {
224
+ err(errors, path, "invalid_record", "an operation record must be an object");
225
+ return { ok: false, errors };
226
+ }
227
+ // A version we do not understand stops here: reporting field errors against
228
+ // a foreign schema would be noise, and the caller must preserve those bytes.
229
+ if (!checkVersion(record, path, errors)) return { ok: false, errors };
230
+
231
+ if (!isNonEmptyString(record.operationId)) {
232
+ err(errors, `${path}.operationId`, "missing_field", "operationId is required");
233
+ }
234
+ const kindOk = checkEnum(record.kind, OPERATION_KINDS, `${path}.kind`, "invalid_value", errors);
235
+ checkEnum(record.status, OPERATION_STATUSES, `${path}.status`, "invalid_value", errors);
236
+ if (!isNonEmptyString(record.requestSummary)) {
237
+ err(
238
+ errors,
239
+ `${path}.requestSummary`,
240
+ "missing_field",
241
+ "requestSummary is required: a concise trace of what the user asked for",
242
+ );
243
+ }
244
+
245
+ if (!Array.isArray(record.targets) || record.targets.length === 0) {
246
+ err(errors, `${path}.targets`, "missing_field", "targets must be a non-empty array");
247
+ return { ok: errors.length === 0, errors };
248
+ }
249
+ const seenSlideKeys = new Set();
250
+ const kind = kindOk ? record.kind : null;
251
+ record.targets.forEach((t, i) => validateTarget(t, kind, `${path}.targets[${i}]`, seenSlideKeys, errors));
252
+
253
+ return { ok: errors.length === 0, errors };
254
+ }
255
+
256
+ /**
257
+ * Validate the whole sidecar: `{ version, operations: [...] }`.
258
+ */
259
+ export function validateEditOperationsFile(doc, { path = "edit-operations" } = {}) {
260
+ const errors = [];
261
+ if (!isPlainObject(doc)) {
262
+ err(errors, path, "invalid_record", "the operations file must be an object");
263
+ return { ok: false, errors };
264
+ }
265
+ if (!checkVersion(doc, path, errors)) return { ok: false, errors };
266
+ if (!Array.isArray(doc.operations)) {
267
+ err(errors, `${path}.operations`, "missing_field", "operations must be an array");
268
+ return { ok: false, errors };
269
+ }
270
+ const seen = new Set();
271
+ doc.operations.forEach((op, i) => {
272
+ const at = `${path}.operations[${i}]`;
273
+ const id = isPlainObject(op) ? op.operationId : null;
274
+ if (isNonEmptyString(id)) {
275
+ if (seen.has(id)) {
276
+ err(errors, `${at}.operationId`, "duplicate_operation_id", `operationId ${id} appears twice`);
277
+ } else {
278
+ seen.add(id);
279
+ }
280
+ }
281
+ errors.push(...validateEditOperation(op, { path: at }).errors);
282
+ });
283
+ return { ok: errors.length === 0, errors };
284
+ }
285
+
286
+ /** The slide keys an operation is scoped to — what a preservation update may touch. */
287
+ export function operationSlideKeys(record) {
288
+ if (!isPlainObject(record) || !Array.isArray(record.targets)) return [];
289
+ return record.targets.filter(isPlainObject).map((t) => t.slideKey).filter(isDurableId);
290
+ }
291
+
292
+ /**
293
+ * A transition the governing operation's kind does not authorize, or null — the
294
+ * one rule both clients enforce: the add-in at every write, the CLI (whose agent
295
+ * writes files itself) before anything is rendered or delivered.
296
+ *
297
+ * out_of_scope a slide the scope does not name — another slide, or a
298
+ * `-2.jsx` sibling beside a target
299
+ * dialect_dropped an imported slide re-authored without its converted-dialect
300
+ * pragma by anything but a redesign
301
+ *
302
+ * `scoped` lists the slides the scope authorizes, in the caller's own identity
303
+ * (the add-in's jsx paths, the CLI's slide keys), and is null when nothing is
304
+ * scoped: no operation, or a `create`, which has no targets to stay inside.
305
+ * `kind` null reads as `targeted-edit` — a redesign is always declared, so no
306
+ * record means "not a redesign".
307
+ *
308
+ * @param {{ kind?: string | null, scoped?: string[] | null }} scope
309
+ * @param {{ slide: string, imported?: boolean, keepsPragma?: boolean }} change
310
+ * @returns {"out_of_scope" | "dialect_dropped" | null}
311
+ */
312
+ export function editTransitionViolation({ kind = null, scoped = null }, { slide, imported = false, keepsPragma = true }) {
313
+ if (Array.isArray(scoped) && !scoped.includes(slide)) return "out_of_scope";
314
+ if (imported && !keepsPragma && (kind ?? "targeted-edit") !== "redesign") return "dialect_dropped";
315
+ return null;
316
+ }