@womp/spark-agent 7.0.0 → 9.0.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 (39) hide show
  1. package/CHANGELOG.md +67 -0
  2. package/README.md +3 -2
  3. package/dist/capabilities/geometry.d.ts +17 -5
  4. package/dist/capabilities/geometry.js +11 -12
  5. package/dist/generated/geometry-declarations.d.ts +1 -1
  6. package/dist/generated/geometry-declarations.js +1 -1
  7. package/dist/geometry/backendRegistry.d.ts +1 -0
  8. package/dist/geometry/common.d.ts +5 -2
  9. package/dist/geometry/common.js +18 -3
  10. package/dist/geometry/compiler.js +2 -2
  11. package/dist/geometry/diagnostics.d.ts +2 -7
  12. package/dist/geometry/diagnostics.js +3 -51
  13. package/dist/geometry/graph.d.ts +1 -1
  14. package/dist/geometry/graph.js +1 -0
  15. package/dist/geometry/index.d.ts +0 -1
  16. package/dist/geometry/index.js +0 -3
  17. package/dist/geometry/lift.js +105 -113
  18. package/dist/geometry/lower.js +29 -49
  19. package/dist/geometry/schemas/flowir-dialect.json +117 -117
  20. package/dist/geometry/writeParamData.js +4 -4
  21. package/dist/geometry.d.ts +2 -5
  22. package/dist/geometry.js +7 -11
  23. package/dist/index.d.ts +1 -1
  24. package/dist/index.js +0 -1
  25. package/dist/intent-bench.js +1 -1
  26. package/dist/kakapo.d.ts +1 -1
  27. package/dist/scene-operations.d.ts +2 -17
  28. package/dist/scene-operations.js +4 -40
  29. package/dist/sdk-geometry-host.d.ts +1 -1
  30. package/dist/sdk-geometry-host.js +252 -251
  31. package/dist/sdk-openscad-host.js +1 -3
  32. package/dist/sdk-scene-host.d.ts +3 -5
  33. package/dist/sdk-scene-host.js +5 -15
  34. package/dist/server/sqlite-store.d.ts +1 -5
  35. package/dist/server/sqlite-store.js +3 -40
  36. package/dist/server/standalone.js +0 -1
  37. package/dist/womp-scene-api.d.ts +9 -1
  38. package/dist/womp-scene-api.js +41 -32
  39. package/package.json +2 -2
@@ -1,293 +1,294 @@
1
+ import { KakapoError, } from "@womp/kakapo-sdk";
1
2
  import { SceneOperationError } from "./sdk-scene-host.js";
2
- import { compileGeometry, migrateGraphExt, readEditableGeometry, } from "./geometry/compiler.js";
3
- import { graphExtSchema } from "./geometry/graph.js";
3
+ import { compileGeometry, migrateGraphExt, readEditableGeometry } from "./geometry/compiler.js";
4
4
  import { boundedDiagnostics } from "./geometry/diagnostics.js";
5
- import { migrateParameterOverrides, readParameterOverrides, } from "./geometry/parameters.js";
5
+ import { graphExtSchema } from "./geometry/graph.js";
6
+ import { migrateParameterOverrides, readParameterOverrides } from "./geometry/parameters.js";
7
+ import { convertAABB, sceneNodePosition, sceneNodeRotation, sceneNodeSize } from "./womp-scene-api.js";
8
+ import { sceneUnitMultiplier } from "./coordinates.js";
6
9
  import { unverifiedSceneHealth } from "./scene-health.js";
7
- import { createGeometryRepairTracker, } from "./repair-state.js";
10
+ import { createGeometryRepairTracker } from "./repair-state.js";
11
+ const MEMORY_LIMIT = "Scene memory limits prevent applying this graph.";
8
12
  function errorMessage(error) {
9
13
  return error instanceof Error ? error.message : String(error);
10
14
  }
15
+ function isRecord(value) {
16
+ return typeof value === "object" && value !== null && !Array.isArray(value);
17
+ }
11
18
  function unusedId(entries) {
12
19
  let id = 0;
13
20
  while (id in entries)
14
21
  id++;
15
22
  return id;
16
23
  }
