@grimoirelabs/core 0.7.0 → 0.9.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/dist/compiler/grimoire/ast.d.ts +5 -0
- package/dist/compiler/grimoire/ast.d.ts.map +1 -1
- package/dist/compiler/grimoire/ast.js.map +1 -1
- package/dist/compiler/grimoire/errors.d.ts +1 -1
- package/dist/compiler/grimoire/errors.d.ts.map +1 -1
- package/dist/compiler/grimoire/errors.js +2 -2
- package/dist/compiler/grimoire/errors.js.map +1 -1
- package/dist/compiler/grimoire/index.d.ts.map +1 -1
- package/dist/compiler/grimoire/index.js +8 -1
- package/dist/compiler/grimoire/index.js.map +1 -1
- package/dist/compiler/grimoire/parser.d.ts +3 -0
- package/dist/compiler/grimoire/parser.d.ts.map +1 -1
- package/dist/compiler/grimoire/parser.js +106 -25
- package/dist/compiler/grimoire/parser.js.map +1 -1
- package/dist/compiler/grimoire/transformer.d.ts +0 -1
- package/dist/compiler/grimoire/transformer.d.ts.map +1 -1
- package/dist/compiler/grimoire/transformer.js +57 -100
- package/dist/compiler/grimoire/transformer.js.map +1 -1
- package/dist/compiler/index.d.ts +1 -1
- package/dist/compiler/index.d.ts.map +1 -1
- package/dist/compiler/index.js +5 -1
- package/dist/compiler/index.js.map +1 -1
- package/dist/compiler/ir-generator.js +33 -0
- package/dist/compiler/ir-generator.js.map +1 -1
- package/dist/compiler/type-checker.d.ts +2 -2
- package/dist/compiler/type-checker.js +2 -2
- package/dist/compiler/validator.d.ts +8 -0
- package/dist/compiler/validator.d.ts.map +1 -1
- package/dist/compiler/validator.js +223 -0
- package/dist/compiler/validator.js.map +1 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/runtime/context.d.ts +3 -1
- package/dist/runtime/context.d.ts.map +1 -1
- package/dist/runtime/context.js +26 -5
- package/dist/runtime/context.js.map +1 -1
- package/dist/runtime/index.d.ts +3 -1
- package/dist/runtime/index.d.ts.map +1 -1
- package/dist/runtime/index.js +3 -1
- package/dist/runtime/index.js.map +1 -1
- package/dist/runtime/interpreter.d.ts +53 -3
- package/dist/runtime/interpreter.d.ts.map +1 -1
- package/dist/runtime/interpreter.js +777 -132
- package/dist/runtime/interpreter.js.map +1 -1
- package/dist/runtime/session-views.d.ts +37 -0
- package/dist/runtime/session-views.d.ts.map +1 -0
- package/dist/runtime/session-views.js +97 -0
- package/dist/runtime/session-views.js.map +1 -0
- package/dist/runtime/session.d.ts +35 -0
- package/dist/runtime/session.d.ts.map +1 -0
- package/dist/runtime/session.js +57 -0
- package/dist/runtime/session.js.map +1 -0
- package/dist/runtime/steps/action.d.ts +22 -0
- package/dist/runtime/steps/action.d.ts.map +1 -1
- package/dist/runtime/steps/action.js +209 -0
- package/dist/runtime/steps/action.js.map +1 -1
- package/dist/runtime/steps/advisory.d.ts +1 -0
- package/dist/runtime/steps/advisory.d.ts.map +1 -1
- package/dist/runtime/steps/advisory.js +277 -11
- package/dist/runtime/steps/advisory.js.map +1 -1
- package/dist/runtime/value-flow.d.ts +27 -0
- package/dist/runtime/value-flow.d.ts.map +1 -0
- package/dist/runtime/value-flow.js +566 -0
- package/dist/runtime/value-flow.js.map +1 -0
- package/dist/types/execution.d.ts +67 -1
- package/dist/types/execution.d.ts.map +1 -1
- package/dist/types/index.d.ts +2 -1
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/receipt.d.ts +193 -0
- package/dist/types/receipt.d.ts.map +1 -0
- package/dist/types/receipt.js +5 -0
- package/dist/types/receipt.js.map +1 -0
- package/dist/types/steps.d.ts +6 -0
- package/dist/types/steps.d.ts.map +1 -1
- package/dist/wallet/tx-builder.js +3 -3
- package/dist/wallet/tx-builder.js.map +1 -1
- package/package.json +1 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Spell Interpreter
|
|
3
|
-
* Executes compiled SpellIR
|
|
3
|
+
* Executes compiled SpellIR via the preview/commit model.
|
|
4
4
|
*/
|
|
5
5
|
import { createExecutor } from "../wallet/executor.js";
|
|
6
6
|
import { createProvider } from "../wallet/provider.js";
|
|
@@ -8,8 +8,9 @@ import { CircuitBreakerManager } from "./circuit-breaker.js";
|
|
|
8
8
|
import { InMemoryLedger, createContext, getPersistentStateObject, incrementAdvisoryCalls, markStepExecuted, } from "./context.js";
|
|
9
9
|
import { createEvalContext, evaluateAsync } from "./expression-evaluator.js";
|
|
10
10
|
import { resolveAdvisorSkill } from "./skills/registry.js";
|
|
11
|
+
import { evaluatePreviewValueFlow, inferDriftClass, } from "./value-flow.js";
|
|
11
12
|
// Step executors
|
|
12
|
-
import { executeActionStep } from "./steps/action.js";
|
|
13
|
+
import { commitActionStep, executeActionStep, previewActionStep, } from "./steps/action.js";
|
|
13
14
|
import { executeAdvisoryStep } from "./steps/advisory.js";
|
|
14
15
|
import { executeComputeStep } from "./steps/compute.js";
|
|
15
16
|
import { executeConditionalStep } from "./steps/conditional.js";
|
|
@@ -20,29 +21,36 @@ import { executeParallelStep } from "./steps/parallel.js";
|
|
|
20
21
|
import { executePipelineStep } from "./steps/pipeline.js";
|
|
21
22
|
import { executeTryStep } from "./steps/try.js";
|
|
22
23
|
import { executeWaitStep } from "./steps/wait.js";
|
|
24
|
+
// Keep preview-issued receipts in-process so commit only accepts known receipts.
|
|
25
|
+
const issuedReceipts = new Map();
|
|
26
|
+
const committedReceipts = new Set();
|
|
23
27
|
/**
|
|
24
|
-
*
|
|
28
|
+
* Preview a spell — runs the full step loop in simulation mode,
|
|
29
|
+
* collects PlannedActions and ValueDeltas, and assembles a Receipt.
|
|
25
30
|
*/
|
|
26
|
-
export async function
|
|
27
|
-
const { spell, vault, chain, params = {}, persistentState = {}
|
|
28
|
-
|
|
29
|
-
const actionExecution =
|
|
30
|
-
// Initialize circuit breaker manager if policy has breakers
|
|
31
|
+
export async function preview(options) {
|
|
32
|
+
const { spell, vault, chain, params = {}, persistentState = {} } = options;
|
|
33
|
+
// Always simulate during preview
|
|
34
|
+
const actionExecution = { mode: "simulate" };
|
|
31
35
|
if (options.policy?.circuitBreakers?.length) {
|
|
32
36
|
actionExecution.circuitBreakerManager = new CircuitBreakerManager(options.policy.circuitBreakers);
|
|
33
37
|
}
|
|
34
|
-
// Create execution context
|
|
35
38
|
const ctx = createContext({
|
|
36
39
|
spell,
|
|
37
40
|
vault,
|
|
38
41
|
chain,
|
|
42
|
+
trigger: options.trigger,
|
|
39
43
|
params,
|
|
40
44
|
persistentState,
|
|
41
45
|
});
|
|
42
46
|
ctx.advisorTooling = buildAdvisorTooling(spell.advisors, options.advisorSkillsDirs);
|
|
43
|
-
|
|
44
|
-
const
|
|
45
|
-
|
|
47
|
+
const ledger = new InMemoryLedger(ctx.runId, spell.id, options.eventCallback);
|
|
48
|
+
const guardResults = [];
|
|
49
|
+
const advisoryResults = [];
|
|
50
|
+
const plannedActions = [];
|
|
51
|
+
const valueDeltas = [];
|
|
52
|
+
const receiptId = `rcpt_${ctx.runId}`;
|
|
53
|
+
ledger.emit({ type: "preview_started", runId: ctx.runId, spellId: spell.id });
|
|
46
54
|
ledger.emit({
|
|
47
55
|
type: "run_started",
|
|
48
56
|
runId: ctx.runId,
|
|
@@ -50,126 +58,631 @@ export async function execute(options) {
|
|
|
50
58
|
trigger: ctx.trigger,
|
|
51
59
|
});
|
|
52
60
|
try {
|
|
53
|
-
// Check pre-execution guards
|
|
54
|
-
const
|
|
55
|
-
|
|
56
|
-
|
|
61
|
+
// Check pre-execution guards and collect results
|
|
62
|
+
const guardCheck = await checkGuards(spell.guards, ctx, ledger);
|
|
63
|
+
collectGuardResults(guardResults, spell.guards, guardCheck);
|
|
64
|
+
if (!guardCheck.success) {
|
|
65
|
+
const structuredError = createStructuredError("preview", "GUARD_FAILED", `Guard failed: ${guardCheck.error}`);
|
|
66
|
+
const receipt = buildReceipt({
|
|
67
|
+
id: receiptId,
|
|
68
|
+
spell,
|
|
69
|
+
ctx,
|
|
70
|
+
guardResults,
|
|
71
|
+
advisoryResults,
|
|
72
|
+
plannedActions,
|
|
73
|
+
valueDeltas,
|
|
74
|
+
status: "rejected",
|
|
75
|
+
error: structuredError.message,
|
|
76
|
+
});
|
|
77
|
+
registerIssuedReceipt(receipt);
|
|
78
|
+
ledger.emit({ type: "receipt_generated", receiptId });
|
|
79
|
+
ledger.emit({ type: "preview_completed", runId: ctx.runId, receiptId, status: "rejected" });
|
|
80
|
+
return {
|
|
81
|
+
success: false,
|
|
82
|
+
receipt,
|
|
83
|
+
error: structuredError,
|
|
84
|
+
ledgerEvents: ledger.getEntries(),
|
|
85
|
+
};
|
|
57
86
|
}
|
|
58
|
-
//
|
|
87
|
+
// Run step loop in preview mode — action steps produce PlannedActions instead of executing
|
|
59
88
|
const stepMap = new Map(spell.steps.map((s) => [s.id, s]));
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
const loc = spell.sourceMap?.[step.id];
|
|
84
|
-
if (loc) {
|
|
85
|
-
enrichStepFailedEvents(ledger, step.id, loc);
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
// Handle halt
|
|
89
|
-
if (result.halted) {
|
|
90
|
-
ledger.emit({
|
|
91
|
-
type: "run_completed",
|
|
92
|
-
runId: ctx.runId,
|
|
93
|
-
success: true,
|
|
94
|
-
metrics: ctx.metrics,
|
|
95
|
-
});
|
|
96
|
-
return {
|
|
97
|
-
success: true,
|
|
98
|
-
runId: ctx.runId,
|
|
99
|
-
startTime: ctx.startTime,
|
|
100
|
-
endTime: Date.now(),
|
|
101
|
-
duration: Date.now() - ctx.startTime,
|
|
102
|
-
metrics: ctx.metrics,
|
|
103
|
-
finalState: getPersistentStateObject(ctx),
|
|
104
|
-
ledgerEvents: ledger.getEntries(),
|
|
105
|
-
};
|
|
106
|
-
}
|
|
107
|
-
// Handle failure
|
|
108
|
-
if (!result.success) {
|
|
109
|
-
const onFailure = "onFailure" in step ? step.onFailure : "revert";
|
|
110
|
-
const loc = spell.sourceMap?.[step.id];
|
|
111
|
-
const locSuffix = loc ? ` at line ${loc.line}, column ${loc.column}` : "";
|
|
112
|
-
switch (onFailure) {
|
|
113
|
-
case "halt":
|
|
114
|
-
throw new Error(`Step '${step.id}' failed${locSuffix}: ${result.error}`);
|
|
115
|
-
case "revert":
|
|
116
|
-
throw new Error(`Step '${step.id}' failed${locSuffix}: ${result.error}`);
|
|
117
|
-
case "skip":
|
|
118
|
-
ledger.emit({
|
|
119
|
-
type: "step_skipped",
|
|
120
|
-
stepId: step.id,
|
|
121
|
-
reason: result.error ?? "Unknown error",
|
|
122
|
-
});
|
|
123
|
-
continue;
|
|
124
|
-
case "catch":
|
|
125
|
-
// Would be handled by try/catch step
|
|
126
|
-
continue;
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
markStepExecuted(ctx, step.id);
|
|
89
|
+
const stepLoopResult = await executeStepLoop(spell, ctx, ledger, stepMap, actionExecution, options.onAdvisory, { isPreview: true, plannedActions, valueDeltas, advisoryResults });
|
|
90
|
+
if (!stepLoopResult.success) {
|
|
91
|
+
const structuredError = createStructuredError("preview", "STEP_FAILED", stepLoopResult.error ?? "Step execution failed");
|
|
92
|
+
const receipt = buildReceipt({
|
|
93
|
+
id: receiptId,
|
|
94
|
+
spell,
|
|
95
|
+
ctx,
|
|
96
|
+
guardResults,
|
|
97
|
+
advisoryResults,
|
|
98
|
+
plannedActions,
|
|
99
|
+
valueDeltas,
|
|
100
|
+
status: "rejected",
|
|
101
|
+
error: structuredError.message,
|
|
102
|
+
});
|
|
103
|
+
registerIssuedReceipt(receipt);
|
|
104
|
+
ledger.emit({ type: "receipt_generated", receiptId });
|
|
105
|
+
ledger.emit({ type: "preview_completed", runId: ctx.runId, receiptId, status: "rejected" });
|
|
106
|
+
return {
|
|
107
|
+
success: false,
|
|
108
|
+
receipt,
|
|
109
|
+
error: structuredError,
|
|
110
|
+
ledgerEvents: ledger.getEntries(),
|
|
111
|
+
};
|
|
130
112
|
}
|
|
131
|
-
//
|
|
132
|
-
const
|
|
133
|
-
if (!
|
|
134
|
-
|
|
113
|
+
// Post-execution guards
|
|
114
|
+
const postGuardCheck = await checkGuards(spell.guards, ctx, ledger);
|
|
115
|
+
if (!postGuardCheck.success && postGuardCheck.severity === "halt") {
|
|
116
|
+
const structuredError = createStructuredError("preview", "POST_GUARD_FAILED", `Post-execution guard failed: ${postGuardCheck.error}`);
|
|
117
|
+
const receipt = buildReceipt({
|
|
118
|
+
id: receiptId,
|
|
119
|
+
spell,
|
|
120
|
+
ctx,
|
|
121
|
+
guardResults,
|
|
122
|
+
advisoryResults,
|
|
123
|
+
plannedActions,
|
|
124
|
+
valueDeltas,
|
|
125
|
+
status: "rejected",
|
|
126
|
+
error: structuredError.message,
|
|
127
|
+
});
|
|
128
|
+
registerIssuedReceipt(receipt);
|
|
129
|
+
ledger.emit({ type: "receipt_generated", receiptId });
|
|
130
|
+
ledger.emit({ type: "preview_completed", runId: ctx.runId, receiptId, status: "rejected" });
|
|
131
|
+
return {
|
|
132
|
+
success: false,
|
|
133
|
+
receipt,
|
|
134
|
+
error: structuredError,
|
|
135
|
+
ledgerEvents: ledger.getEntries(),
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
const valueFlow = evaluatePreviewValueFlow(ctx, plannedActions, valueDeltas);
|
|
139
|
+
if (valueFlow.violation) {
|
|
140
|
+
const structuredError = structuredErrorFromValueFlowViolation("preview", valueFlow.violation);
|
|
141
|
+
const receipt = buildReceipt({
|
|
142
|
+
id: receiptId,
|
|
143
|
+
spell,
|
|
144
|
+
ctx,
|
|
145
|
+
guardResults,
|
|
146
|
+
advisoryResults,
|
|
147
|
+
plannedActions,
|
|
148
|
+
valueDeltas,
|
|
149
|
+
status: "rejected",
|
|
150
|
+
error: structuredError.message,
|
|
151
|
+
constraintResults: valueFlow.constraintResults,
|
|
152
|
+
driftKeys: valueFlow.driftKeys,
|
|
153
|
+
requiresApproval: valueFlow.requiresApproval,
|
|
154
|
+
accounting: valueFlow.accounting,
|
|
155
|
+
});
|
|
156
|
+
registerIssuedReceipt(receipt);
|
|
157
|
+
ledger.emit({ type: "receipt_generated", receiptId });
|
|
158
|
+
ledger.emit({ type: "preview_completed", runId: ctx.runId, receiptId, status: "rejected" });
|
|
159
|
+
return {
|
|
160
|
+
success: false,
|
|
161
|
+
receipt,
|
|
162
|
+
error: structuredError,
|
|
163
|
+
ledgerEvents: ledger.getEntries(),
|
|
164
|
+
};
|
|
135
165
|
}
|
|
136
|
-
|
|
166
|
+
const requiresApproval = valueFlow.requiresApproval;
|
|
167
|
+
const receipt = buildReceipt({
|
|
168
|
+
id: receiptId,
|
|
169
|
+
spell,
|
|
170
|
+
ctx,
|
|
171
|
+
guardResults,
|
|
172
|
+
advisoryResults,
|
|
173
|
+
plannedActions,
|
|
174
|
+
valueDeltas,
|
|
175
|
+
status: "ready",
|
|
176
|
+
constraintResults: valueFlow.constraintResults,
|
|
177
|
+
driftKeys: valueFlow.driftKeys,
|
|
178
|
+
requiresApproval: valueFlow.requiresApproval,
|
|
179
|
+
accounting: valueFlow.accounting,
|
|
180
|
+
});
|
|
181
|
+
registerIssuedReceipt(receipt);
|
|
182
|
+
if (requiresApproval) {
|
|
183
|
+
ledger.emit({
|
|
184
|
+
type: "approval_required",
|
|
185
|
+
receiptId,
|
|
186
|
+
reason: "One or more actions crossed approval_required_above",
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
ledger.emit({ type: "receipt_generated", receiptId });
|
|
137
190
|
ledger.emit({
|
|
138
191
|
type: "run_completed",
|
|
139
192
|
runId: ctx.runId,
|
|
140
193
|
success: true,
|
|
141
194
|
metrics: ctx.metrics,
|
|
142
195
|
});
|
|
196
|
+
ledger.emit({ type: "preview_completed", runId: ctx.runId, receiptId, status: "ready" });
|
|
197
|
+
return { success: true, receipt, ledgerEvents: ledger.getEntries() };
|
|
198
|
+
}
|
|
199
|
+
catch (error) {
|
|
200
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
201
|
+
const structuredError = createStructuredError("preview", "PREVIEW_INTERNAL_ERROR", message);
|
|
202
|
+
ledger.emit({ type: "run_failed", runId: ctx.runId, error: message });
|
|
203
|
+
ledger.emit({ type: "preview_completed", runId: ctx.runId, receiptId, status: "rejected" });
|
|
204
|
+
return { success: false, error: structuredError, ledgerEvents: ledger.getEntries() };
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* Commit a receipt — executes planned actions from the preview.
|
|
209
|
+
*/
|
|
210
|
+
export async function commit(options) {
|
|
211
|
+
const { receipt, wallet } = options;
|
|
212
|
+
const runId = receipt.id.replace("rcpt_", "");
|
|
213
|
+
const ledger = new InMemoryLedger(runId, receipt.spellId, options.eventCallback);
|
|
214
|
+
ledger.emit({ type: "commit_started", runId, receiptId: receipt.id });
|
|
215
|
+
// Validate receipt status
|
|
216
|
+
if (receipt.status !== "ready") {
|
|
217
|
+
const structuredError = createStructuredError("commit", "RECEIPT_INVALID_STATUS", `Receipt status is '${receipt.status}', expected 'ready'`);
|
|
218
|
+
ledger.emit({ type: "commit_completed", runId, receiptId: receipt.id, success: false });
|
|
143
219
|
return {
|
|
144
|
-
success:
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
metrics: ctx.metrics,
|
|
150
|
-
finalState: getPersistentStateObject(ctx),
|
|
220
|
+
success: false,
|
|
221
|
+
receiptId: receipt.id,
|
|
222
|
+
transactions: [],
|
|
223
|
+
driftChecks: [],
|
|
224
|
+
finalState: receipt.finalState,
|
|
151
225
|
ledgerEvents: ledger.getEntries(),
|
|
226
|
+
error: structuredError,
|
|
152
227
|
};
|
|
153
228
|
}
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
ledger.emit({
|
|
157
|
-
type: "run_failed",
|
|
158
|
-
runId: ctx.runId,
|
|
159
|
-
error: message,
|
|
160
|
-
});
|
|
229
|
+
const receiptValidationError = validateCommitReceipt(receipt);
|
|
230
|
+
if (receiptValidationError) {
|
|
231
|
+
ledger.emit({ type: "commit_completed", runId, receiptId: receipt.id, success: false });
|
|
161
232
|
return {
|
|
162
233
|
success: false,
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
error: message,
|
|
168
|
-
metrics: ctx.metrics,
|
|
169
|
-
finalState: getPersistentStateObject(ctx),
|
|
234
|
+
receiptId: receipt.id,
|
|
235
|
+
transactions: [],
|
|
236
|
+
driftChecks: [],
|
|
237
|
+
finalState: receipt.finalState,
|
|
170
238
|
ledgerEvents: ledger.getEntries(),
|
|
239
|
+
error: receiptValidationError,
|
|
171
240
|
};
|
|
172
241
|
}
|
|
242
|
+
// Check receipt age
|
|
243
|
+
if (options.driftPolicy?.maxAge) {
|
|
244
|
+
const ageSec = (Date.now() - receipt.timestamp) / 1000;
|
|
245
|
+
if (ageSec > options.driftPolicy.maxAge) {
|
|
246
|
+
const structuredError = createStructuredError("commit", "RECEIPT_EXPIRED", `Receipt expired: age ${Math.round(ageSec)}s exceeds maxAge ${options.driftPolicy.maxAge}s`, {
|
|
247
|
+
actual: Math.round(ageSec),
|
|
248
|
+
limit: options.driftPolicy.maxAge,
|
|
249
|
+
suggestion: "Run preview again to generate a fresh receipt.",
|
|
250
|
+
});
|
|
251
|
+
ledger.emit({ type: "commit_completed", runId, receiptId: receipt.id, success: false });
|
|
252
|
+
return {
|
|
253
|
+
success: false,
|
|
254
|
+
receiptId: receipt.id,
|
|
255
|
+
transactions: [],
|
|
256
|
+
driftChecks: [],
|
|
257
|
+
finalState: receipt.finalState,
|
|
258
|
+
ledgerEvents: ledger.getEntries(),
|
|
259
|
+
error: structuredError,
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
const driftChecks = [];
|
|
264
|
+
for (const driftKey of receipt.driftKeys) {
|
|
265
|
+
if (options.driftPolicy?.maxAge) {
|
|
266
|
+
const keyAgeSec = Math.max(0, Math.floor((Date.now() - driftKey.timestamp) / 1000));
|
|
267
|
+
if (keyAgeSec > options.driftPolicy.maxAge) {
|
|
268
|
+
const structuredError = createStructuredError("commit", "DRIFT_KEY_STALE", `Drift key '${driftKey.field}' is stale (${keyAgeSec}s > ${options.driftPolicy.maxAge}s)`, {
|
|
269
|
+
constraint: "drift_key_freshness",
|
|
270
|
+
actual: keyAgeSec,
|
|
271
|
+
limit: options.driftPolicy.maxAge,
|
|
272
|
+
path: driftKey.field,
|
|
273
|
+
suggestion: "Run preview again to refresh drift keys.",
|
|
274
|
+
});
|
|
275
|
+
ledger.emit({ type: "commit_completed", runId, receiptId: receipt.id, success: false });
|
|
276
|
+
return {
|
|
277
|
+
success: false,
|
|
278
|
+
receiptId: receipt.id,
|
|
279
|
+
transactions: [],
|
|
280
|
+
driftChecks,
|
|
281
|
+
finalState: receipt.finalState,
|
|
282
|
+
ledgerEvents: ledger.getEntries(),
|
|
283
|
+
error: structuredError,
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
let resolvedValue;
|
|
288
|
+
try {
|
|
289
|
+
resolvedValue = await resolveCommitDriftValue(driftKey, options);
|
|
290
|
+
}
|
|
291
|
+
catch (error) {
|
|
292
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
293
|
+
const structuredError = createStructuredError("commit", "DRIFT_RESOLUTION_FAILED", `Failed to resolve drift value for '${driftKey.field}': ${message}`, {
|
|
294
|
+
constraint: "drift_keys",
|
|
295
|
+
path: driftKey.field,
|
|
296
|
+
});
|
|
297
|
+
ledger.emit({ type: "commit_completed", runId, receiptId: receipt.id, success: false });
|
|
298
|
+
return {
|
|
299
|
+
success: false,
|
|
300
|
+
receiptId: receipt.id,
|
|
301
|
+
transactions: [],
|
|
302
|
+
driftChecks,
|
|
303
|
+
finalState: receipt.finalState,
|
|
304
|
+
ledgerEvents: ledger.getEntries(),
|
|
305
|
+
error: structuredError,
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
if (!resolvedValue.found && options.driftPolicy) {
|
|
309
|
+
const structuredError = createStructuredError("commit", "DRIFT_VALUE_MISSING", `Missing commit-time drift value for '${driftKey.field}'`, {
|
|
310
|
+
constraint: "drift_keys",
|
|
311
|
+
path: driftKey.field,
|
|
312
|
+
suggestion: "Provide driftValues for this key or configure resolveDriftValue to fetch commit-time values.",
|
|
313
|
+
});
|
|
314
|
+
ledger.emit({ type: "commit_completed", runId, receiptId: receipt.id, success: false });
|
|
315
|
+
return {
|
|
316
|
+
success: false,
|
|
317
|
+
receiptId: receipt.id,
|
|
318
|
+
transactions: [],
|
|
319
|
+
driftChecks,
|
|
320
|
+
finalState: receipt.finalState,
|
|
321
|
+
ledgerEvents: ledger.getEntries(),
|
|
322
|
+
error: structuredError,
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
const commitValue = resolvedValue.found ? resolvedValue.value : driftKey.previewValue;
|
|
326
|
+
const driftResult = evaluateDriftKey(driftKey, commitValue, options.driftPolicy);
|
|
327
|
+
driftChecks.push(driftResult);
|
|
328
|
+
ledger.emit({
|
|
329
|
+
type: "drift_check",
|
|
330
|
+
field: driftKey.field,
|
|
331
|
+
passed: driftResult.passed,
|
|
332
|
+
previewValue: driftResult.previewValue,
|
|
333
|
+
commitValue: driftResult.commitValue,
|
|
334
|
+
});
|
|
335
|
+
if (!driftResult.passed) {
|
|
336
|
+
const tolerance = resolveToleranceBps(driftKey, options.driftPolicy);
|
|
337
|
+
const structuredError = createStructuredError("commit", "DRIFT_EXCEEDED", `Drift exceeded for '${driftKey.field}'`, {
|
|
338
|
+
constraint: "drift_policy",
|
|
339
|
+
actual: driftResult.driftBps,
|
|
340
|
+
limit: tolerance,
|
|
341
|
+
path: driftKey.field,
|
|
342
|
+
suggestion: "Run preview again or increase drift tolerance for this key class.",
|
|
343
|
+
});
|
|
344
|
+
ledger.emit({ type: "commit_completed", runId, receiptId: receipt.id, success: false });
|
|
345
|
+
return {
|
|
346
|
+
success: false,
|
|
347
|
+
receiptId: receipt.id,
|
|
348
|
+
transactions: [],
|
|
349
|
+
driftChecks,
|
|
350
|
+
finalState: receipt.finalState,
|
|
351
|
+
ledgerEvents: ledger.getEntries(),
|
|
352
|
+
error: structuredError,
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
// Execute planned actions
|
|
357
|
+
const { chainId } = receipt.chainContext;
|
|
358
|
+
const provider = options.provider ?? createProvider(chainId, options.rpcUrl);
|
|
359
|
+
const executor = createExecutor({
|
|
360
|
+
wallet,
|
|
361
|
+
provider,
|
|
362
|
+
mode: "execute",
|
|
363
|
+
gasMultiplier: options.gasMultiplier,
|
|
364
|
+
confirmCallback: options.confirmCallback,
|
|
365
|
+
progressCallback: options.progressCallback,
|
|
366
|
+
skipTestnetConfirmation: options.skipTestnetConfirmation,
|
|
367
|
+
adapters: options.adapters,
|
|
368
|
+
});
|
|
369
|
+
const transactions = [];
|
|
370
|
+
for (const planned of receipt.plannedActions) {
|
|
371
|
+
try {
|
|
372
|
+
const txResult = await commitActionStep(planned, executor);
|
|
373
|
+
if (txResult.success) {
|
|
374
|
+
transactions.push({
|
|
375
|
+
stepId: planned.stepId,
|
|
376
|
+
hash: txResult.hash,
|
|
377
|
+
gasUsed: txResult.gasUsed,
|
|
378
|
+
success: true,
|
|
379
|
+
});
|
|
380
|
+
if (txResult.hash) {
|
|
381
|
+
ledger.emit({
|
|
382
|
+
type: "action_submitted",
|
|
383
|
+
action: planned.action,
|
|
384
|
+
txHash: txResult.hash,
|
|
385
|
+
});
|
|
386
|
+
}
|
|
387
|
+
if (txResult.gasUsed !== undefined) {
|
|
388
|
+
ledger.emit({
|
|
389
|
+
type: "action_confirmed",
|
|
390
|
+
txHash: txResult.hash ?? "",
|
|
391
|
+
gasUsed: txResult.gasUsed.toString(),
|
|
392
|
+
});
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
else {
|
|
396
|
+
transactions.push({
|
|
397
|
+
stepId: planned.stepId,
|
|
398
|
+
hash: txResult.hash,
|
|
399
|
+
success: false,
|
|
400
|
+
error: txResult.error,
|
|
401
|
+
});
|
|
402
|
+
if (planned.onFailure === "skip") {
|
|
403
|
+
ledger.emit({
|
|
404
|
+
type: "step_skipped",
|
|
405
|
+
stepId: planned.stepId,
|
|
406
|
+
reason: txResult.error ?? "Action execution failed",
|
|
407
|
+
});
|
|
408
|
+
continue;
|
|
409
|
+
}
|
|
410
|
+
const structuredError = createStructuredError("commit", "ACTION_COMMIT_FAILED", `Action step '${planned.stepId}' failed: ${txResult.error}`);
|
|
411
|
+
ledger.emit({ type: "commit_completed", runId, receiptId: receipt.id, success: false });
|
|
412
|
+
return {
|
|
413
|
+
success: false,
|
|
414
|
+
receiptId: receipt.id,
|
|
415
|
+
transactions,
|
|
416
|
+
driftChecks,
|
|
417
|
+
finalState: receipt.finalState,
|
|
418
|
+
ledgerEvents: ledger.getEntries(),
|
|
419
|
+
error: structuredError,
|
|
420
|
+
};
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
catch (error) {
|
|
424
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
425
|
+
transactions.push({ stepId: planned.stepId, success: false, error: message });
|
|
426
|
+
const structuredError = createStructuredError("commit", "COMMIT_INTERNAL_ERROR", message);
|
|
427
|
+
ledger.emit({ type: "commit_completed", runId, receiptId: receipt.id, success: false });
|
|
428
|
+
return {
|
|
429
|
+
success: false,
|
|
430
|
+
receiptId: receipt.id,
|
|
431
|
+
transactions,
|
|
432
|
+
driftChecks,
|
|
433
|
+
finalState: receipt.finalState,
|
|
434
|
+
ledgerEvents: ledger.getEntries(),
|
|
435
|
+
error: structuredError,
|
|
436
|
+
};
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
committedReceipts.add(receipt.id);
|
|
440
|
+
ledger.emit({ type: "commit_completed", runId, receiptId: receipt.id, success: true });
|
|
441
|
+
return {
|
|
442
|
+
success: true,
|
|
443
|
+
receiptId: receipt.id,
|
|
444
|
+
transactions,
|
|
445
|
+
driftChecks,
|
|
446
|
+
finalState: receipt.finalState,
|
|
447
|
+
ledgerEvents: ledger.getEntries(),
|
|
448
|
+
};
|
|
449
|
+
}
|
|
450
|
+
// =============================================================================
|
|
451
|
+
// EXECUTE (backward-compatible wrapper)
|
|
452
|
+
// =============================================================================
|
|
453
|
+
/**
|
|
454
|
+
* Execute a compiled spell (backward-compatible wrapper).
|
|
455
|
+
* Internally uses preview() and, if needed, commit().
|
|
456
|
+
*/
|
|
457
|
+
export async function execute(options) {
|
|
458
|
+
const { spell, vault, chain, params = {}, persistentState = {}, simulate = false } = options;
|
|
459
|
+
const actionMode = resolveExecutionMode(options, simulate);
|
|
460
|
+
const previewResult = await preview({
|
|
461
|
+
spell,
|
|
462
|
+
vault,
|
|
463
|
+
chain,
|
|
464
|
+
params,
|
|
465
|
+
persistentState,
|
|
466
|
+
trigger: options.trigger,
|
|
467
|
+
adapters: options.adapters,
|
|
468
|
+
policy: options.policy,
|
|
469
|
+
advisorSkillsDirs: options.advisorSkillsDirs,
|
|
470
|
+
onAdvisory: options.onAdvisory,
|
|
471
|
+
progressCallback: options.progressCallback,
|
|
472
|
+
eventCallback: options.eventCallback,
|
|
473
|
+
});
|
|
474
|
+
if (!previewResult.success || !previewResult.receipt) {
|
|
475
|
+
return convertPreviewToExecutionResult(previewResult, spell);
|
|
476
|
+
}
|
|
477
|
+
const receipt = previewResult.receipt;
|
|
478
|
+
// Preview-only execution for simulate/dry-run/no-wallet and compute-only spells.
|
|
479
|
+
if (actionMode === "simulate" ||
|
|
480
|
+
actionMode === "dry-run" ||
|
|
481
|
+
!options.wallet ||
|
|
482
|
+
receipt.plannedActions.length === 0) {
|
|
483
|
+
return convertPreviewToExecutionResult(previewResult, spell);
|
|
484
|
+
}
|
|
485
|
+
const commitResult = await commit({
|
|
486
|
+
receipt,
|
|
487
|
+
wallet: options.wallet,
|
|
488
|
+
provider: options.provider,
|
|
489
|
+
rpcUrl: options.rpcUrl,
|
|
490
|
+
gasMultiplier: options.gasMultiplier,
|
|
491
|
+
adapters: options.adapters,
|
|
492
|
+
confirmCallback: options.confirmCallback,
|
|
493
|
+
progressCallback: options.progressCallback,
|
|
494
|
+
skipTestnetConfirmation: options.skipTestnetConfirmation,
|
|
495
|
+
eventCallback: options.eventCallback,
|
|
496
|
+
});
|
|
497
|
+
return convertPreviewCommitToExecutionResult(previewResult, commitResult);
|
|
498
|
+
}
|
|
499
|
+
/**
|
|
500
|
+
* Shared step execution loop used by both preview() and execute().
|
|
501
|
+
*/
|
|
502
|
+
async function executeStepLoop(spell, ctx, ledger, stepMap, actionExecution, advisoryHandler, collectors) {
|
|
503
|
+
for (const step of spell.steps) {
|
|
504
|
+
if (ctx.executedSteps.includes(step.id)) {
|
|
505
|
+
continue;
|
|
506
|
+
}
|
|
507
|
+
for (const depId of step.dependsOn) {
|
|
508
|
+
if (!ctx.executedSteps.includes(depId)) {
|
|
509
|
+
return {
|
|
510
|
+
success: false,
|
|
511
|
+
error: `Step '${step.id}' depends on '${depId}' which has not been executed`,
|
|
512
|
+
};
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
// In preview mode, action steps go through previewActionStep
|
|
516
|
+
let result;
|
|
517
|
+
if (collectors?.isPreview && step.kind === "action") {
|
|
518
|
+
const previewResult = await previewActionStep(step, ctx, ledger, actionExecution);
|
|
519
|
+
result = previewResult.stepResult;
|
|
520
|
+
if (previewResult.plannedAction) {
|
|
521
|
+
collectors.plannedActions?.push(previewResult.plannedAction);
|
|
522
|
+
}
|
|
523
|
+
if (previewResult.valueDeltas?.length) {
|
|
524
|
+
collectors.valueDeltas?.push(...previewResult.valueDeltas);
|
|
525
|
+
for (const delta of previewResult.valueDeltas) {
|
|
526
|
+
ledger.emit({ type: "value_delta", delta });
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
else if (collectors?.isPreview && step.kind === "advisory") {
|
|
531
|
+
result = await executeAdvisoryStep(step, ctx, ledger, advisoryHandler);
|
|
532
|
+
if (result.success) {
|
|
533
|
+
collectors.advisoryResults?.push({
|
|
534
|
+
stepId: step.id,
|
|
535
|
+
advisor: step.advisor,
|
|
536
|
+
output: result.output,
|
|
537
|
+
fallback: result.fallback ?? false,
|
|
538
|
+
rawOutput: result.rawOutput,
|
|
539
|
+
effectiveOutput: result.effectiveOutput ?? result.output,
|
|
540
|
+
onViolation: result.violationPolicy ?? step.violationPolicy ?? "reject",
|
|
541
|
+
policyScope: step.policyScope,
|
|
542
|
+
clampConstraints: step.clampConstraints,
|
|
543
|
+
clamped: result.clamped ?? false,
|
|
544
|
+
violations: result.advisoryViolations,
|
|
545
|
+
});
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
else {
|
|
549
|
+
result = await executeStep(step, ctx, ledger, stepMap, actionExecution, advisoryHandler);
|
|
550
|
+
}
|
|
551
|
+
for (const childId of getChildStepIds(step)) {
|
|
552
|
+
if (!ctx.executedSteps.includes(childId)) {
|
|
553
|
+
markStepExecuted(ctx, childId);
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
if (!result.success) {
|
|
557
|
+
const loc = spell.sourceMap?.[step.id];
|
|
558
|
+
if (loc) {
|
|
559
|
+
enrichStepFailedEvents(ledger, step.id, loc);
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
if (result.halted) {
|
|
563
|
+
return { success: true, halted: true };
|
|
564
|
+
}
|
|
565
|
+
if (!result.success) {
|
|
566
|
+
const onFailure = "onFailure" in step ? step.onFailure : "revert";
|
|
567
|
+
const loc = spell.sourceMap?.[step.id];
|
|
568
|
+
const locSuffix = loc ? ` at line ${loc.line}, column ${loc.column}` : "";
|
|
569
|
+
switch (onFailure) {
|
|
570
|
+
case "halt":
|
|
571
|
+
return { success: false, error: `Step '${step.id}' failed${locSuffix}: ${result.error}` };
|
|
572
|
+
case "revert":
|
|
573
|
+
return { success: false, error: `Step '${step.id}' failed${locSuffix}: ${result.error}` };
|
|
574
|
+
case "skip":
|
|
575
|
+
ledger.emit({
|
|
576
|
+
type: "step_skipped",
|
|
577
|
+
stepId: step.id,
|
|
578
|
+
reason: result.error ?? "Unknown error",
|
|
579
|
+
});
|
|
580
|
+
continue;
|
|
581
|
+
case "catch":
|
|
582
|
+
continue;
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
markStepExecuted(ctx, step.id);
|
|
586
|
+
}
|
|
587
|
+
return { success: true };
|
|
588
|
+
}
|
|
589
|
+
// =============================================================================
|
|
590
|
+
// HELPERS
|
|
591
|
+
// =============================================================================
|
|
592
|
+
function convertPreviewToExecutionResult(previewResult, _spell) {
|
|
593
|
+
const receipt = previewResult.receipt;
|
|
594
|
+
const now = Date.now();
|
|
595
|
+
const startTime = receipt?.timestamp ?? now;
|
|
596
|
+
return {
|
|
597
|
+
success: previewResult.success,
|
|
598
|
+
runId: receipt?.id.replace("rcpt_", "") ?? `preview_${now}`,
|
|
599
|
+
startTime,
|
|
600
|
+
endTime: now,
|
|
601
|
+
duration: now - startTime,
|
|
602
|
+
error: previewResult.error ? formatStructuredError(previewResult.error) : undefined,
|
|
603
|
+
structuredError: previewResult.error,
|
|
604
|
+
metrics: receipt?.metrics ?? {
|
|
605
|
+
stepsExecuted: 0,
|
|
606
|
+
actionsExecuted: 0,
|
|
607
|
+
gasUsed: 0n,
|
|
608
|
+
advisoryCalls: 0,
|
|
609
|
+
errors: 0,
|
|
610
|
+
retries: 0,
|
|
611
|
+
},
|
|
612
|
+
finalState: receipt?.finalState ?? {},
|
|
613
|
+
ledgerEvents: previewResult.ledgerEvents,
|
|
614
|
+
receipt,
|
|
615
|
+
};
|
|
616
|
+
}
|
|
617
|
+
function convertPreviewCommitToExecutionResult(previewResult, commitResult) {
|
|
618
|
+
const receipt = previewResult.receipt;
|
|
619
|
+
const endTime = Date.now();
|
|
620
|
+
const startTime = receipt?.timestamp ?? endTime;
|
|
621
|
+
const txGasUsed = commitResult.transactions.reduce((sum, tx) => sum + (tx.gasUsed ?? 0n), 0n);
|
|
622
|
+
const baseMetrics = receipt?.metrics ?? {
|
|
623
|
+
stepsExecuted: 0,
|
|
624
|
+
actionsExecuted: 0,
|
|
625
|
+
gasUsed: 0n,
|
|
626
|
+
advisoryCalls: 0,
|
|
627
|
+
errors: 0,
|
|
628
|
+
retries: 0,
|
|
629
|
+
};
|
|
630
|
+
return {
|
|
631
|
+
success: commitResult.success,
|
|
632
|
+
runId: receipt?.id.replace("rcpt_", "") ?? `commit_${endTime}`,
|
|
633
|
+
startTime,
|
|
634
|
+
endTime,
|
|
635
|
+
duration: endTime - startTime,
|
|
636
|
+
error: commitResult.error ? formatStructuredError(commitResult.error) : undefined,
|
|
637
|
+
structuredError: commitResult.error,
|
|
638
|
+
metrics: {
|
|
639
|
+
...baseMetrics,
|
|
640
|
+
gasUsed: baseMetrics.gasUsed + txGasUsed,
|
|
641
|
+
},
|
|
642
|
+
finalState: commitResult.finalState,
|
|
643
|
+
ledgerEvents: [...previewResult.ledgerEvents, ...commitResult.ledgerEvents],
|
|
644
|
+
receipt,
|
|
645
|
+
commit: commitResult,
|
|
646
|
+
};
|
|
647
|
+
}
|
|
648
|
+
function buildReceipt(opts) {
|
|
649
|
+
return {
|
|
650
|
+
id: opts.id,
|
|
651
|
+
spellId: opts.spell.id,
|
|
652
|
+
phase: "preview",
|
|
653
|
+
timestamp: Date.now(),
|
|
654
|
+
chainContext: {
|
|
655
|
+
chainId: opts.ctx.chain,
|
|
656
|
+
vault: opts.ctx.vault,
|
|
657
|
+
},
|
|
658
|
+
guardResults: opts.guardResults,
|
|
659
|
+
advisoryResults: opts.advisoryResults,
|
|
660
|
+
plannedActions: opts.plannedActions,
|
|
661
|
+
valueDeltas: opts.valueDeltas,
|
|
662
|
+
accounting: opts.accounting ??
|
|
663
|
+
{
|
|
664
|
+
assets: [],
|
|
665
|
+
totalUnaccounted: 0n,
|
|
666
|
+
passed: true,
|
|
667
|
+
},
|
|
668
|
+
constraintResults: opts.constraintResults ?? [],
|
|
669
|
+
driftKeys: opts.driftKeys ?? [],
|
|
670
|
+
requiresApproval: opts.requiresApproval ?? false,
|
|
671
|
+
status: opts.status,
|
|
672
|
+
metrics: { ...opts.ctx.metrics },
|
|
673
|
+
finalState: getPersistentStateObject(opts.ctx),
|
|
674
|
+
error: opts.error,
|
|
675
|
+
};
|
|
676
|
+
}
|
|
677
|
+
function collectGuardResults(guardResults, guards, check) {
|
|
678
|
+
for (const guard of guards) {
|
|
679
|
+
guardResults.push({
|
|
680
|
+
guardId: guard.id,
|
|
681
|
+
passed: check.success,
|
|
682
|
+
severity: check.severity ?? ("severity" in guard ? String(guard.severity) : "warn"),
|
|
683
|
+
message: check.success ? undefined : check.error,
|
|
684
|
+
});
|
|
685
|
+
}
|
|
173
686
|
}
|
|
174
687
|
function resolveExecutionMode(options, simulate) {
|
|
175
688
|
if (options.executionMode) {
|
|
@@ -183,25 +696,159 @@ function resolveExecutionMode(options, simulate) {
|
|
|
183
696
|
}
|
|
184
697
|
return "simulate";
|
|
185
698
|
}
|
|
186
|
-
function
|
|
187
|
-
|
|
188
|
-
|
|
699
|
+
function createStructuredError(phase, code, message, extras) {
|
|
700
|
+
return {
|
|
701
|
+
phase,
|
|
702
|
+
code,
|
|
703
|
+
message,
|
|
704
|
+
...extras,
|
|
705
|
+
};
|
|
706
|
+
}
|
|
707
|
+
function structuredErrorFromValueFlowViolation(phase, violation) {
|
|
708
|
+
return createStructuredError(phase, violation.code, violation.message, {
|
|
709
|
+
constraint: violation.constraint,
|
|
710
|
+
actual: violation.actual,
|
|
711
|
+
limit: violation.limit,
|
|
712
|
+
path: violation.path,
|
|
713
|
+
suggestion: violation.suggestion,
|
|
714
|
+
});
|
|
715
|
+
}
|
|
716
|
+
function formatStructuredError(error) {
|
|
717
|
+
return `[${error.code}] ${error.message}`;
|
|
718
|
+
}
|
|
719
|
+
function registerIssuedReceipt(receipt) {
|
|
720
|
+
issuedReceipts.set(receipt.id, {
|
|
721
|
+
spellId: receipt.spellId,
|
|
722
|
+
chainId: receipt.chainContext.chainId,
|
|
723
|
+
vault: receipt.chainContext.vault,
|
|
724
|
+
timestamp: receipt.timestamp,
|
|
725
|
+
});
|
|
726
|
+
}
|
|
727
|
+
function validateCommitReceipt(receipt) {
|
|
728
|
+
if (receipt.phase !== "preview") {
|
|
729
|
+
return createStructuredError("commit", "RECEIPT_INVALID_PHASE", `Receipt phase is '${receipt.phase}', expected 'preview'`);
|
|
189
730
|
}
|
|
190
|
-
if (!
|
|
191
|
-
|
|
731
|
+
if (!receipt.id.startsWith("rcpt_")) {
|
|
732
|
+
return createStructuredError("commit", "RECEIPT_INVALID_ID", "Receipt ID must start with 'rcpt_'");
|
|
192
733
|
}
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
734
|
+
if (committedReceipts.has(receipt.id)) {
|
|
735
|
+
return createStructuredError("commit", "RECEIPT_ALREADY_COMMITTED", "Receipt has already been committed.");
|
|
736
|
+
}
|
|
737
|
+
const issuedReceipt = issuedReceipts.get(receipt.id);
|
|
738
|
+
if (!issuedReceipt) {
|
|
739
|
+
return createStructuredError("commit", "PREVIEW_RECEIPT_UNKNOWN", "Commit requires a valid preview receipt generated by this runtime.");
|
|
740
|
+
}
|
|
741
|
+
if (issuedReceipt.spellId !== receipt.spellId ||
|
|
742
|
+
issuedReceipt.chainId !== receipt.chainContext.chainId ||
|
|
743
|
+
issuedReceipt.vault !== receipt.chainContext.vault ||
|
|
744
|
+
issuedReceipt.timestamp !== receipt.timestamp) {
|
|
745
|
+
return createStructuredError("commit", "PREVIEW_RECEIPT_TAMPERED", "Receipt identity does not match the preview-generated artifact.");
|
|
746
|
+
}
|
|
747
|
+
return undefined;
|
|
748
|
+
}
|
|
749
|
+
async function resolveCommitDriftValue(driftKey, options) {
|
|
750
|
+
if (options.driftValues &&
|
|
751
|
+
Object.prototype.hasOwnProperty.call(options.driftValues, driftKey.field)) {
|
|
752
|
+
return { found: true, value: options.driftValues[driftKey.field] };
|
|
753
|
+
}
|
|
754
|
+
if (options.resolveDriftValue) {
|
|
755
|
+
const value = await options.resolveDriftValue(driftKey);
|
|
756
|
+
if (value !== undefined) {
|
|
757
|
+
return { found: true, value };
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
return { found: false, value: undefined };
|
|
761
|
+
}
|
|
762
|
+
function evaluateDriftKey(driftKey, commitValue, policy) {
|
|
763
|
+
const tolerance = resolveToleranceBps(driftKey, policy);
|
|
764
|
+
const numericPreview = toNumeric(driftKey.previewValue);
|
|
765
|
+
const numericCommit = toNumeric(commitValue);
|
|
766
|
+
if (tolerance !== undefined && numericPreview !== undefined && numericCommit !== undefined) {
|
|
767
|
+
const driftBps = computeDriftBps(numericPreview, numericCommit);
|
|
768
|
+
return {
|
|
769
|
+
field: driftKey.field,
|
|
770
|
+
passed: driftBps <= tolerance,
|
|
771
|
+
previewValue: driftKey.previewValue,
|
|
772
|
+
commitValue,
|
|
773
|
+
driftBps,
|
|
774
|
+
};
|
|
775
|
+
}
|
|
776
|
+
if (tolerance !== undefined) {
|
|
777
|
+
return {
|
|
778
|
+
field: driftKey.field,
|
|
779
|
+
passed: false,
|
|
780
|
+
previewValue: driftKey.previewValue,
|
|
781
|
+
commitValue,
|
|
782
|
+
};
|
|
783
|
+
}
|
|
784
|
+
return {
|
|
785
|
+
field: driftKey.field,
|
|
786
|
+
passed: valuesEquivalent(driftKey.previewValue, commitValue),
|
|
787
|
+
previewValue: driftKey.previewValue,
|
|
788
|
+
commitValue,
|
|
789
|
+
};
|
|
790
|
+
}
|
|
791
|
+
function resolveToleranceBps(driftKey, policy) {
|
|
792
|
+
if (!policy)
|
|
793
|
+
return undefined;
|
|
794
|
+
const driftClass = driftKey.class ?? inferDriftClass(driftKey.field);
|
|
795
|
+
switch (driftClass) {
|
|
796
|
+
case "balance":
|
|
797
|
+
return policy.balance?.toleranceBps;
|
|
798
|
+
case "quote":
|
|
799
|
+
return policy.quote?.toleranceBps;
|
|
800
|
+
case "rate":
|
|
801
|
+
return policy.rate?.toleranceBps;
|
|
802
|
+
case "gas":
|
|
803
|
+
return policy.gas?.toleranceBps;
|
|
804
|
+
default:
|
|
805
|
+
return undefined;
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
function toNumeric(value) {
|
|
809
|
+
if (typeof value === "bigint") {
|
|
810
|
+
return value;
|
|
811
|
+
}
|
|
812
|
+
if (typeof value === "number") {
|
|
813
|
+
if (!Number.isFinite(value))
|
|
814
|
+
return undefined;
|
|
815
|
+
return BigInt(Math.trunc(value));
|
|
816
|
+
}
|
|
817
|
+
if (typeof value === "string") {
|
|
818
|
+
const trimmed = value.trim();
|
|
819
|
+
if (!/^[-+]?\d+$/.test(trimmed))
|
|
820
|
+
return undefined;
|
|
821
|
+
try {
|
|
822
|
+
return BigInt(trimmed);
|
|
823
|
+
}
|
|
824
|
+
catch {
|
|
825
|
+
return undefined;
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
return undefined;
|
|
829
|
+
}
|
|
830
|
+
function computeDriftBps(preview, commit) {
|
|
831
|
+
const delta = preview >= commit ? preview - commit : commit - preview;
|
|
832
|
+
if (preview === 0n) {
|
|
833
|
+
return delta === 0n ? 0 : 10_000;
|
|
834
|
+
}
|
|
835
|
+
return Number((delta * 10000n) / (preview >= 0n ? preview : -preview));
|
|
836
|
+
}
|
|
837
|
+
function valuesEquivalent(left, right) {
|
|
838
|
+
if (typeof left === "bigint" || typeof right === "bigint") {
|
|
839
|
+
const leftBigint = toNumeric(left);
|
|
840
|
+
const rightBigint = toNumeric(right);
|
|
841
|
+
if (leftBigint === undefined || rightBigint === undefined)
|
|
842
|
+
return false;
|
|
843
|
+
return leftBigint === rightBigint;
|
|
844
|
+
}
|
|
845
|
+
if (typeof left === "number" && typeof right === "number") {
|
|
846
|
+
return Number.isFinite(left) && Number.isFinite(right) && left === right;
|
|
847
|
+
}
|
|
848
|
+
if (typeof left === "string" && typeof right === "string") {
|
|
849
|
+
return left === right;
|
|
850
|
+
}
|
|
851
|
+
return Object.is(left, right);
|
|
205
852
|
}
|
|
206
853
|
function buildAdvisorTooling(advisors, searchDirs) {
|
|
207
854
|
if (!advisors.length)
|
|
@@ -397,8 +1044,6 @@ async function checkGuards(guards, ctx, ledger) {
|
|
|
397
1044
|
}
|
|
398
1045
|
/**
|
|
399
1046
|
* Enrich step_failed ledger events with source location info.
|
|
400
|
-
* Scans recent events to find step_failed events for the given stepId
|
|
401
|
-
* and adds line/column from the source map.
|
|
402
1047
|
*/
|
|
403
1048
|
function enrichStepFailedEvents(ledger, stepId, loc) {
|
|
404
1049
|
const entries = ledger.getEntries();
|