@womp/spark-agent 4.4.1 → 5.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/README.md +23 -2
  2. package/dist/capabilities/geometry.d.ts +0 -3
  3. package/dist/capabilities/geometry.js +7 -8
  4. package/dist/capabilities/openscad-instructions.d.ts +1 -1
  5. package/dist/capabilities/openscad-instructions.js +80 -81
  6. package/dist/capabilities/openscad.d.ts +0 -3
  7. package/dist/capabilities/openscad.js +1 -2
  8. package/dist/capabilities/scene.d.ts +2 -6
  9. package/dist/capabilities/scene.js +44 -45
  10. package/dist/generated/geometry-declarations.d.ts +1 -1
  11. package/dist/generated/geometry-declarations.js +1 -1
  12. package/dist/generated/openscad-declarations.d.ts +1 -1
  13. package/dist/generated/openscad-declarations.js +1 -1
  14. package/dist/generated/scene-declarations.d.ts +1 -1
  15. package/dist/generated/scene-declarations.js +1 -1
  16. package/dist/native-executor.js +2 -2
  17. package/dist/openscad.d.ts +1 -2
  18. package/dist/openscad.js +1 -19
  19. package/dist/quickjs-executor.js +1 -1
  20. package/dist/repair-state.d.ts +2 -0
  21. package/dist/repair-state.js +15 -0
  22. package/dist/runtime.d.ts +1 -5
  23. package/dist/runtime.js +8 -14
  24. package/dist/scene-environment.js +1 -1
  25. package/dist/scene-health.d.ts +1 -2
  26. package/dist/scene-health.js +0 -17
  27. package/dist/scene-operations.d.ts +5 -5
  28. package/dist/scene-operations.js +24 -43
  29. package/dist/sdk-geometry-host.d.ts +2 -2
  30. package/dist/sdk-geometry-host.js +44 -61
  31. package/dist/sdk-openscad-host.js +63 -102
  32. package/dist/sdk-scene-health-host.d.ts +2 -1
  33. package/dist/sdk-scene-health-host.js +54 -4
  34. package/dist/sdk-scene-host.d.ts +20 -5
  35. package/dist/sdk-scene-host.js +34 -31
  36. package/dist/sdk-scene-screenshot.d.ts +1 -1
  37. package/dist/sdk-scene-screenshot.js +4 -3
  38. package/dist/server/scene-access.d.ts +4 -0
  39. package/dist/server/scene-access.js +40 -0
  40. package/dist/server/standalone.js +24 -19
  41. package/dist/womp-scene-api.d.ts +15 -0
  42. package/dist/womp-scene-api.js +18 -1
  43. package/package.json +139 -139
  44. package/dist/server/scene-initialization.d.ts +0 -3
  45. package/dist/server/scene-initialization.js +0 -44
@@ -1,5 +1,4 @@
1
1
  import { KakapoSceneCommitError } from "@womp/kakapo-sdk";