17
- function scriptStorage(source) {
18
- const scripts = source.getSceneStorage().scripts;
19
- if (!scripts || typeof scripts !== "object" || Array.isArray(scripts))
20
- return { graphs: {}, instances: {} };
21
- const value = scripts;
24
+ function readScripts(scene) {
25
+ const stored = scene.getSceneStorage().scripts;
26
+ const scripts = isRecord(stored) ? stored : {};
22
27
  return {
23
- graphs: value.graphs && typeof value.graphs === "object"
24
- ? value.graphs
25
- : {},
26
- instances: value.instances && typeof value.instances === "object"
27
- ? value.instances
28
- : {},
28
+ ...scripts,
29
+ graphs: isRecord(scripts.graphs) ? scripts.graphs : {},
30
+ instances: isRecord(scripts.instances) ? scripts.instances : {},
29
31
  };
30
32
  }
31
- function objectIdsFor(source, instanceId) {
32
- return source
33
- .listNodes({ kind: "union" })
34
- .filter((node) => node.kind === "union" && node.scriptInstanceId === instanceId)
35
- .map((node) => node.id);
33
+ function readGraphExts(scene) {
34
+ const graphExts = scene.getSceneExt(["graphsExt"]).graphsExt;
35
+ return isRecord(graphExts) ? graphExts : {};
36
+ }
37
+ function graphExtOf(graphExts, graphId) {
38
+ return graphExtSchema.parse(graphExts[graphId] ?? {});
36
39
  }
37
- function resolveTarget(source, objectId) {
38
- const node = source.getNode(objectId);
40
+ function graphInstances(scripts, graphId) {
41
+ return Object.entries(scripts.instances).filter(([, instance]) => instance.graph_id === graphId);
42
+ }
43
+ function objectIdsByInstance(scene) {
44
+ const objectIds = new Map();
45
+ for (const node of scene.listNodes({ kind: "union" })) {
46
+ if (node.kind !== "union" || node.scriptInstanceId === undefined)
47
+ continue;
48
+ const ids = objectIds.get(node.scriptInstanceId);
49
+ if (ids)
50
+ ids.push(node.id);
51
+ else
52
+ objectIds.set(node.scriptInstanceId, [node.id]);
53
+ }
54
+ return objectIds;
55
+ }
56
+ function resolveTarget(scene, objectId) {
57
+ const node = scene.getNode(objectId);
39
58
  if (objectId !== 0 && node.kind !== "union" && node.kind !== "group")
40
59
  throw new Error("Geometry Nodes requires an Area, Group, or scene root target.");
60
+ const scripts = readScripts(scene);
41
61
  const instanceId = node.kind === "union" ? node.scriptInstanceId : undefined;
42
- const scripts = scriptStorage(source);
43
- const instance = instanceId === undefined ? undefined : scripts.instances[instanceId];
44
- if (instanceId !== undefined && !instance)
62
+ if (instanceId === undefined)
63
+ return { node, scripts };
64
+ const instance = scripts.instances[instanceId];
65
+ if (!instance)
45
66
  throw new Error("The target's Geometry Nodes instance is missing. Inspect it before editing.");
46
- const graphId = instance?.graph_id;
47
- const graph = graphId === undefined ? undefined : scripts.graphs[graphId];
48
- if (instance && (graph === undefined || !Number.isInteger(graphId)))
67
+ const graphId = instance.graph_id;
68
+ const graph = scripts.graphs[graphId];
69
+ if (graph === undefined || !Number.isInteger(graphId))
49
70
  throw new Error("The target's Geometry Nodes graph is missing. Inspect it before editing.");
50
- return { node, graph, graphId, instanceId, scripts };
71
+ return { node, scripts, graphId, instanceId, graph };
51
72
  }
52
- async function readTarget(api, objectId, target = resolveTarget(api, objectId)) {
53
- const result = {
54
- kind: target.graphId === undefined ? "container" : "graph",
55
- objectId,
56
- title: target.node.name,
57
- graphId: target.graphId,
58
- instanceId: target.instanceId,
59
- source: null,
60
- instances: [],
61
- diagnostics: [],
62
- };
63
- if (target.graphId === undefined || target.graph === undefined)
64
- return result;
73
+ function describeGraph(api, target) {
74
+ if (target.graph === undefined)
75
+ return { source: null, instances: [], diagnostics: [] };
65
76
  try {
66
- const ext = graphExtSchema.parse(api.getSceneExt().graphsExt?.[target.graphId] ?? {});
77
+ const ext = graphExtOf(readGraphExts(api), target.graphId);
67
78
  const editable = readEditableGeometry(target.graph, ext);
68
- result.source = editable.source;
69
- for (const [id, instance] of Object.entries(target.scripts.instances)) {
70
- if (instance.graph_id !== target.graphId)
71
- continue;
72
- const { params } = readParameterOverrides(editable.graph, ext, instance.data ?? {});
73
- result.instances.push({
79
+ const objectIds = objectIdsByInstance(api);
80
+ return {
81
+ source: editable.source,
82
+ instances: graphInstances(target.scripts, target.graphId).map(([id, instance]) => ({
74
83
  instanceId: Number(id),
75
- objectIds: objectIdsFor(api, Number(id)),
76
- params,
77
- });
78
- }
84
+ objectIds: objectIds.get(Number(id)) ?? [],
85
+ params: readParameterOverrides(editable.graph, ext, instance.data ?? {}).params,
86
+ })),
87
+ diagnostics: [],
88
+ };
79
89
  }
80
90
  catch (error) {
81
- result.source = null;
82
- // Bounded: a compiler diagnostic is unbounded text (./geometry/diagnostics.ts).
83
- result.diagnostics.push(...boundedDiagnostics([errorMessage(error)]));
91
+ return { source: null, instances: [], diagnostics: boundedDiagnostics([errorMessage(error)]) };
84
92
  }
85
- return structuredClone(result);
86
93
  }
87
- function writeGraphStorage(draft, graphId, graph, instances) {
88
- const scripts = draft.getSceneStorage().scripts;
89
- if (!scripts || typeof scripts !== "object" || Array.isArray(scripts)) {
90
- draft.patchSceneStorage([
91
- {
92
- op: "add",
93
- path: "/scripts",
94
- value: { graphs: {}, instances: {} },
95
- },
96
- ], { returnScene: false });
94
+ async function readPlacement(api, objectId, target) {
95
+ if (objectId === 0)
96
+ return { transform: null, bounds: null, diagnostics: [] };
97
+ const reader = { requireNode: (id) => api.getNode(id), getNode: (id) => api.getNode(id) };
98
+ const unitMultiplier = sceneUnitMultiplier(api.getSceneProperties().unit_multiplier);
99
+ const transform = {
100
+ position: sceneNodePosition(reader, 0, unitMultiplier, objectId),
101
+ rotation: sceneNodeRotation(reader, objectId),
102
+ size: sceneNodeSize(reader, 0, unitMultiplier, objectId),
103
+ };
104
+ if (target.graph === undefined || target.node.parentId !== 0)
105
+ return { transform, bounds: null, diagnostics: [] };
106
+ try {
107
+ return { transform, bounds: convertAABB(await api.getNodeBoundingBox(objectId), unitMultiplier), diagnostics: [] };
97
108
  }
98
- else {
99
- const record = scripts;
100
- const additions = [];
101
- if (!record.graphs)
102
- additions.push({
103
- op: "add",
104
- path: "/scripts/graphs",
105
- value: {},
106
- });
107
- if (!record.instances)
108
- additions.push({
109
- op: "add",
110
- path: "/scripts/instances",
111
- value: {},
112
- });
113
- if (additions.length)
114
- draft.patchSceneStorage(additions, { returnScene: false });
109
+ catch (error) {
110
+ if (error instanceof KakapoError && error.code === "EMPTY_BOUNDING_BOX")
111
+ return { transform, bounds: null, diagnostics: [] };
112
+ return { transform, bounds: null, diagnostics: [`Bounds are unavailable: ${errorMessage(error)}`] };
115
113
  }
116
- draft.patchSceneStorage([
117
- {
118
- op: "add",
119
- path: `/scripts/graphs/${graphId}`,
120
- value: graph,
121
- },
122
- ...[...instances].map(([id, instance]) => ({
123
- op: "add",
124
- path: `/scripts/instances/${id}`,
125
- value: instance,
126
- })),
127
- ], { returnScene: false });
114
+ }
115
+ async function readTarget(api, objectId) {
116
+ const target = resolveTarget(api, objectId);
117
+ const graph = describeGraph(api, target);
118
+ const placement = await readPlacement(api, objectId, target);
119
+ return {
120
+ kind: target.graph === undefined ? "container" : "graph",
121
+ objectId,
122
+ title: target.node.name,
123
+ graphId: target.graphId,
124
+ instanceId: target.instanceId,
125
+ source: graph.source,
126
+ transform: placement.transform,
127
+ bounds: placement.bounds,
128
+ instances: graph.instances,
129
+ diagnostics: boundedDiagnostics([...graph.diagnostics, ...placement.diagnostics]),
130
+ };
131
+ }
132
+ function planUpdate(target, graphExts, compiled) {
133
+ const ext = graphExtOf(graphExts, target.graphId);
134
+ const previous = readEditableGeometry(target.graph, ext);
135
+ const nextExt = migrateGraphExt(ext, previous.nodeIds, compiled);
136
+ const instances = {};
137
+ const diagnostics = [];
138
+ for (const [id, instance] of graphInstances(target.scripts, target.graphId)) {
139
+ const migrated = migrateParameterOverrides(previous.graph, ext, compiled.graph, nextExt, instance.data ?? {});
140
+ instances[id] = { graph_id: target.graphId, data: migrated.data };
141
+ if (migrated.removed.length)
142
+ diagnostics.push(`Instance ${id}: removed or type-changed overrides ${migrated.removed.join(", ")} now use graph defaults where present.`);
143
+ }
144
+ return { graphId: target.graphId, instanceId: target.instanceId, graph: compiled.graph, ext: nextExt, instances, diagnostics };
145
+ }
146
+ function planCreation(target, compiled) {
147
+ const graphId = unusedId(target.scripts.graphs);
148
+ const instanceId = unusedId(target.scripts.instances);
149
+ return {
150
+ graphId,
151
+ instanceId,
152
+ graph: compiled.graph,
153
+ ext: migrateGraphExt({ nodes: {} }, {}, compiled),
154
+ instances: { [instanceId]: { graph_id: graphId, data: {} } },
155
+ diagnostics: [],
156
+ };
157
+ }
158
+ function attachArea(draft, objectId, node, instanceId) {
159
+ const areaId = objectId !== 0 && node.kind === "union"
160
+ ? objectId
161
+ : draft.createNode({ kind: "union", parentId: objectId, name: "Area" }).id;
162
+ draft.updateNode(areaId, { scriptInstanceId: instanceId });
163
+ return areaId;
164
+ }
165
+ function writeGraph(draft, scripts, graphExts, change) {
166
+ const nextScripts = {
167
+ ...scripts,
168
+ graphs: { ...scripts.graphs, [change.graphId]: change.graph },
169
+ instances: { ...scripts.instances, ...change.instances },
170
+ };
171
+ draft.patchSceneStorage([{ op: "add", path: "/scripts", value: nextScripts }], { returnScene: false });
172
+ draft.patchSceneExt([{ op: "add", path: "/graphsExt", value: { ...graphExts, [change.graphId]: change.ext } }], { returnScene: false });
128
173
  }
129
174
  export function createSdkGeometryHost(options) {
130
- const editor = options.editor;
175
+ const { editor } = options;
131
176
  const repairs = createGeometryRepairTracker(options.repairs, "geometry");
132
177
  const pendingTargets = new Set();
133
- return {
134
- read: (objectId, invocation) => editor.read(invocation, (api) => readTarget(api, objectId)),
135
- write: async (input, invocation) => {
136
- invocation.signal.throwIfAborted();
137
- const target = await editor.read(invocation, api => readTarget(api, input.objectId));
138
- const pendingKey = target.graphId === undefined
139
- ? `object:${input.objectId}`
140
- : `graph:${target.graphId}`;
141
- if (pendingTargets.has(pendingKey))
142
- throw new Error("This Geometry Nodes target is still being updated. Await its current update first.");
143
- pendingTargets.add(pendingKey);
178
+ const write = async (input, invocation) => {
179
+ const target = await editor.read(invocation, (api) => resolveTarget(api, input.objectId));
180
+ const rejected = (diagnostics) => ({
181
+ status: "rejected",
182
+ objectId: input.objectId,
183
+ graphId: target.graphId,
184
+ instanceId: target.instanceId,
185
+ affectedObjectIds: [],
186
+ diagnostics: boundedDiagnostics(diagnostics),
187
+ });
188
+ const pendingKey = target.graphId === undefined ? `object:${input.objectId}` : `graph:${target.graphId}`;
189
+ if (pendingTargets.has(pendingKey))
190
+ throw new Error("This Geometry Nodes target is still being updated. Await its current update first.");
191
+ pendingTargets.add(pendingKey);
192
+ try {
193
+ const repairKey = target.graphId === undefined ? undefined : String(target.graphId);
194
+ const repairing = repairKey !== undefined && (await repairs.isPending(repairKey));
195
+ if (repairKey !== undefined)
196
+ await repairs.assertCanWrite(repairKey);
197
+ let compiled;
144
198
  try {
145
- const repairingGeometry = target.graphId !== undefined &&
146
- (await repairs.isPending(String(target.graphId)));
147
- if (target.graphId !== undefined)
148
- await repairs.assertCanWrite(String(target.graphId));
149
- const rejected = (diagnostics) => ({
150
- status: "rejected",
151
- objectId: input.objectId,
152
- graphId: target.graphId,
153
- instanceId: target.instanceId,
154
- affectedObjectIds: [],
155
- diagnostics: boundedDiagnostics(diagnostics),
156
- });
157
- let compiled;
158
- const diagnostics = [];
159
- try {
160
- compiled = compileGeometry(input.source);
161
- readEditableGeometry(compiled.graph, compiled.graphExt);
162
- }
163
- catch (error) {
164
- return rejected([errorMessage(error)]);
165
- }
166
- if (repairingGeometry === false && options.canGrow?.() === false)
167
- return rejected(["Scene memory limits prevent applying this graph."]);
168
- const receiptTarget = {
169
- objectId: input.objectId,
170
- };
171
- let operationId;
172
- let applied;
173
- try {
174
- applied = await editor.access({
175
- capability: "geometry",
176
- operation: "write",
177
- target: () => receiptTarget,
178
- }, async (_api, draft, id) => {
179
- operationId = id;
180
- const current = resolveTarget(draft, input.objectId);
181
- if (repairingGeometry === false && options.canGrow?.() === false)
182
- throw new Error("Scene memory limits prevent applying this graph.");
183
- if (current.graphId !== target.graphId)
184
- throw new Error("The Geometry Nodes target changed. Read it again.");
185
- let nextExt;
186
- const updates = new Map();
187
- try {
188
- if (target.graphId === undefined) {
189
- nextExt = migrateGraphExt({ nodes: {} }, {}, compiled);
190
- }
191
- else {
192
- if (current.graph === undefined)
193
- throw new Error("The Geometry Nodes graph is missing.");
194
- const graphExt = draft.getSceneExt(["graphsExt"]).graphsExt;
195
- const ext = graphExtSchema.parse(graphExt?.[target.graphId] ?? {});
196
- const previous = readEditableGeometry(current.graph, ext);
197
- nextExt = migrateGraphExt(ext, previous.nodeIds, compiled);
198
- for (const [id, instance] of Object.entries(current.scripts.instances)) {
199
- if (instance.graph_id !== target.graphId)
200
- continue;
201
- const old = readParameterOverrides(previous.graph, ext, instance.data ?? {});
202
- const migrated = migrateParameterOverrides(previous.graph, ext, compiled.graph, nextExt, old.data);
203
- updates.set(id, {
204
- graph_id: target.graphId,
205
- data: migrated.data,
206
- });
207
- if (migrated.removed.length)
208
- diagnostics.push(`Instance ${id}: removed or type-changed overrides ${migrated.removed.join(", ")} now use graph defaults where present.`);
209
- }
210
- }
211
- }
212
- catch (error) {
213
- return { rejected: [errorMessage(error)] };
214
- }
215
- if (current.graphId !== undefined)
216
- await repairs.reserve(String(current.graphId), id);
217
- let objectId = input.objectId;
218
- let graphId = current.graphId;
219
- let instanceId = current.instanceId;
220
- const nextInstances = new Map(updates);
221
- if (graphId === undefined) {
222
- graphId = unusedId(current.scripts.graphs);
223
- instanceId = unusedId(current.scripts.instances);
224
- if (objectId === 0 || current.node.kind !== "union") {
225
- objectId = draft.createNode({
226
- kind: "union",
227
- parentId: objectId,
228
- name: "Area",
229
- }).id;
230
- }
231
- draft.updateNode(objectId, { scriptInstanceId: instanceId });
232
- nextInstances.set(String(instanceId), {
233
- graph_id: graphId,
234
- data: {},
235
- });
236
- }
237
- writeGraphStorage(draft, graphId, compiled.graph, nextInstances);
238
- const ext = draft.getSceneExt();
239
- if (!ext.graphsExt)
240
- draft.patchSceneExt([
241
- { op: "add", path: "/graphsExt", value: {} },
242
- ], { returnScene: false });
243
- draft.patchSceneExt([
244
- {
245
- op: "add",
246
- path: `/graphsExt/${graphId}`,
247
- value: nextExt,
248
- },
249
- ], { returnScene: false });
250
- const affectedObjectIds = Object.entries(scriptStorage(draft).instances)
251
- .filter(([, instance]) => instance.graph_id === graphId)
252
- .flatMap(([id]) => objectIdsFor(draft, Number(id)));
253
- Object.assign(receiptTarget, { objectId, graphId, instanceId });
254
- return { objectId, graphId, instanceId, affectedObjectIds };
255
- }, invocation, async (operation) => {
256
- const graphId = receiptTarget.graphId;
257
- if (graphId === undefined)
258
- return;
259
- const recorded = await repairs.record(String(graphId), operation.id, unverifiedSceneHealth([], "Health was not requested."));
260
- return {
261
- geometryRepairAttempts: recorded.repairAttempts,
262
- geometryRepairLimit: recorded.repairLimit,
263
- };
264
- });
265
- }
266
- catch (error) {
267
- if (operationId !== undefined && target.graphId !== undefined) {
268
- const operation = error instanceof SceneOperationError
269
- ? error.operation : await editor.getOperation(operationId);
270
- if (operation?.status === "rolled_back")
271
- await repairs.refund(String(target.graphId), operationId);
199
+ compiled = compileGeometry(input.source);
200
+ readEditableGeometry(compiled.graph, compiled.graphExt);
201
+ }
202
+ catch (error) {
203
+ return rejected([errorMessage(error)]);
204
+ }
205
+ const memoryBlocked = () => repairing === false && options.canGrow?.() === false;
206
+ if (memoryBlocked())
207
+ return rejected([MEMORY_LIMIT]);
208
+ const receipt = { objectId: input.objectId };
209
+ let operationId;
210
+ let applied;
211
+ try {
212
+ applied = await editor.access({ capability: "geometry", operation: "write", target: () => receipt }, async (_api, draft, id) => {
213
+ operationId = id;
214
+ const current = resolveTarget(draft, input.objectId);
215
+ if (memoryBlocked())
216
+ throw new Error(MEMORY_LIMIT);
217
+ if (current.graphId !== target.graphId)
218
+ throw new Error("The Geometry Nodes target changed. Read it again.");
219
+ const graphExts = readGraphExts(draft);
220
+ let change;
221
+ try {
222
+ change = current.graph === undefined ? planCreation(current, compiled) : planUpdate(current, graphExts, compiled);
272
223
  }
273
- throw error;
274
- }
275
- if (applied.value.rejected)
276
- return rejected(applied.value.rejected);
277
- invocation.signal.throwIfAborted();
278
- try {
279
- await options.onSelect?.(applied.value.objectId);
224
+ catch (error) {
225
+ return { rejected: [errorMessage(error)] };
226
+ }
227
+ if (current.graph !== undefined)
228
+ await repairs.reserve(String(current.graphId), id);
229
+ const objectId = current.graph === undefined ? attachArea(draft, input.objectId, current.node, change.instanceId) : input.objectId;
230
+ writeGraph(draft, current.scripts, graphExts, change);
231
+ const objectIds = objectIdsByInstance(draft);
232
+ Object.assign(receipt, { objectId, graphId: change.graphId, instanceId: change.instanceId });
233
+ return {
234
+ objectId,
235
+ graphId: change.graphId,
236
+ instanceId: change.instanceId,
237
+ affectedObjectIds: Object.keys(change.instances).flatMap((instanceId) => objectIds.get(Number(instanceId)) ?? []),
238
+ diagnostics: change.diagnostics,
239
+ };
240
+ }, invocation, async (operation) => {
241
+ if (receipt.graphId === undefined)
242
+ return;
243
+ const recorded = await repairs.record(String(receipt.graphId), operation.id, unverifiedSceneHealth([], "Health was not requested."));
244
+ return { geometryRepairAttempts: recorded.repairAttempts, geometryRepairLimit: recorded.repairLimit };
245
+ });
246
+ }
247
+ catch (error) {
248
+ if (operationId !== undefined && repairKey !== undefined) {
249
+ if (error instanceof SceneOperationError && error.operation.status === "rolled_back")
250
+ await repairs.refund(repairKey, operationId);
280
251
  }
281
- catch { }
282
- return {
283
- status: "applied",
284
- ...applied.value,
285
- diagnostics: boundedDiagnostics(diagnostics),
286
- };
252
+ throw error;
287
253
  }
288
- finally {
289
- pendingTargets.delete(pendingKey);
254
+ const result = applied.value;
255
+ if (result.rejected)
256
+ return rejected(result.rejected);
257
+ invocation.signal.throwIfAborted();
258
+ try {
259
+ await options.onSelect?.(result.objectId);
260
+ }
261
+ catch { }
262
+ let committed;
263
+ try {
264
+ committed = await editor.read(invocation, (api) => readTarget(api, result.objectId));
290
265
  }
291
- },
266
+ catch (error) {
267
+ committed = {
268
+ source: null,
269
+ transform: null,
270
+ bounds: null,
271
+ diagnostics: [`The write was applied, but reading it back failed: ${errorMessage(error)} Read the target before editing it again.`],
272
+ };
273
+ }
274
+ return {
275
+ status: "applied",
276
+ objectId: result.objectId,
277
+ graphId: result.graphId,
278
+ instanceId: result.instanceId,
279
+ affectedObjectIds: result.affectedObjectIds,
280
+ source: committed.source ?? undefined,
281
+ transform: committed.transform,
282
+ bounds: committed.bounds,
283
+ diagnostics: boundedDiagnostics([...result.diagnostics, ...committed.diagnostics]),
284
+ };
285
+ }
286
+ finally {
287
+ pendingTargets.delete(pendingKey);
288
+ }
289
+ };
290
+ return {
291
+ read: (objectId, invocation) => editor.read(invocation, (api) => readTarget(api, objectId)),
292
+ write,
292
293
  };
293
294
  }
@@ -238,9 +238,7 @@ export function createSdkOpenScadHost(options) {
238
238
  }
239
239
  catch (error) {
240
240
  if (creating === false && operationId !== "") {
241
- const operation = error instanceof SceneOperationError
242
- ? error.operation : await editor.getOperation(operationId);
243
- if (operation?.status === "rolled_back") {
241
+ if (error instanceof SceneOperationError && error.operation.status === "rolled_back") {
244
242
  await geometryRepairs.refund(resolved.repairKey, operationId);
245
243
  await printabilityRepairs.refund(resolved.repairKey, operationId);
246
244
  }
@@ -1,7 +1,7 @@
1
1
  import type { FontFamily, KakapoAPI, SceneEdit, MaterialResource, NativeSceneSnapshot } from "@womp/kakapo-sdk";
2
2
  import type { ArtifactRef, SparkInvocationContext } from "./capabilities.js";
3
3
  import type { SceneHost, SceneHealthHost, SceneScreenshotOptions } from "./capabilities/scene.js";
4
- import { type SceneOperation, type SceneOperationStore } from "./scene-operations.js";
4
+ import { type SceneOperation } from "./scene-operations.js";
5
5
  import type { SceneHdr } from "./scene-environment.js";
6
6
  export type SdkSceneServices = {
7
7
  fonts: FontFamily[];
@@ -19,7 +19,6 @@ export type SdkSceneServices = {
19
19
  export type SdkSceneEditorOptions = {
20
20
  targetKey: string;
21
21
  projectId?: number;
22
- operations?: SceneOperationStore;
23
22
  saveToRenderer?: boolean;
24
23
  withConnection<T>(context: SparkInvocationContext, action: (api: KakapoAPI, beginSettlement: () => void,
25
24
  /** Supply this when the host adds a stricter connection lifetime. */
@@ -27,7 +26,7 @@ export type SdkSceneEditorOptions = {
27
26
  assertEditable(context: SparkInvocationContext): void | Promise<void>;
28
27
  /** Suspend for the complete access, including reads and failed preparation. */
29
28
  acquireAccess?(context: SparkInvocationContext): Promise<(synchronized: boolean) => void | Promise<void>>;
30
- /** Must adopt each operation ID at most once, including during recovery. */
29
+ /** Must adopt each operation ID at most once. */
31
30
  onCommitted?(operation: SceneOperation, api: KakapoAPI, context: SparkInvocationContext, identity: SdkSceneEditIdentity, scene: NativeSceneSnapshot): Promise<Record<string, unknown> | void>;
32
31
  };
33
32
  export type SdkSceneEditIdentity = {
@@ -35,13 +34,12 @@ export type SdkSceneEditIdentity = {
35
34
  operation: string;
36
35
  target?: Record<string, unknown> | (() => Record<string, unknown>);
37
36
  };
38
- /** Carries this invocation's outcome without requiring a persistent journal. */
37
+ /** A failed edit's outcome, for callers that settle per-edit state such as repair budgets. */
39
38
  export declare class SceneOperationError extends Error {
40
39
  readonly operation: SceneOperation;
41
40
  constructor(operation: SceneOperation, cause: unknown);
42
41
  }
43
42
  export type SdkSceneEditor = {
44
- getOperation(operationId: string): Promise<SceneOperation | undefined>;
45
43
  withConnection<T>(context: SparkInvocationContext, action: (api: KakapoAPI, beginSettlement: () => void) => T | Promise<T>): Promise<T>;
46
44
  read<T>(context: SparkInvocationContext, action: (api: KakapoAPI, beginSettlement: () => void) => T | Promise<T>): Promise<T>;
47
45
  access<T>(identity: SdkSceneEditIdentity, callback: (api: KakapoAPI, draft: SceneEdit, operationId: string) => T | Promise<T>, context: SparkInvocationContext, afterCommitted?: (operation: SceneOperation, api: KakapoAPI) => Promise<Record<string, unknown> | void>): Promise<{