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.
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/AGENTS.md +591 -15
- package/README.md +202 -7
- package/bin/slidemind.mjs +50 -2
- package/package.json +2 -2
- package/skills/create-slides/SKILL.md +484 -886
- package/skills/search/SKILL.md +46 -23
- package/src/bridge/build-and-push.mjs +548 -73
- package/src/bridge/bundle.mjs +100 -31
- package/src/bridge/capture.mjs +14 -17
- package/src/bridge/client/brands.mjs +157 -18
- package/src/bridge/client/engine-routing.mjs +48 -13
- package/src/bridge/client/engine-search.mjs +24 -1
- package/src/bridge/client/render.mjs +27 -1
- package/src/bridge/client/transport.mjs +499 -23
- package/src/bridge/client.mjs +10 -0
- package/src/bridge/config.mjs +67 -6
- package/src/bridge/engine-download.mjs +0 -10
- package/src/bridge/graph-auth.mjs +55 -3
- package/src/bridge/image-intent-envelope.mjs +239 -0
- package/src/bridge/package-preview.mjs +67 -0
- package/src/bridge/sharepoint.mjs +914 -0
- package/src/builders/asset-fetch.mjs +203 -0
- package/src/builders/block-dividers.mjs +83 -0
- package/src/builders/build.mjs +171 -87
- package/src/builders/card-height.mjs +294 -0
- package/src/builders/chart-hole.mjs +183 -0
- package/src/builders/chart-jsx.mjs +410 -0
- package/src/builders/chart-labels.mjs +326 -0
- package/src/builders/concept-geometry.mjs +370 -0
- package/src/builders/concept-intent.mjs +200 -0
- package/src/builders/concept-layout.mjs +79 -0
- package/src/builders/concept-lines.mjs +509 -0
- package/src/builders/concept-lists.mjs +270 -0
- package/src/builders/concept-marks.mjs +84 -0
- package/src/builders/concept-relationships.mjs +12 -0
- package/src/builders/concept-spec.mjs +645 -0
- package/src/builders/concept-text.mjs +140 -0
- package/src/builders/concept-waves.mjs +57 -0
- package/src/builders/copy-fit.mjs +139 -0
- package/src/builders/design-tokens.mjs +347 -0
- package/src/builders/draft-check.mjs +552 -340
- package/src/builders/draft-lines.mjs +369 -0
- package/src/builders/edit-route.mjs +29 -0
- package/src/builders/figure-sketch.mjs +375 -0
- package/src/builders/fold-sets.mjs +639 -0
- package/src/builders/gate-server.mjs +9 -4
- package/src/builders/gate-session.js +273 -12
- package/src/builders/gate-shell.js +1494 -0
- package/src/builders/image-intent-check.mjs +308 -0
- package/src/builders/image-use-reconcile.mjs +107 -0
- package/src/builders/kit-helpers.mjs +121 -0
- package/src/builders/layout-names.mjs +32 -0
- package/src/builders/legacy-reference-uses.mjs +179 -0
- package/src/builders/line-fit.mjs +224 -0
- package/src/builders/line-native.mjs +895 -0
- package/src/builders/line-relations.mjs +230 -0
- package/src/builders/lint-fix.mjs +117 -9
- package/src/builders/lint.mjs +25 -20
- package/src/builders/list-jsx.mjs +88 -0
- package/src/builders/placement.mjs +133 -0
- package/src/builders/plan-checks.mjs +135 -0
- package/src/builders/plan-comments.mjs +19 -0
- package/src/builders/plan-handlers.mjs +95 -0
- package/src/builders/plan-markdown-edit.mjs +3 -3
- package/src/builders/plan-overlay.js +1530 -1608
- package/src/builders/plan.mjs +689 -186
- package/src/builders/preset-definitions.json +1 -0
- package/src/builders/preset-eval.mjs +244 -0
- package/src/builders/preview.html +51 -120
- package/src/builders/red-thread-backbone.mjs +135 -36
- package/src/builders/repair.mjs +205 -0
- package/src/builders/repeat-groups.mjs +540 -0
- package/src/builders/resolve-skill.mjs +5 -4
- package/src/builders/review-overlay.js +296 -293
- package/src/builders/review.mjs +305 -84
- package/src/builders/rule-shift.mjs +96 -0
- package/src/builders/seat.mjs +354 -0
- package/src/builders/settle-driver.mjs +236 -0
- package/src/builders/settle.mjs +1448 -0
- package/src/builders/slide-prep.mjs +67 -0
- package/src/builders/spec-assets.mjs +236 -0
- package/src/builders/spec-match.mjs +56 -0
- package/src/builders/stack.mjs +413 -0
- package/src/builders/storyboard-markdown.mjs +70 -27
- package/src/builders/storyboard-schema.mjs +54 -114
- package/src/builders/text-blocks.mjs +212 -0
- package/src/builders/text-card-fit.mjs +552 -0
- package/src/builders/text-stack.mjs +276 -0
- package/src/builders/transcribe.mjs +1069 -0
- package/src/cli/assets.mjs +164 -0
- package/src/cli/common.mjs +173 -12
- package/src/cli/concept-spec-support.mjs +76 -0
- package/src/cli/concepts.mjs +1002 -103
- package/src/cli/deck.mjs +401 -65
- package/src/cli/diagnose.mjs +4 -2
- package/src/cli/engine-helpers.mjs +17 -10
- package/src/cli/engine-results.mjs +76 -3
- package/src/cli/help.mjs +232 -28
- package/src/cli/install.mjs +9 -0
- package/src/cli/maintenance.mjs +104 -0
- package/src/cli/read.mjs +2 -1
- package/src/cli/ready.mjs +6 -3
- package/src/cli/record.mjs +522 -0
- package/src/cli/remote.mjs +818 -0
- package/src/cli/revision.mjs +686 -0
- package/src/cli/search.mjs +30 -11
- package/src/cli/session.mjs +56 -13
- package/src/cli/setup.mjs +46 -2
- package/src/cli/status.mjs +142 -33
- package/src/cli/tasks.mjs +246 -0
- package/src/cli/transcribe.mjs +854 -0
- package/src/cli/workspace-ui.mjs +260 -0
- package/src/cli/workspace.mjs +212 -34
- package/src/install-channel.mjs +64 -7
- package/src/lib/adopt.mjs +346 -25
- package/src/lib/concept-spec-binding.mjs +96 -0
- package/src/lib/conversions.mjs +70 -6
- package/src/lib/convert-folded.mjs +107 -0
- package/src/lib/deck-paths.mjs +418 -0
- package/src/lib/durable-ids.mjs +47 -0
- package/src/lib/edit-operations.mjs +316 -0
- package/src/lib/edit-state.mjs +1244 -0
- package/src/lib/edit-transitions.mjs +120 -0
- package/src/lib/extensions.mjs +501 -0
- package/src/lib/image-assets.mjs +465 -0
- package/src/lib/image-derivatives.mjs +234 -0
- package/src/lib/image-intent-fingerprint.mjs +70 -0
- package/src/lib/image-intent-tags.mjs +176 -0
- package/src/lib/image-intent.mjs +802 -0
- package/src/lib/image-use-overrides.mjs +279 -0
- package/src/lib/inline-components.mjs +613 -0
- package/src/lib/lint-converted.mjs +68 -4
- package/src/lib/merge-json.mjs +163 -0
- package/src/lib/overwrite-guard.mjs +3 -3
- package/src/lib/presentation-context.mjs +531 -0
- package/src/lib/preservation-tags.mjs +157 -0
- package/src/lib/preservation-verdict.mjs +1342 -0
- package/src/lib/publish.mjs +1557 -0
- package/src/lib/rebase.mjs +945 -0
- package/src/lib/reconcile-ack.mjs +4 -6
- package/src/lib/reconcile.mjs +62 -24
- package/src/lib/record-client.mjs +175 -0
- package/src/lib/record-state.mjs +151 -0
- package/src/lib/remote.mjs +661 -0
- package/src/lib/revision.mjs +1210 -0
- package/src/lib/slide-guids.mjs +4 -6
- package/src/lib/slide-preservation.mjs +423 -0
- package/src/lib/slide-selection.mjs +9 -0
- package/src/lib/workspace-transaction.mjs +117 -0
- package/src/pptx/content-digest.mjs +334 -0
- package/src/pptx/deck-assemble.mjs +1278 -0
- package/src/pptx/fill/engine.mjs +2 -3
- package/src/pptx/fill/freeform.mjs +6 -3
- package/src/pptx/ooxml.mjs +20 -0
- package/src/pptx/package-evidence.mjs +383 -0
- package/src/pptx/tags.mjs +163 -11
- package/src/update-check.mjs +22 -4
- package/src/workspace/adapters.mjs +222 -0
- package/src/workspace/auth.mjs +256 -0
- package/src/workspace/autostart.mjs +204 -0
- package/src/workspace/daemon.mjs +360 -0
- package/src/workspace/discovery.mjs +180 -0
- package/src/workspace/edit.mjs +167 -0
- package/src/workspace/events.mjs +52 -0
- package/src/workspace/gate-client.mjs +290 -0
- package/src/workspace/handback.mjs +73 -0
- package/src/workspace/lifecycle.mjs +635 -0
- package/src/workspace/operations.mjs +200 -0
- package/src/workspace/pick-folder.mjs +152 -0
- package/src/workspace/registry.mjs +359 -0
- package/src/workspace/remote-links.mjs +397 -0
- package/src/workspace/remote-status.mjs +359 -0
- package/src/workspace/routes.mjs +1331 -0
- package/src/workspace/server.mjs +159 -0
- package/src/workspace/sync.mjs +1119 -0
- package/src/workspace/task-actions.mjs +215 -0
- package/src/workspace/tasks.mjs +507 -0
- package/src/workspace/ui/app.js +1886 -0
- package/src/workspace/ui/index.html +141 -0
- package/src/workspace/ui/plan-bridge.js +120 -0
- package/src/workspace/ui/sharepoint.svg +59 -0
- package/src/workspace/ui/styles.css +423 -0
- 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
|
+
}
|