slidemind 0.3.19 → 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/engine-slides.mjs +1 -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
|
@@ -11,12 +11,13 @@
|
|
|
11
11
|
*
|
|
12
12
|
* Pure string logic — no React, no Office (unit-testable).
|
|
13
13
|
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
14
|
+
* Canonical here; the add-in imports a byte-identical mirror
|
|
15
|
+
* (scripts/sync-transcription-runtime.mjs). Top-level helpers are prefixed
|
|
16
|
+
* `rt…` so the browser bundle (one IIFE across five modules) has no name
|
|
17
|
+
* collisions.
|
|
17
18
|
*/
|
|
18
19
|
|
|
19
|
-
import { extractRedThreadMessages, findRedThreadSection } from "./storyboard-markdown.mjs";
|
|
20
|
+
import { extractFrameLine, extractRedThreadMessages, findRedThreadSection, replaceStoryboardSegment, splitPlanMarkdownIntoSegments } from "./storyboard-markdown.mjs";
|
|
20
21
|
|
|
21
22
|
/**
|
|
22
23
|
* One slide in the user-edited backbone draft:
|
|
@@ -54,19 +55,44 @@ function rtFindSlideSections(markdown) {
|
|
|
54
55
|
}
|
|
55
56
|
|
|
56
57
|
/**
|
|
57
|
-
*
|
|
58
|
+
* A frame slide is a section but not a move: the red thread lists body moves
|
|
59
|
+
* only and names the frame on its `Frame:` line. Returns a frame slide's card
|
|
60
|
+
* label, or null for a body slide (an unreadable record counts as body).
|
|
61
|
+
*/
|
|
62
|
+
function rtFrameLabel(sectionText) {
|
|
63
|
+
const board = splitPlanMarkdownIntoSegments(sectionText).find(
|
|
64
|
+
(segment) => segment.type === "storyboard",
|
|
65
|
+
);
|
|
66
|
+
const data = board?.data;
|
|
67
|
+
if (data?.role !== "frame") return null;
|
|
68
|
+
const heading = /^###\s+Slide\s+\d+\s*:?[ \t]*(.*)$/m.exec(sectionText)?.[1];
|
|
69
|
+
return data.keyMessage?.trim() || heading?.trim() || data.frameKind || "Frame";
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* The backbone is editable only when the red-thread moves and the BODY slide
|
|
58
74
|
* sections line up one-to-one — otherwise a positional restructure could
|
|
59
|
-
* attach the wrong storyboard to a move.
|
|
75
|
+
* attach the wrong storyboard to a move. Frame slides sit between them in play
|
|
76
|
+
* order; a whole presentation always has some, so counting them as moves left
|
|
77
|
+
* every presentation's storyline read-only.
|
|
60
78
|
*
|
|
61
|
-
* Returns { messages, sectionCount } or null
|
|
79
|
+
* Returns { messages, sectionCount, frame } or null: `messages` holds one card
|
|
80
|
+
* label per section (a body slide's move, a frame slide's key message), `frame`
|
|
81
|
+
* marks the frame sections.
|
|
62
82
|
*/
|
|
63
83
|
export function parsePlanBackbone(markdown) {
|
|
64
|
-
const
|
|
65
|
-
if (!
|
|
84
|
+
const moves = extractRedThreadMessages(markdown);
|
|
85
|
+
if (!moves.length) return null;
|
|
66
86
|
if (!findRedThreadSection(markdown)) return null;
|
|
67
87
|
const sections = rtFindSlideSections(markdown);
|
|
68
|
-
|
|
69
|
-
|
|
88
|
+
const labels = sections.map((section) => rtFrameLabel(section.text));
|
|
89
|
+
if (labels.filter((label) => label === null).length !== moves.length) return null;
|
|
90
|
+
let move = 0;
|
|
91
|
+
return {
|
|
92
|
+
messages: labels.map((label) => label ?? moves[move++] ?? ""),
|
|
93
|
+
sectionCount: sections.length,
|
|
94
|
+
frame: labels.map((label) => label !== null),
|
|
95
|
+
};
|
|
70
96
|
}
|
|
71
97
|
|
|
72
98
|
function rtSanitizeMessage(message) {
|
|
@@ -84,34 +110,87 @@ function rtRenumberSection(text, slideNumber, editedMessage) {
|
|
|
84
110
|
return next.replace(/("slide"\s*:\s*)\d+/, `$1${slideNumber}`);
|
|
85
111
|
}
|
|
86
112
|
|
|
87
|
-
|
|
113
|
+
// ---------------------------------------------------------------------------
|
|
114
|
+
// image uses across a restructure
|
|
115
|
+
// ---------------------------------------------------------------------------
|
|
116
|
+
//
|
|
117
|
+
// Reordering and deletion need no help: a slide's `assetUses` live inside its
|
|
118
|
+
// own storyboard fence, so they move with the section text and vanish with it,
|
|
119
|
+
// and `useId`s stay exactly as they were (design decision 8 — never persist a
|
|
120
|
+
// use's ownership as a slide number).
|
|
121
|
+
//
|
|
122
|
+
// DUPLICATION does: the copy would carry the original's `useId`s, and a use ID
|
|
123
|
+
// is the identity the spec binding, the render evidence and the targeted-edit
|
|
124
|
+
// override all key on. Two slides claiming one ID is not a picture used twice,
|
|
125
|
+
// it is one obligation the renderer cannot attribute to a slide. So the first
|
|
126
|
+
// occurrence keeps its IDs and every later copy mints fresh ones (A12).
|
|
127
|
+
|
|
128
|
+
// Every useId the plan already spends, so a minted one can't collide. Scanned
|
|
129
|
+
// off the RAW markdown rather than the parsed records: a fence that fails to
|
|
130
|
+
// parse still spends its IDs the moment someone fixes it, and a taken set that
|
|
131
|
+
// is too large only ever costs a suffix. Matches the add-in's scan verbatim.
|
|
132
|
+
const RT_USE_ID_RE = /"useId"\s*:\s*"([A-Za-z0-9_-]+)"/g;
|
|
133
|
+
|
|
134
|
+
function rtUseIdsIn(markdown) {
|
|
135
|
+
const taken = new Set();
|
|
136
|
+
for (const match of markdown.matchAll(RT_USE_ID_RE)) taken.add(match[1]);
|
|
137
|
+
return taken;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// The contract caps an ID at 128 characters, so the stem is trimmed before the
|
|
141
|
+
// `_c<n>` suffix is appended — an over-long mint would fail the contract's own
|
|
142
|
+
// parser and take the whole duplicated record down with it. The trim is PER
|
|
143
|
+
// CANDIDATE, because the suffix grows with n: a fixed 124-character stem is
|
|
144
|
+
// right for `_c2` and four characters too long for `_c1000`. Must stay
|
|
145
|
+
// byte-identical to the add-in's `mintUseId`
|
|
146
|
+
// (src/ui/src/pages/plan-review/red-thread-backbone.ts).
|
|
147
|
+
const RT_USE_ID_MAX = 128;
|
|
148
|
+
|
|
149
|
+
function rtMintUseId(base, n) {
|
|
150
|
+
const suffix = `_c${n}`;
|
|
151
|
+
return `${base.slice(0, RT_USE_ID_MAX - suffix.length)}${suffix}`;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function rtFreshUseId(base, taken) {
|
|
155
|
+
for (let n = 2; ; n++) {
|
|
156
|
+
const id = rtMintUseId(base, n);
|
|
157
|
+
if (taken.has(id)) continue;
|
|
158
|
+
taken.add(id);
|
|
159
|
+
return id;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Rewrite one section's image uses with fresh IDs. A section whose record
|
|
165
|
+
* carries none is returned byte-identical — a restructure of a deck with no
|
|
166
|
+
* image intent must not reformat a single fence.
|
|
167
|
+
*/
|
|
168
|
+
function rtRemintAssetUses(sectionText, taken) {
|
|
169
|
+
const board = splitPlanMarkdownIntoSegments(sectionText).find(
|
|
170
|
+
(segment) => segment.type === "storyboard" && segment.data?.assetUses?.length,
|
|
171
|
+
);
|
|
172
|
+
if (!board) return sectionText;
|
|
173
|
+
return replaceStoryboardSegment(sectionText, board, {
|
|
174
|
+
...board.data,
|
|
175
|
+
assetUses: board.data.assetUses.map((use) => ({
|
|
176
|
+
...use,
|
|
177
|
+
useId: rtFreshUseId(use.useId, taken),
|
|
178
|
+
})),
|
|
179
|
+
});
|
|
180
|
+
}
|
|
88
181
|
|
|
89
182
|
function rtPlaceholderSection(message, slideNumber) {
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
slide: slideNumber,
|
|
95
|
-
blocks: [
|
|
96
|
-
{
|
|
97
|
-
id: "block-1",
|
|
98
|
-
x: 80,
|
|
99
|
-
y: 110,
|
|
100
|
-
w: 1440,
|
|
101
|
-
h: 300,
|
|
102
|
-
designElement: "Open canvas",
|
|
103
|
-
content,
|
|
104
|
-
},
|
|
105
|
-
],
|
|
106
|
-
keyMessage: message,
|
|
107
|
-
};
|
|
183
|
+
// A slide the user inserted, with no content yet: the record carries the move
|
|
184
|
+
// as its story beat and nothing else. The agent fills in the content points
|
|
185
|
+
// and the layout on the round the composer sends.
|
|
186
|
+
const record = { version: 1, slide: slideNumber, keyMessage: message };
|
|
108
187
|
return [
|
|
109
188
|
`### Slide ${slideNumber}: ${message}`,
|
|
110
189
|
"",
|
|
111
190
|
`Core message: ${message}`,
|
|
112
191
|
"",
|
|
113
192
|
"```storyboard-canvas",
|
|
114
|
-
JSON.stringify(
|
|
193
|
+
JSON.stringify(record, null, 2),
|
|
115
194
|
"```",
|
|
116
195
|
].join("\n");
|
|
117
196
|
}
|
|
@@ -127,14 +206,19 @@ function rtPlaceholderSection(message, slideNumber) {
|
|
|
127
206
|
export function stageStoryIteration(markdown, slides) {
|
|
128
207
|
const editedPlanMarkdown = applyPlanBackbone(markdown, slides);
|
|
129
208
|
if (!editedPlanMarkdown) return null;
|
|
130
|
-
const
|
|
209
|
+
const backbone = parsePlanBackbone(markdown);
|
|
210
|
+
const committed = backbone?.messages ?? [];
|
|
131
211
|
const changes = [];
|
|
132
212
|
slides.forEach((slide, index) => {
|
|
133
213
|
const position = index + 1;
|
|
134
214
|
if (slide.originalIndex === null) {
|
|
135
215
|
changes.push(`Added slide ${position} for the move "${slide.message}".`);
|
|
136
216
|
} else if (committed[slide.originalIndex] !== slide.message) {
|
|
137
|
-
changes.push(
|
|
217
|
+
changes.push(
|
|
218
|
+
backbone?.frame[slide.originalIndex]
|
|
219
|
+
? `Renamed slide ${position} to "${slide.message}".`
|
|
220
|
+
: `Renamed slide ${position}'s move to "${slide.message}".`,
|
|
221
|
+
);
|
|
138
222
|
}
|
|
139
223
|
});
|
|
140
224
|
if (!changes.length) return null;
|
|
@@ -166,10 +250,21 @@ export function applyPlanBackbone(markdown, slides) {
|
|
|
166
250
|
if (!firstSection || !lastSection) return null;
|
|
167
251
|
if (firstSection.start < redThread.bodyEnd) return null;
|
|
168
252
|
|
|
169
|
-
|
|
253
|
+
// Frame slides keep their place among the sections but are not moves.
|
|
254
|
+
const moves = cleaned.filter(
|
|
255
|
+
(slide) => slide.originalIndex === null || !backbone.frame[slide.originalIndex],
|
|
256
|
+
);
|
|
257
|
+
if (!moves.length) return null;
|
|
258
|
+
// The `Frame:` declaration is not a move, so it is not in `cleaned` — and
|
|
259
|
+
// rewriting the body without it would silently drop the deck's frame from an
|
|
260
|
+
// approved plan on the first reorder.
|
|
261
|
+
const frameLine = extractFrameLine(markdown);
|
|
262
|
+
const newRedThreadBody = `\n\n${moves
|
|
170
263
|
.map((slide) => `- ${slide.message}`)
|
|
171
|
-
.join("\n")}\n\n`;
|
|
264
|
+
.join("\n")}${frameLine ? `\n\n${frameLine}` : ""}\n\n`;
|
|
172
265
|
|
|
266
|
+
const takenUseIds = rtUseIdsIn(markdown);
|
|
267
|
+
const placed = new Set();
|
|
173
268
|
const newSections = cleaned.map((slide, index) => {
|
|
174
269
|
const slideNumber = index + 1;
|
|
175
270
|
if (slide.originalIndex === null) {
|
|
@@ -180,7 +275,11 @@ export function applyPlanBackbone(markdown, slides) {
|
|
|
180
275
|
const originalMessage = backbone.messages[slide.originalIndex] ?? "";
|
|
181
276
|
const edited =
|
|
182
277
|
rtSanitizeMessage(originalMessage) !== slide.message ? slide.message : null;
|
|
183
|
-
|
|
278
|
+
const text = rtRenumberSection(section.text.trimEnd(), slideNumber, edited);
|
|
279
|
+
// Second and later appearances of one original section are copies.
|
|
280
|
+
if (placed.has(slide.originalIndex)) return rtRemintAssetUses(text, takenUseIds);
|
|
281
|
+
placed.add(slide.originalIndex);
|
|
282
|
+
return text;
|
|
184
283
|
});
|
|
185
284
|
|
|
186
285
|
return [
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
// Bounded, transactional repair of transcribed slides
|
|
2
|
+
// (openspec/changes/safeguard-slide-editing 6.3).
|
|
3
|
+
//
|
|
4
|
+
// The engine itself is pure and lives in the mirrored core (settle.mjs:
|
|
5
|
+
// `validateSettleTarget`, `repairPasses`, `REPAIR_BUDGET`). This module is the
|
|
6
|
+
// CLI half that gives it a place to work and a way to publish: a CANDIDATE
|
|
7
|
+
// workspace, and one atomic commit at the end. The two are separate files
|
|
8
|
+
// because settle.mjs ships byte-identical into the add-in runtime
|
|
9
|
+
// (scripts/sync-transcription-runtime.mjs), where none of the deck state below
|
|
10
|
+
// exists.
|
|
11
|
+
//
|
|
12
|
+
// The order is the whole point:
|
|
13
|
+
//
|
|
14
|
+
// 1. the operation is a `repair`, and its source is still the bytes it was
|
|
15
|
+
// prepared against (`assertOperationFresh`);
|
|
16
|
+
// 2. every target's CURRENT representation is one the core may rewrite
|
|
17
|
+
// (`validateSettleTarget`) — before a candidate is even staged;
|
|
18
|
+
// 3. all passes run in the candidate, rendered there, budget checked against
|
|
19
|
+
// the ORIGINAL after every pass;
|
|
20
|
+
// 4. the last mutation is rendered before anything is published;
|
|
21
|
+
// 5. compare-and-swap, then one atomic activation of source + assets +
|
|
22
|
+
// records together.
|
|
23
|
+
//
|
|
24
|
+
// Anything that fails discards the candidate. Nothing is restored, because
|
|
25
|
+
// nothing was published.
|
|
26
|
+
import fs from "node:fs";
|
|
27
|
+
import path from "node:path";
|
|
28
|
+
|
|
29
|
+
import { conversionProvenance } from "../lib/conversions.mjs";
|
|
30
|
+
import { assertOperationFresh, readSlidePreservation, recordOperation, sourceHash, writeSlidePreservation } from "../lib/edit-state.mjs";
|
|
31
|
+
import { inlineComponents } from "../lib/inline-components.mjs";
|
|
32
|
+
import { compareSlideSource } from "../lib/preservation-verdict.mjs";
|
|
33
|
+
import { stageWorkspace } from "../lib/workspace-transaction.mjs";
|
|
34
|
+
import { refoldLike } from "./fold-sets.mjs";
|
|
35
|
+
import { repairPasses, repairPolicyFor, repairDelta, validateSettleTarget } from "./settle.mjs";
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* One slide's result, in the shape both adapters report and the preservation
|
|
39
|
+
* verdict composes with (safeguard-slide-editing 6.5).
|
|
40
|
+
*
|
|
41
|
+
* `elements` is `compareSlideSource`'s own output — changed elements keyed to
|
|
42
|
+
* the operation record's PROPERTY_SCOPES (geom | text | fill | line | media |
|
|
43
|
+
* chart | native), so a repair's summary and a preservation requirement speak
|
|
44
|
+
* one vocabulary rather than two. `delta` carries the raw numbers the budget
|
|
45
|
+
* was read from; `changes` and `routeChanges` are the engine's own sentences.
|
|
46
|
+
*
|
|
47
|
+
* @typedef {{
|
|
48
|
+
* slideKey: string, jsxPath: string,
|
|
49
|
+
* verdict: "repaired" | "unchanged" | "refused",
|
|
50
|
+
* changes: string[], routeChanges: string[], unresolved: string[],
|
|
51
|
+
* elements: Array<{elementId: string, action: string, properties: string[]}>,
|
|
52
|
+
* unknown: Array<object>, unsupported: Array<object>, ambiguous: Array<object>,
|
|
53
|
+
* delta: object,
|
|
54
|
+
* }} RepairSlideSummary
|
|
55
|
+
*/
|
|
56
|
+
function summarise(slides, verdictOf) {
|
|
57
|
+
return slides.map((s) => {
|
|
58
|
+
const comparison = compareSlideSource(s.original, s.src);
|
|
59
|
+
return {
|
|
60
|
+
slideKey: s.key,
|
|
61
|
+
jsxPath: s.path,
|
|
62
|
+
verdict: verdictOf(s),
|
|
63
|
+
changes: s.changes,
|
|
64
|
+
routeChanges: s.routeChanges,
|
|
65
|
+
unresolved: s.unresolved,
|
|
66
|
+
elements: comparison.changes,
|
|
67
|
+
unknown: comparison.unknown,
|
|
68
|
+
unsupported: comparison.unsupported,
|
|
69
|
+
ambiguous: comparison.ambiguous,
|
|
70
|
+
delta: repairDelta(s.original, s.src),
|
|
71
|
+
};
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const refusal = (code, message, extra = {}) => ({ ok: false, committed: false, passes: 0, verified: false, slides: [], reasons: [{ code, message, ...extra }] });
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Repair the slides one `repair` operation scopes.
|
|
79
|
+
*
|
|
80
|
+
* @param {string} workspaceDir the deck workspace (`<deck>/.slidemind`, or the
|
|
81
|
+
* deck itself for a workspace-less deck)
|
|
82
|
+
* @param {object} options
|
|
83
|
+
* @param {object} options.operation the operation record (kind `repair`)
|
|
84
|
+
* @param {(ctx: {workspaceDir: string, files: string[]}) => Promise<Map<string, Map>>} options.capture
|
|
85
|
+
* render the CANDIDATE and return each file's measured boxes
|
|
86
|
+
* @param {() => boolean} [options.cancelled] checked before every pass
|
|
87
|
+
* @param {() => void} [options.checkpoint] runs inside the commit, before the swap
|
|
88
|
+
* @param {(rel: string) => void} [options.fault] runs after each activated file
|
|
89
|
+
* @returns {Promise<{ok: boolean, committed: boolean, passes: number, verified: boolean,
|
|
90
|
+
* slides: Array<object>, reasons: Array<{code: string, message: string}>}>}
|
|
91
|
+
*/
|
|
92
|
+
export async function repairOperation(workspaceDir, { operation, capture, cancelled = () => false, checkpoint = () => {}, fault = () => {} }) {
|
|
93
|
+
if (operation?.kind !== "repair") {
|
|
94
|
+
return refusal("not_a_repair", `bounded repair runs under a repair operation; this one is ${operation?.kind ?? "absent"}`);
|
|
95
|
+
}
|
|
96
|
+
const targets = operation.targets ?? [];
|
|
97
|
+
if (!targets.length) return refusal("no_targets", "this operation scopes no slides");
|
|
98
|
+
|
|
99
|
+
try {
|
|
100
|
+
assertOperationFresh(workspaceDir, operation);
|
|
101
|
+
} catch (err) {
|
|
102
|
+
return refusal(err.code ?? "source_moved", err.message);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// 2. The representation gate, on every target, before a candidate exists.
|
|
106
|
+
const slides = [];
|
|
107
|
+
const reasons = [];
|
|
108
|
+
for (const target of targets) {
|
|
109
|
+
const file = path.join(workspaceDir, target.jsxPath);
|
|
110
|
+
let original;
|
|
111
|
+
try {
|
|
112
|
+
original = fs.readFileSync(file, "utf8");
|
|
113
|
+
} catch (err) {
|
|
114
|
+
reasons.push({ code: "source_missing", slide: target.slideKey, message: `${target.jsxPath} cannot be read (${err.message})` });
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
// Three states, not two: a record that exists and cannot be read is
|
|
118
|
+
// `undefined` — unknown, which the validator refuses — and never `null`,
|
|
119
|
+
// which would say "looked, not converted" about a slide nobody could
|
|
120
|
+
// classify. The byte marker catches a converted slide that still carries
|
|
121
|
+
// its pragma; this is what catches one whose pragma was removed (E15).
|
|
122
|
+
const conversion = conversionProvenance(workspaceDir, target.jsxPath);
|
|
123
|
+
if (conversion === undefined) {
|
|
124
|
+
reasons.push({
|
|
125
|
+
code: "conversion_unreadable",
|
|
126
|
+
slide: target.slideKey,
|
|
127
|
+
jsxPath: target.jsxPath,
|
|
128
|
+
message: `conversions/${path.basename(target.jsxPath).replace(/\.jsx$/i, "")}.json exists but cannot be read, so whether ${target.jsxPath} is converted is unknown; repair or remove that record first`,
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
// A set component is repaired in its members' own lines: the core reads
|
|
132
|
+
// and rewrites one literal element per line, so each call site is inlined
|
|
133
|
+
// first and the result folded back afterwards (fold-sets.mjs refoldLike).
|
|
134
|
+
const flat = inlineComponents(original, { preserveLines: false });
|
|
135
|
+
for (const u of flat.unreadable) {
|
|
136
|
+
reasons.push({ code: "unreadable_component", slide: target.slideKey, jsxPath: target.jsxPath, line: u.line, message: `<${u.component}> is spread from an object, so bounded repair cannot read its members — edit the JSX directly` });
|
|
137
|
+
}
|
|
138
|
+
const check = validateSettleTarget(flat.src, { conversion });
|
|
139
|
+
for (const reason of check.reasons) reasons.push({ ...reason, slide: target.slideKey, jsxPath: target.jsxPath });
|
|
140
|
+
const { policy, errors } = repairPolicyFor(target);
|
|
141
|
+
for (const error of errors) reasons.push({ ...error, slide: target.slideKey });
|
|
142
|
+
slides.push({ key: target.slideKey, path: target.jsxPath, original: flat.src, raw: original, policy });
|
|
143
|
+
}
|
|
144
|
+
if (reasons.length) return { ok: false, committed: false, passes: 0, verified: false, slides: [], reasons };
|
|
145
|
+
|
|
146
|
+
// 3–4. The candidate. Every write below lands in the staged copy.
|
|
147
|
+
let candidate;
|
|
148
|
+
try {
|
|
149
|
+
candidate = stageWorkspace(workspaceDir);
|
|
150
|
+
} catch (err) {
|
|
151
|
+
return refusal(err.code ?? "stage_failed", err.message);
|
|
152
|
+
}
|
|
153
|
+
try {
|
|
154
|
+
const measured = new Map();
|
|
155
|
+
const result = await repairPasses({
|
|
156
|
+
slides,
|
|
157
|
+
cancelled,
|
|
158
|
+
render: async (sources) => {
|
|
159
|
+
for (const [rel, src] of sources) fs.writeFileSync(path.join(candidate.workspace, rel), src);
|
|
160
|
+
const captured = await capture({ workspaceDir: candidate.workspace, files: [...sources.keys()] });
|
|
161
|
+
for (const [rel, boxes] of captured ?? []) measured.set(rel, boxes);
|
|
162
|
+
return captured;
|
|
163
|
+
},
|
|
164
|
+
});
|
|
165
|
+
// An untouched slide keeps its own bytes; a repaired one goes back to its
|
|
166
|
+
// components where that is still exact.
|
|
167
|
+
const published = (s) => (s.src === s.original ? s.raw : refoldLike(s.raw, s.src, measured.get(s.path) ?? null).src);
|
|
168
|
+
if (!result.ok) {
|
|
169
|
+
return { ok: false, committed: false, passes: result.passes, verified: result.verified, slides: summarise(result.slides, () => "refused"), reasons: result.reasons };
|
|
170
|
+
}
|
|
171
|
+
const summary = summarise(result.slides, (s) => (s.src === s.original ? "unchanged" : "repaired"));
|
|
172
|
+
|
|
173
|
+
// 5. Publish: the repaired source, then the records that describe it, then
|
|
174
|
+
// one activation. `assertOperationFresh` names the slide that moved;
|
|
175
|
+
// `activate` refuses on ANY file that changed since staging. Every step
|
|
176
|
+
// here writes into the candidate only, so a throw anywhere leaves the deck
|
|
177
|
+
// exactly as it was — and comes back as a reason, not as an exception.
|
|
178
|
+
try {
|
|
179
|
+
for (const s of result.slides) fs.writeFileSync(path.join(candidate.workspace, s.path), published(s));
|
|
180
|
+
const preservation = readSlidePreservation(candidate.workspace);
|
|
181
|
+
let touched = false;
|
|
182
|
+
for (const s of result.slides) {
|
|
183
|
+
const entry = preservation.slides?.[s.key];
|
|
184
|
+
if (!entry?.baseline) continue;
|
|
185
|
+
// Requirements do not move; the source they were read from does.
|
|
186
|
+
entry.baseline = { ...entry.baseline, sourceHash: sourceHash(candidate.workspace, s.path) };
|
|
187
|
+
touched = true;
|
|
188
|
+
}
|
|
189
|
+
if (touched) writeSlidePreservation(candidate.workspace, preservation, { operation });
|
|
190
|
+
recordOperation(candidate.workspace, { ...operation, status: "committed" });
|
|
191
|
+
candidate.activate({
|
|
192
|
+
checkpoint: () => {
|
|
193
|
+
checkpoint();
|
|
194
|
+
assertOperationFresh(workspaceDir, operation);
|
|
195
|
+
},
|
|
196
|
+
fault,
|
|
197
|
+
});
|
|
198
|
+
} catch (err) {
|
|
199
|
+
return { ok: false, committed: false, passes: result.passes, verified: result.verified, slides: summary, reasons: [{ code: err.code ?? "commit_failed", message: err.message }] };
|
|
200
|
+
}
|
|
201
|
+
return { ok: true, committed: true, passes: result.passes, verified: result.verified, slides: summary, reasons: [] };
|
|
202
|
+
} finally {
|
|
203
|
+
candidate.dispose();
|
|
204
|
+
}
|
|
205
|
+
}
|