2
- import { metadataObject } from "./scene-metadata.js";
3
2
  function savedResult(value) {
4
3
  if (value === undefined)
5
4
  return { empty: true };
@@ -11,9 +10,9 @@ function savedResult(value) {
11
10
  return;
12
11
  if (typeof item !== "object" || item === null ||
13
12
  (!Array.isArray(item) && Object.getPrototypeOf(item) !== Object.prototype && Object.getPrototypeOf(item) !== null))
14
- throw new Error("scene.edit must return JSON data or undefined");
13
+ throw new Error("scene.access must return JSON data or undefined");
15
14
  if (active.has(item))
16
- throw new Error("scene.edit cannot return cyclic data");
15
+ throw new Error("scene.access cannot return cyclic data");
17
16
  active.add(item);
18
17
  for (const nested of Object.values(item))
19
18
  validate(nested);
@@ -27,26 +26,17 @@ function resultValue(operation) {
27
26
  throw new Error("The saved scene operation result is missing");
28
27
  return ("value" in operation.result ? structuredClone(operation.result.value) : undefined);
29
28
  }
30
- function committedIds(ext) {
31
- const ids = metadataObject(ext.spark).committedSceneEdits;
32
- return Array.isArray(ids) ? ids.filter((id) => typeof id === "string") : [];
33
- }
34
29
  function complete(operation, status) {
35
30
  return { ...operation, status, completedAt: operation.completedAt ?? new Date().toISOString() };
36
31
  }
37
- /** Resolve a journaled edit from a fresh renderer snapshot without executing its JavaScript. */
32
+ /** Recover acknowledged/no-op results without replaying or refetching an uncertain edit. */
38
33
  export async function recoverSceneOperation(options) {
39
34
  await options.assertActive();
40
35
  const previous = await options.store.getSceneOperation(options.targetKey, options.id);
41
36
  if (!previous || previous.status === "committed" || previous.status === "rolled_back")
42
37
  return previous;
43
- await options.api.refreshScene();
44
- const ext = options.api.getSceneExt();
45
- const revision = await options.api.getSceneRevision();
46
38
  await options.assertActive();
47
- const matches = previous.resultRevision !== undefined &&
48
- previous.resultRevision === revision && committedIds(ext).includes(previous.id);
49
- if ((previous.changed === false || matches) && previous.result) {
39
+ if (previous.changed === false && previous.result) {
50
40
  return options.store.saveSceneOperation(complete(previous, "committed"));
51
41
  }
52
42
  return previous;
@@ -58,19 +48,17 @@ export async function executeSceneOperation(options) {
58
48
  await options.assertEditable();
59
49
  const previous = await store.getSceneOperation(identity.targetKey, identity.id);
60
50
  if (previous) {
61
- if (previous.expectedRevision !== identity.expectedRevision)
62
- throw new Error("Scene operation identity was reused with a different revision");
63
51
  if ((previous.save !== undefined) !== savesScene)
64
52
  throw new Error("Scene operation identity was reused with a different save policy");
65
53
  if (previous.status === "committed")
66
- return { value: resultValue(previous), operation: previous };
54
+ return { value: resultValue(previous), operation: previous, reused: true };
67
55
  if (previous.status === "rolled_back")
68
56
  throw new Error(previous.error ?? "Scene operation was rolled back");
69
57
  const recovered = await recoverSceneOperation({
70
58
  api, store, targetKey: identity.targetKey, id: identity.id, assertActive: options.assertEditable,
71
59
  });
72
60
  if (recovered?.status === "committed")
73
- return { value: resultValue(recovered), operation: recovered };
61
+ return { value: resultValue(recovered), operation: recovered, reused: true };
74
62
  throw new Error("Scene edit outcome is uncertain. Inspect the scene before making a new edit; this operation was not replayed.");
75
63
  }
76
64
  let operation = {
@@ -82,33 +70,26 @@ export async function executeSceneOperation(options) {
82
70
  try {
83
71
  const edit = async (draft) => {
84
72
  await options.assertEditable();
85
- if (await api.getSceneRevision() !== identity.expectedRevision)
86
- throw new Error("Scene changed since inspection. Read the scene again before editing.");
87
- const result = savedResult(await options.edit(draft));
88
- await options.assertEditable();
89
- const changed = await api.getSceneRevision() !== identity.expectedRevision;
90
- if (changed) {
91
- const spark = metadataObject(draft.getSceneExt().spark);
92
- const ids = [...committedIds(api.getSceneExt()), identity.id].slice(-64);
93
- draft.patchSceneExt([{ op: "add", path: "/spark", value: { ...spark, committedSceneEdits: ids } }]);
94
- }
95
- operation = await store.saveSceneOperation({
96
- ...operation, status: "prepared", result, changed,
97
- ...(changed ? { resultRevision: await api.getSceneRevision() } : {}),
98
- ...(savesScene && !changed ? { save: { status: "not_needed" } } : {}),
99
- });
100
- await options.assertEditable();
101
- if (changed)
102
- await options.prepareCommit?.(operation);
73
+ operation = { ...operation, result: savedResult(await options.edit(draft)) };
103
74
  await options.assertEditable();
104
75
  };
105
- if (options.saveToRenderer) {
106
- const committed = await api.editSceneAndSave(edit);
76
+ const committed = await api.accessScene(edit, {
77
+ save: savesScene,
78
+ beforeCommit: async (changed) => {
79
+ await options.assertEditable();
80
+ operation = await store.saveSceneOperation({
81
+ ...operation, status: "prepared", changed,
82
+ ...(savesScene && !changed ? { save: { status: "not_needed" } } : {}),
83
+ });
84
+ await options.assertEditable();
85
+ if (changed)
86
+ await options.prepareCommit?.(operation);
87
+ },
88
+ });
89
+ if (savesScene)
107
90
  operation = { ...operation, save: committed.save };
108
- }
109
- else {
110
- await api.editScene(edit);
111
- }
91
+ if (committed.changed)
92
+ options.onCommittedScene?.(committed.scene);
112
93
  }
113
94
  catch (error) {
114
95
  const status = error instanceof KakapoSceneCommitError ? error.outcome
@@ -125,7 +106,7 @@ export async function executeSceneOperation(options) {
125
106
  }
126
107
  throw error;
127
108
  }
128
- // Recovery requires both the operation marker and the exact prepared scene revision.
109
+ // A prepared record alone never proves an uncertain commit succeeded.
129
110
  operation = await store.saveSceneOperation(complete(operation, "committed"));
130
111
  return { value: resultValue(operation), operation };
131
112
  }
@@ -1,12 +1,12 @@
1
1
  import { type SdkSceneEditor } from "./sdk-scene-host.js";
2
2
  import type { GeometryHost } from "./capabilities.js";
3
- import { readStableSdkSceneHealth } from "./scene-health.js";
3
+ import { readSdkSceneHealth } from "./scene-health.js";
4
4
  import { type RepairStateStore } from "./repair-state.js";
5
5
  export type SdkGeometryHostOptions = {
6
6
  editor: SdkSceneEditor;
7
7
  repairs: RepairStateStore;
8
8
  canGrow?: () => boolean;
9
- onHealth?: (health: Awaited<ReturnType<typeof readStableSdkSceneHealth>>) => void;
9
+ onHealth?: (health: Awaited<ReturnType<typeof readSdkSceneHealth>>) => void;
10
10
  onSelect?: (objectId: number) => void | Promise<void>;
11
11
  };
12
12
  export declare function createSdkGeometryHost(options: SdkGeometryHostOptions): GeometryHost;
@@ -2,7 +2,7 @@ import { compileGeometry, migrateGraphExt, readEditableGeometry, } from "./geome
2
2
  import { graphExtSchema } from "./geometry/graph.js";
3
3
  import { boundedDiagnostics } from "./geometry/diagnostics.js";
4
4
  import { migrateParameterOverrides, readParameterOverrides, } from "./geometry/parameters.js";
5
- import { getSdkSceneAreaId, readStableSdkSceneHealth } from "./scene-health.js";
5
+ import { unverifiedSceneHealth } from "./scene-health.js";
6
6
  import { createGeometryRepairTracker, } from "./repair-state.js";
7
7
  function errorMessage(error) {
8
8
  return error instanceof Error ? error.message : String(error);
@@ -57,7 +57,6 @@ async function readTarget(api, objectId, target = resolveTarget(api, objectId))
57
57
  instanceId: target.instanceId,
58
58
  source: null,
59
59
  instances: [],
60
- revision: await api.getSceneRevision(),
61
60
  diagnostics: [],
62
61
  };
63
62
  if (target.graphId === undefined || target.graph === undefined)
@@ -93,7 +92,7 @@ function writeGraphStorage(draft, graphId, graph, instances) {
93
92
  path: "/scripts",
94
93
  value: { graphs: {}, instances: {} },
95
94
  },
96
- ]);
95
+ ], { returnScene: false });
97
96
  }
98
97
  else {
99
98
  const record = scripts;
@@ -111,7 +110,7 @@ function writeGraphStorage(draft, graphId, graph, instances) {
111
110
  value: {},
112
111
  });
113
112
  if (additions.length)
114
- draft.patchSceneStorage(additions);
113
+ draft.patchSceneStorage(additions, { returnScene: false });
115
114
  }
116
115
  draft.patchSceneStorage([
117
116
  {
@@ -124,7 +123,7 @@ function writeGraphStorage(draft, graphId, graph, instances) {
124
123
  path: `/scripts/instances/${id}`,
125
124
  value: instance,
126
125
  })),
127
- ]);
126
+ ], { returnScene: false });
128
127
  }
129
128
  export function createSdkGeometryHost(options) {
130
129
  const editor = options.editor;
@@ -134,17 +133,7 @@ export function createSdkGeometryHost(options) {
134
133
  read: (objectId, invocation) => editor.read(invocation, (api) => readTarget(api, objectId)),
135
134
  write: async (input, invocation) => {
136
135
  invocation.signal.throwIfAborted();
137
- const inspected = await editor.read(invocation, async (api) => {
138
- const current = resolveTarget(api, input.objectId);
139
- return {
140
- target: await readTarget(api, input.objectId, current),
141
- current,
142
- graphExt: current.graphId === undefined
143
- ? undefined
144
- : api.getSceneExt().graphsExt?.[current.graphId],
145
- };
146
- });
147
- const { target } = inspected;
136
+ const target = await editor.read(invocation, api => readTarget(api, input.objectId));
148
137
  const pendingKey = target.graphId === undefined
149
138
  ? `object:${input.objectId}`
150
139
  : `graph:${target.graphId}`;
@@ -152,8 +141,6 @@ export function createSdkGeometryHost(options) {
152
141
  throw new Error("This Geometry Nodes target is still being updated. Await its current update first.");
153
142
  pendingTargets.add(pendingKey);
154
143
  try {
155
- if (target.revision !== input.expectedRevision)
156
- throw new Error("The scene changed. Read it again before editing.");
157
144
  const repairingGeometry = target.graphId !== undefined &&
158
145
  (await repairs.isPending(String(target.graphId)));
159
146
  if (target.graphId !== undefined)
@@ -164,39 +151,13 @@ export function createSdkGeometryHost(options) {
164
151
  graphId: target.graphId,
165
152
  instanceId: target.instanceId,
166
153
  affectedObjectIds: [],
167
- revision: target.revision,
168
154
  diagnostics: boundedDiagnostics(diagnostics),
169
155
  });
170
156
  let compiled;
171
- let nextExt;
172
- const updates = new Map();
173
157
  const diagnostics = [];
174
158
  try {
175
159
  compiled = compileGeometry(input.source);
176
160
  readEditableGeometry(compiled.graph, compiled.graphExt);
177
- if (target.graphId === undefined) {
178
- nextExt = migrateGraphExt({ nodes: {} }, {}, compiled);
179
- }
180
- else {
181
- const { current } = inspected;
182
- if (current.graph === undefined)
183
- throw new Error("The Geometry Nodes graph is missing.");
184
- const ext = graphExtSchema.parse(inspected.graphExt ?? {});
185
- const previous = readEditableGeometry(current.graph, ext);
186
- nextExt = migrateGraphExt(ext, previous.nodeIds, compiled);
187
- for (const [id, instance] of Object.entries(current.scripts.instances)) {
188
- if (instance.graph_id !== target.graphId)
189
- continue;
190
- const old = readParameterOverrides(previous.graph, ext, instance.data ?? {});
191
- const migrated = migrateParameterOverrides(previous.graph, ext, compiled.graph, nextExt, old.data);
192
- updates.set(id, {
193
- graph_id: target.graphId,
194
- data: migrated.data,
195
- });
196
- if (migrated.removed.length)
197
- diagnostics.push(`Instance ${id}: removed or type-changed overrides ${migrated.removed.join(", ")} now use graph defaults where present.`);
198
- }
199
- }
200
161
  }
201
162
  catch (error) {
202
163
  return rejected([errorMessage(error)]);
@@ -209,7 +170,7 @@ export function createSdkGeometryHost(options) {
209
170
  let operationId;
210
171
  let applied;
211
172
  try {
212
- applied = await editor.edit({
173
+ applied = await editor.access({
213
174
  capability: "geometry",
214
175
  operation: "write",
215
176
  target: () => receiptTarget,
@@ -220,6 +181,36 @@ export function createSdkGeometryHost(options) {
220
181
  throw new Error("Scene memory limits prevent applying this graph.");
221
182
  if (current.graphId !== target.graphId)
222
183
  throw new Error("The Geometry Nodes target changed. Read it again.");
184
+ let nextExt;
185
+ const updates = new Map();
186
+ try {
187
+ if (target.graphId === undefined) {
188
+ nextExt = migrateGraphExt({ nodes: {} }, {}, compiled);
189
+ }
190
+ else {
191
+ if (current.graph === undefined)
192
+ throw new Error("The Geometry Nodes graph is missing.");
193
+ const graphExt = draft.getSceneExt(["graphsExt"]).graphsExt;
194
+ const ext = graphExtSchema.parse(graphExt?.[target.graphId] ?? {});
195
+ const previous = readEditableGeometry(current.graph, ext);
196
+ nextExt = migrateGraphExt(ext, previous.nodeIds, compiled);
197
+ for (const [id, instance] of Object.entries(current.scripts.instances)) {
198
+ if (instance.graph_id !== target.graphId)
199
+ continue;
200
+ const old = readParameterOverrides(previous.graph, ext, instance.data ?? {});
201
+ const migrated = migrateParameterOverrides(previous.graph, ext, compiled.graph, nextExt, old.data);
202
+ updates.set(id, {
203
+ graph_id: target.graphId,
204
+ data: migrated.data,
205
+ });
206
+ if (migrated.removed.length)
207
+ diagnostics.push(`Instance ${id}: removed or type-changed overrides ${migrated.removed.join(", ")} now use graph defaults where present.`);
208
+ }
209
+ }
210
+ }
211
+ catch (error) {
212
+ return { rejected: [errorMessage(error)] };
213
+ }
223
214
  if (current.graphId !== undefined)
224
215
  await repairs.reserve(String(current.graphId), id);
225
216
  let objectId = input.objectId;
@@ -247,34 +238,25 @@ export function createSdkGeometryHost(options) {
247
238
  if (!ext.graphsExt)
248
239
  draft.patchSceneExt([
249
240
  { op: "add", path: "/graphsExt", value: {} },
250
- ]);
241
+ ], { returnScene: false });
251
242
  draft.patchSceneExt([
252
243
  {
253
244
  op: "add",
254
245
  path: `/graphsExt/${graphId}`,
255
246
  value: nextExt,
256
247
  },
257
- ]);
248
+ ], { returnScene: false });
258
249
  const affectedObjectIds = Object.entries(scriptStorage(draft).instances)
259
250
  .filter(([, instance]) => instance.graph_id === graphId)
260
251
  .flatMap(([id]) => objectIdsFor(draft, Number(id)));
261
252
  Object.assign(receiptTarget, { objectId, graphId, instanceId });
262
253
  return { objectId, graphId, instanceId, affectedObjectIds };
263
- }, input.expectedRevision, invocation, async (_operation, api) => {
264
- const committed = resolveTarget(api, receiptTarget.objectId);
265
- if (committed.graphId === undefined)
254
+ }, invocation, async (operation) => {
255
+ const graphId = receiptTarget.graphId;
256
+ if (graphId === undefined)
266
257
  return;
267
- const affectedObjectIds = Object.entries(committed.scripts.instances)
268
- .filter(([, instance]) => instance.graph_id === committed.graphId)
269
- .flatMap(([id]) => objectIdsFor(api, Number(id)));
270
- const areaIds = [
271
- ...new Set(affectedObjectIds.map((id) => getSdkSceneAreaId(api, id))),
272
- ];
273
- const sceneHealth = await readStableSdkSceneHealth(api, areaIds, invocation.signal);
274
- options.onHealth?.(sceneHealth);
275
- const recorded = await repairs.record(String(committed.graphId), _operation.id, sceneHealth);
258
+ const recorded = await repairs.record(String(graphId), operation.id, unverifiedSceneHealth([], "Health was not requested."));
276
259
  return {
277
- sceneHealth,
278
260
  geometryRepairAttempts: recorded.repairAttempts,
279
261
  geometryRepairLimit: recorded.repairLimit,
280
262
  };
@@ -288,6 +270,8 @@ export function createSdkGeometryHost(options) {
288
270
  }
289
271
  throw error;
290
272
  }
273
+ if (applied.value.rejected)
274
+ return rejected(applied.value.rejected);
291
275
  invocation.signal.throwIfAborted();
292
276
  try {
293
277
  await options.onSelect?.(applied.value.objectId);
@@ -296,7 +280,6 @@ export function createSdkGeometryHost(options) {
296
280
  return {
297
281
  status: "applied",
298
282
  ...applied.value,
299
- revision: applied.operation.resultRevision ?? input.expectedRevision,
300
283
  diagnostics: boundedDiagnostics(diagnostics),
301
284
  };
302
285
  }
@@ -1,8 +1,7 @@
1
1
  import { z } from "zod";
2
- import { getOpenScadGroundedPosition, MAX_PRINTABILITY_REPAIR_ATTEMPTS, requestStableOpenScadPrintability, toOpenScadCompileResponse, } from "./openscad.js";
3
- import { waitForOpenScadRenderer } from "./sdk-openscad-renderer.js";
2
+ import { getOpenScadGroundedPosition, MAX_PRINTABILITY_REPAIR_ATTEMPTS, requestTurnOpenScadPrintability, toOpenScadCompileResponse, } from "./openscad.js";
4
3
  import { createGeometryRepairTracker, createRepairTracker, } from "./repair-state.js";
5
- import { getSdkSceneAreaId, hasGeometryOverflow, readStableSdkSceneHealth, unverifiedSceneHealth, } from "./scene-health.js";
4
+ import { getSdkSceneAreaId, unverifiedSceneHealth, } from "./scene-health.js";
6
5
  const compileMetadata = z.object({
7
6
  error: z.string(),
8
7
  params: z.array(z.object({ name: z.string() })),
@@ -19,7 +18,6 @@ export function createSdkOpenScadHost(options) {
19
18
  const printabilityRepairs = createRepairTracker(options.repairs, "openscad.printability", MAX_PRINTABILITY_REPAIR_ATTEMPTS, (status) => status === "unavailable" ? undefined : status === "failed");
20
19
  const pendingTargets = new Set();
21
20
  const resolveTarget = async (api, objectId) => {
22
- const revision = await api.getSceneRevision();
23
21
  const node = objectId === 0 ? undefined : api.getNode(objectId);
24
22
  if (objectId === 0 || node?.kind === "union" || node?.kind === "group") {
25
23
  return {
@@ -30,18 +28,12 @@ export function createSdkOpenScadHost(options) {
30
28
  title: node?.name ?? "",
31
29
  source: null,
32
30
  params: {},
33
- revision,
34
31
  },
35
32
  };
36
33
  }
37
34
  if (node?.kind !== "openScad")
38
35
  throw new Error("The target must be an OpenSCAD part or an Area/Group.");
39
36
  const script = api.getOpenScadScript(node.scriptId);
40
- let rendererBounds;
41
- try {
42
- rendererBounds = await api.getNodeBoundingBox(objectId);
43
- }
44
- catch { }
45
37
  return {
46
38
  target: {
47
39
  kind: "part",
@@ -50,10 +42,8 @@ export function createSdkOpenScadHost(options) {
50
42
  source: script.source,
51
43
  title: script.name || node.name || "OpenSCAD part",
52
44
  params: structuredClone(node.params ?? {}),
53
- revision,
54
45
  },
55
46
  repairKey: `script:${node.scriptId}`,
56
- rendererBounds,
57
47
  };
58
48
  };
59
49
  const write = async (source, targetId, creating, invocation) => {
@@ -68,8 +58,6 @@ export function createSdkOpenScadHost(options) {
68
58
  let operationId = "";
69
59
  let operationRepairKey = resolved.repairKey;
70
60
  try {
71
- if (target.revision !== source.expectedRevision)
72
- throw new Error("The scene changed. Read it again before editing.");
73
61
  if (target.kind !== (creating ? "container" : "part"))
74
62
  throw new Error(creating
75
63
  ? "Creation requires a parent container ID."
@@ -85,63 +73,60 @@ export function createSdkOpenScadHost(options) {
85
73
  }
86
74
  if (creating && options.canGrow?.() === false)
87
75
  throw new Error("OpenSCAD creation is disabled because scene memory is low.");
88
- const params = z
89
- .record(z.string(), z.json())
90
- .parse({ ...target.params, ...source.params });
91
- invocation.reportStatus("running", "Compiling the parametric part...");
92
- let rawValidation;
93
- try {
94
- rawValidation = await editor.read(invocation, (api) => api.validateOpenScad(source.source, params));
95
- }
96
- catch {
97
- invocation.signal.throwIfAborted();
98
- }
99
- const response = toOpenScadCompileResponse(rawValidation);
100
- const metadataAvailable = compileMetadata.safeParse(rawValidation).success;
101
- const validation = response && (response.error || metadataAvailable)
102
- ? {
103
- status: response.error ? "invalid" : "valid",
104
- error: response.error || undefined,
105
- warnings: response.warnings,
106
- }
107
- : {
108
- status: "unavailable",
109
- warnings: [
110
- "OpenSCAD validation is unavailable; compilation and parameter validity could not be confirmed.",
111
- ],
112
- };
113
- if (validation.status === "invalid" ||
114
- (creating &&
115
- (validation.status !== "valid" ||
116
- response?.boundingBox === undefined))) {
117
- if (creating && response?.boundingBox === undefined)
118
- validation.warnings.push("The part was not added because validated placement bounds are unavailable.");
119
- return {
120
- status: "rejected",
121
- objectId,
122
- revision: target.revision,
123
- validation,
124
- repairAttempts: 0,
125
- repairLimit: MAX_PRINTABILITY_REPAIR_ATTEMPTS,
126
- };
127
- }
128
- const validNames = new Set(response && metadataAvailable
129
- ? response.params.map((param) => param.name)
130
- : Object.keys(params));
131
- const nextParams = Object.fromEntries(Object.entries(params).filter(([name]) => validNames.has(name)));
132
76
  const receiptTarget = {
133
77
  parentId: target.parentId,
134
78
  };
135
79
  if (objectId !== undefined)
136
80
  receiptTarget.objectId = objectId;
137
81
  let affectedObjectIds = [];
138
- let sceneHealth;
139
- const applied = await editor.edit({
82
+ let affectedAreaIds = [];
83
+ const applied = await editor.access({
140
84
  capability: "openscad",
141
85
  operation: creating ? "create" : "update",
142
86
  target: () => receiptTarget,
143
- }, async (_api, draft, id) => {
87
+ }, async (api, draft, id) => {
144
88
  operationId = id;
89
+ const current = draft.getNode(targetId);
90
+ if (creating === false && (current.kind !== "openScad" || `script:${current.scriptId}` !== resolved.repairKey))
91
+ throw new Error("The OpenSCAD target changed. Read it again.");
92
+ const currentParams = current.kind === "openScad" ? current.params : {};
93
+ const params = z
94
+ .record(z.string(), z.json())
95
+ .parse({ ...currentParams, ...source.params });
96
+ invocation.reportStatus("running", "Compiling the parametric part...");
97
+ let rawValidation;
98
+ try {
99
+ rawValidation = await api.validateOpenScad(source.source, params);
100
+ }
101
+ catch {
102
+ invocation.signal.throwIfAborted();
103
+ }
104
+ const response = toOpenScadCompileResponse(rawValidation);
105
+ const metadataAvailable = compileMetadata.safeParse(rawValidation).success;
106
+ const validation = response && (response.error || metadataAvailable)
107
+ ? {
108
+ status: response.error ? "invalid" : "valid",
109
+ ...(response.error ? { error: response.error } : {}),
110
+ warnings: response.warnings,
111
+ }
112
+ : {
113
+ status: "unavailable",
114
+ warnings: [
115
+ "OpenSCAD validation is unavailable; compilation and parameter validity could not be confirmed.",
116
+ ],
117
+ };
118
+ if (validation.status === "invalid" ||
119
+ (creating &&
120
+ (validation.status !== "valid" ||
121
+ response?.boundingBox === undefined))) {
122
+ if (creating && response?.boundingBox === undefined)
123
+ validation.warnings.push("The part was not added because validated placement bounds are unavailable.");
124
+ return { rejected: validation };
125
+ }
126
+ const validNames = new Set(response && metadataAvailable
127
+ ? response.params.map((param) => param.name)
128
+ : Object.keys(params));
129
+ const nextParams = Object.fromEntries(Object.entries(params).filter(([name]) => validNames.has(name)));
145
130
  if (creating === false) {
146
131
  if (resolved.repairKey === undefined)
147
132
  throw new Error("The OpenSCAD repair target is unavailable.");
@@ -158,7 +143,8 @@ export function createSdkOpenScadHost(options) {
158
143
  });
159
144
  draft.setNodeName(targetId, source.title);
160
145
  draft.setOpenScadScript(targetId, node.scriptId, nextParams);
161
- return { objectId: targetId, repairKey: resolved.repairKey };
146
+ affectedAreaIds = [...new Set(affectedObjectIds.map(id => Number(getSdkSceneAreaId(draft, id))))];
147
+ return { objectId: targetId, repairKey: resolved.repairKey, validation };
162
148
  }
163
149
  if (options.canGrow?.() === false)
164
150
  throw new Error("OpenSCAD creation is disabled because scene memory is low.");
@@ -186,69 +172,46 @@ export function createSdkOpenScadHost(options) {
186
172
  objectId = node.id;
187
173
  affectedObjectIds = [node.id];
188
174
  Object.assign(receiptTarget, { objectId: node.id, parentId });
189
- return { objectId: node.id, repairKey: operationRepairKey };
190
- }, source.expectedRevision, invocation, async (operation, api) => {
191
- if (objectId === undefined)
192
- return;
193
- const areaIds = [
194
- ...new Set(affectedObjectIds.map((id) => getSdkSceneAreaId(api, id))),
195
- ];
196
- try {
197
- const rendererReady = response?.boundingBox === undefined ||
198
- (creating === false && resolved.rendererBounds === undefined)
199
- ? false
200
- : await waitForOpenScadRenderer({
201
- api,
202
- objectId,
203
- expectedBounds: response.boundingBox,
204
- baselineBounds: resolved.rendererBounds,
205
- signal: invocation.signal,
206
- });
207
- sceneHealth = rendererReady
208
- ? await readStableSdkSceneHealth(api, areaIds, invocation.signal)
209
- : unverifiedSceneHealth(areaIds, "The renderer did not confirm the committed OpenSCAD geometry before health checks.");
210
- }
211
- catch (error) {
212
- sceneHealth = unverifiedSceneHealth(areaIds, `OpenSCAD renderer checks stopped after the edit committed: ${String(error)}`);
213
- }
214
- options.onHealth?.(sceneHealth);
175
+ affectedAreaIds = [Number(getSdkSceneAreaId(draft, node.id))];
176
+ return { objectId: node.id, repairKey: operationRepairKey, validation };
177
+ }, invocation, async (operation) => {
215
178
  if (operationRepairKey === undefined)
216
179
  return;
217
- const recorded = await geometryRepairs.record(operationRepairKey, operation.id, sceneHealth);
180
+ const recorded = await geometryRepairs.record(operationRepairKey, operation.id, unverifiedSceneHealth([], "Health was not requested."));
218
181
  return {
219
- sceneHealth,
220
182
  affectedObjectIds,
221
183
  geometryRepairAttempts: recorded.repairAttempts,
222
184
  geometryRepairLimit: recorded.repairLimit,
223
185
  };
224
186
  });
187
+ if (applied.value.rejected)
188
+ return {
189
+ status: "rejected", objectId, validation: applied.value.rejected,
190
+ repairAttempts: 0, repairLimit: MAX_PRINTABILITY_REPAIR_ATTEMPTS,
191
+ };
192
+ const { validation } = applied.value;
225
193
  objectId = applied.value.objectId;
226
194
  try {
227
195
  await options.onSelect?.(objectId);
228
196
  }
229
197
  catch { }
230
- const revision = applied.operation.resultRevision ?? source.expectedRevision;
231
- if (sceneHealth?.status === "unverified" ||
232
- (sceneHealth !== undefined && hasGeometryOverflow(sceneHealth))) {
233
- await printabilityRepairs.refund(applied.value.repairKey, operationId);
198
+ // Geometry repairs do not consume the independent printability budget.
199
+ // Explicit health must resolve the known geometry failure first.
200
+ if (repairingGeometry) {
234
201
  return {
235
202
  status: "applied",
236
203
  objectId,
237
- revision,
238
204
  validation,
239
205
  repairAttempts: 0,
240
206
  repairLimit: MAX_PRINTABILITY_REPAIR_ATTEMPTS,
241
207
  };
242
208
  }
243
209
  invocation.reportStatus("running", "Checking wall thickness...");
244
- const printability = await editor.read(invocation, (api, beginSettlement) => (options.requestPrintability ?? requestStableOpenScadPrintability)({
210
+ const printability = await editor.withConnection(invocation, (api, beginSettlement) => (options.requestPrintability ?? requestTurnOpenScadPrintability)({
245
211
  api,
246
- expectedRevision: revision,
247
212
  signal: invocation.signal,
248
213
  onSubmissionStarted: beginSettlement,
249
- objectIds: [
250
- ...new Set(affectedObjectIds.map((id) => Number(getSdkSceneAreaId(api, id)))),
251
- ],
214
+ objectIds: affectedAreaIds,
252
215
  }));
253
216
  invocation.signal.throwIfAborted();
254
217
  if (printability.reason === "context_busy") {
@@ -256,7 +219,6 @@ export function createSdkOpenScadHost(options) {
256
219
  return {
257
220
  status: "applied",
258
221
  objectId,
259
- revision,
260
222
  validation,
261
223
  printability,
262
224
  repairAttempts: refunded.repairAttempts,
@@ -267,7 +229,6 @@ export function createSdkOpenScadHost(options) {
267
229
  return {
268
230
  status: "applied",
269
231
  objectId,
270
- revision,
271
232
  validation,
272
233
  printability,
273
234
  repairAttempts: recorded.repairAttempts,
@@ -1,4 +1,5 @@
1
1
  import type { SceneHealthHost } from "./capabilities.js";
2
2
  import { type SdkSceneEditor } from "./sdk-scene-host.js";
3
+ import { type RepairStateStore } from "./repair-state.js";
3
4
  import type { SceneHealth } from "./scene-health.js";
4
- export declare function createSdkSceneHealthHost(editor: SdkSceneEditor, onHealth?: (health: SceneHealth) => void): SceneHealthHost;
5
+ export declare function createSdkSceneHealthHost(editor: SdkSceneEditor, onHealth?: (health: SceneHealth) => void, repairs?: RepairStateStore): SceneHealthHost;