@crediolabs/policy-synth 0.1.4 → 0.1.6

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 (154) hide show
  1. package/LICENSE +21 -0
  2. package/dist/codegen/template.js +3 -3
  3. package/dist/record/decode.d.ts +6 -3
  4. package/dist/record/decode.js +56 -3
  5. package/dist/record/freshness.d.ts +14 -1
  6. package/dist/record/freshness.js +32 -2
  7. package/dist/record/index.d.ts +11 -0
  8. package/dist/record/index.js +25 -0
  9. package/dist/record/movements.d.ts +15 -3
  10. package/dist/record/movements.js +42 -7
  11. package/dist/run/index.d.ts +43 -0
  12. package/dist/run/index.js +222 -0
  13. package/dist/run/schemas.d.ts +2016 -0
  14. package/dist/run/schemas.js +223 -0
  15. package/dist/synth/address.d.ts +7 -0
  16. package/dist/synth/address.js +12 -0
  17. package/dist/synth/compose-from-recording.d.ts +4 -3
  18. package/dist/synth/compose-from-recording.js +16 -6
  19. package/dist/synth/deny-cases.d.ts +12 -2
  20. package/dist/synth/deny-cases.js +76 -4
  21. package/dist/synth/evaluate.js +2 -2
  22. package/dist/synth/index.d.ts +1 -0
  23. package/dist/synth/index.js +4 -0
  24. package/dist/synth/minimize.d.ts +1 -1
  25. package/dist/synth/minimize.js +3 -3
  26. package/dist/synth/synthesize-from-recording.d.ts +5 -2
  27. package/dist/synth/synthesize-from-recording.js +175 -69
  28. package/dist/types.d.ts +41 -1
  29. package/dist/types.js +17 -0
  30. package/dist/verify/simulate.js +18 -2
  31. package/dist/verify/verify.js +18 -2
  32. package/dist-cjs/adapters/interpreter/adapter.d.ts +38 -0
  33. package/dist-cjs/adapters/interpreter/adapter.js +527 -0
  34. package/dist-cjs/adapters/interpreter/index.d.ts +1 -0
  35. package/dist-cjs/adapters/interpreter/index.js +8 -0
  36. package/dist-cjs/adapters/oz/adapter.d.ts +20 -0
  37. package/dist-cjs/adapters/oz/adapter.js +289 -0
  38. package/dist-cjs/adapters/oz/index.d.ts +1 -0
  39. package/dist-cjs/adapters/oz/index.js +8 -0
  40. package/dist-cjs/codegen/compile-gate.d.ts +33 -0
  41. package/dist-cjs/codegen/compile-gate.js +123 -0
  42. package/dist-cjs/codegen/index.d.ts +2 -0
  43. package/dist-cjs/codegen/index.js +14 -0
  44. package/dist-cjs/codegen/template.d.ts +18 -0
  45. package/dist-cjs/codegen/template.js +134 -0
  46. package/dist-cjs/errors.d.ts +37 -0
  47. package/dist-cjs/errors.js +3 -0
  48. package/dist-cjs/index.d.ts +11 -0
  49. package/dist-cjs/index.js +27 -0
  50. package/dist-cjs/ir/index.d.ts +1 -0
  51. package/dist-cjs/ir/index.js +3 -0
  52. package/dist-cjs/ir/types.d.ts +108 -0
  53. package/dist-cjs/ir/types.js +12 -0
  54. package/dist-cjs/mandate/index.d.ts +2 -0
  55. package/dist-cjs/mandate/index.js +6 -0
  56. package/dist-cjs/mandate/to-ir.d.ts +3 -0
  57. package/dist-cjs/mandate/to-ir.js +63 -0
  58. package/dist-cjs/mandate/types.d.ts +20 -0
  59. package/dist-cjs/mandate/types.js +9 -0
  60. package/dist-cjs/package.json +3 -0
  61. package/dist-cjs/predicate/encode.d.ts +10 -0
  62. package/dist-cjs/predicate/encode.js +252 -0
  63. package/dist-cjs/predicate/index.d.ts +1 -0
  64. package/dist-cjs/predicate/index.js +6 -0
  65. package/dist-cjs/record/decode.d.ts +79 -0
  66. package/dist-cjs/record/decode.js +441 -0
  67. package/dist-cjs/record/freshness.d.ts +30 -0
  68. package/dist-cjs/record/freshness.js +85 -0
  69. package/dist-cjs/record/index.d.ts +32 -0
  70. package/dist-cjs/record/index.js +191 -0
  71. package/dist-cjs/record/movements.d.ts +32 -0
  72. package/dist-cjs/record/movements.js +227 -0
  73. package/dist-cjs/record/rpc.d.ts +22 -0
  74. package/dist-cjs/record/rpc.js +74 -0
  75. package/dist-cjs/record/validate.d.ts +22 -0
  76. package/dist-cjs/record/validate.js +63 -0
  77. package/dist-cjs/registry/identify.d.ts +11 -0
  78. package/dist-cjs/registry/identify.js +87 -0
  79. package/dist-cjs/registry/index.d.ts +3 -0
  80. package/dist-cjs/registry/index.js +15 -0
  81. package/dist-cjs/registry/known-addresses.d.ts +16 -0
  82. package/dist-cjs/registry/known-addresses.js +53 -0
  83. package/dist-cjs/registry/protocols.d.ts +38 -0
  84. package/dist-cjs/registry/protocols.js +153 -0
  85. package/dist-cjs/review-card/builder.d.ts +14 -0
  86. package/dist-cjs/review-card/builder.js +264 -0
  87. package/dist-cjs/review-card/conflict.d.ts +40 -0
  88. package/dist-cjs/review-card/conflict.js +114 -0
  89. package/dist-cjs/review-card/cross-check.d.ts +11 -0
  90. package/dist-cjs/review-card/cross-check.js +151 -0
  91. package/dist-cjs/review-card/index.d.ts +3 -0
  92. package/dist-cjs/review-card/index.js +10 -0
  93. package/dist-cjs/run/index.d.ts +43 -0
  94. package/dist-cjs/run/index.js +237 -0
  95. package/dist-cjs/run/schemas.d.ts +2016 -0
  96. package/dist-cjs/run/schemas.js +226 -0
  97. package/dist-cjs/seams/index.d.ts +1 -0
  98. package/dist-cjs/seams/index.js +3 -0
  99. package/dist-cjs/seams/types.d.ts +66 -0
  100. package/dist-cjs/seams/types.js +12 -0
  101. package/dist-cjs/synth/address.d.ts +7 -0
  102. package/dist-cjs/synth/address.js +15 -0
  103. package/dist-cjs/synth/compose-from-recording.d.ts +65 -0
  104. package/dist-cjs/synth/compose-from-recording.js +366 -0
  105. package/dist-cjs/synth/deny-cases.d.ts +22 -0
  106. package/dist-cjs/synth/deny-cases.js +438 -0
  107. package/dist-cjs/synth/evaluate.d.ts +39 -0
  108. package/dist-cjs/synth/evaluate.js +428 -0
  109. package/dist-cjs/synth/harness.d.ts +16 -0
  110. package/dist-cjs/synth/harness.js +29 -0
  111. package/dist-cjs/synth/index.d.ts +10 -0
  112. package/dist-cjs/synth/index.js +28 -0
  113. package/dist-cjs/synth/lower.d.ts +23 -0
  114. package/dist-cjs/synth/lower.js +119 -0
  115. package/dist-cjs/synth/minimize.d.ts +4 -0
  116. package/dist-cjs/synth/minimize.js +41 -0
  117. package/dist-cjs/synth/predicate-literals.d.ts +5 -0
  118. package/dist-cjs/synth/predicate-literals.js +28 -0
  119. package/dist-cjs/synth/scope.d.ts +26 -0
  120. package/dist-cjs/synth/scope.js +81 -0
  121. package/dist-cjs/synth/synthesize-from-mandate.d.ts +5 -0
  122. package/dist-cjs/synth/synthesize-from-mandate.js +37 -0
  123. package/dist-cjs/synth/synthesize-from-recording.d.ts +49 -0
  124. package/dist-cjs/synth/synthesize-from-recording.js +759 -0
  125. package/dist-cjs/types.d.ts +311 -0
  126. package/dist-cjs/types.js +55 -0
  127. package/dist-cjs/verify/envelope.d.ts +15 -0
  128. package/dist-cjs/verify/envelope.js +23 -0
  129. package/dist-cjs/verify/index.d.ts +3 -0
  130. package/dist-cjs/verify/index.js +8 -0
  131. package/dist-cjs/verify/simulate.d.ts +31 -0
  132. package/dist-cjs/verify/simulate.js +261 -0
  133. package/dist-cjs/verify/verify.d.ts +21 -0
  134. package/dist-cjs/verify/verify.js +192 -0
  135. package/package.json +43 -3
  136. package/src/codegen/template.ts +3 -3
  137. package/src/contracts/policy-template/OZ_POLICY_TRAIT.md +171 -0
  138. package/src/record/corpus-fixtures.json +532 -0
  139. package/src/record/decode.ts +63 -10
  140. package/src/record/freshness.ts +34 -2
  141. package/src/record/index.ts +40 -0
  142. package/src/record/movements.ts +40 -7
  143. package/src/run/index.ts +277 -0
  144. package/src/run/schemas.ts +250 -0
  145. package/src/synth/address.ts +14 -0
  146. package/src/synth/compose-from-recording.ts +20 -9
  147. package/src/synth/deny-cases.ts +87 -4
  148. package/src/synth/evaluate.ts +2 -2
  149. package/src/synth/index.ts +4 -0
  150. package/src/synth/minimize.ts +7 -3
  151. package/src/synth/synthesize-from-recording.ts +200 -68
  152. package/src/types.ts +37 -1
  153. package/src/verify/simulate.ts +21 -2
  154. package/src/verify/verify.ts +21 -2
@@ -0,0 +1,438 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.OVERPERMISSIVE_DIMENSIONS = exports.ORIGINAL_DIMENSIONS = void 0;
4
+ exports.generateCases = generateCases;
5
+ const types_ts_1 = require("../types.js");
6
+ const predicate_literals_ts_1 = require("./predicate-literals.js");
7
+ const ORACLE_CASES = [
8
+ ['oracle_stale', 'stale'],
9
+ ['oracle_missing', 'missing'],
10
+ ['oracle_deviation_exceeded', 'deviation'],
11
+ ['oracle_paused', 'paused'],
12
+ ];
13
+ // Deterministic XLM/USDC adjacency fixture; the shared registry can replace this boundary later.
14
+ const ADJACENT_ASSETS = [
15
+ 'CAS3J7GYLGXMF6TDJ5WQ2PEN4GRVNXJUIQ2TZU3ZB3OQ2V4DRCWI7WPF',
16
+ 'CCWCLTASNDT57N3BCHOSVB5QWMV5URK4BXLDDF6ZZQYMBQ4OKZA3ZB2N',
17
+ ];
18
+ // Phase 1 property-harness mutation dimensions excluded from the synth pipeline's
19
+ // self-verify call so existing fixtures still emit policies. The harness tests
20
+ // them as FINDINGS against the already-emitted policy.
21
+ const OVERPERMISSIVE_DIMENSIONS = ['argument_reorder'];
22
+ exports.OVERPERMISSIVE_DIMENSIONS = OVERPERMISSIVE_DIMENSIONS;
23
+ // The 15 dimensions the synth pipeline uses for self-verify and minimise.
24
+ const ORIGINAL_DIMENSIONS = [
25
+ 'amount',
26
+ 'asset',
27
+ 'contract',
28
+ 'function',
29
+ 'timing',
30
+ 'time_window',
31
+ 'invocation_count',
32
+ 'arg_amount_bound',
33
+ 'arg_bound',
34
+ 'scope_contract_fn_arg',
35
+ 'oracle_stale',
36
+ 'oracle_missing',
37
+ 'oracle_deviation_exceeded',
38
+ 'oracle_paused',
39
+ 'soroswap_allowed_path',
40
+ ];
41
+ exports.ORIGINAL_DIMENSIONS = ORIGINAL_DIMENSIONS;
42
+ /** Build deterministic model-evaluated alternatives without mutating the intended call.
43
+ *
44
+ * @param predicate - the synthesized predicate
45
+ * @param permitCtx - EvalContext for the intended (permitted) call
46
+ * @param dimensions - optional whitelist of dimension names to emit; when omitted
47
+ * all known dimensions (including over-permissiveness mutations)
48
+ * are emitted. The synth pipeline passes ORIGINAL_DIMENSIONS so
49
+ * existing fixtures do not regress. */
50
+ function generateCases(predicate, permitCtx, dimensions) {
51
+ const facts = inspectPredicate(predicate);
52
+ const denies = [];
53
+ for (const comparison of facts.comparisons) {
54
+ if (comparison.left.kind !== 'amount')
55
+ continue;
56
+ const mutated = mutateBigIntRecord(permitCtx, 'amountByToken', comparison.left.token, comparison);
57
+ if (mutated)
58
+ denies.push({ dimension: 'amount', ctx: mutated });
59
+ }
60
+ const movedTokens = new Set();
61
+ for (const comparison of facts.comparisons) {
62
+ if (comparison.left.kind === 'amount' || comparison.left.kind === 'window_spent') {
63
+ movedTokens.add(comparison.left.token);
64
+ }
65
+ }
66
+ for (const token of movedTokens) {
67
+ denies.push({ dimension: 'asset', ctx: mutateAsset(predicate, permitCtx, token) });
68
+ }
69
+ const contractConstraints = [
70
+ ...facts.comparisons.filter((node) => node.op === 'eq' &&
71
+ node.left.kind === 'call_contract' &&
72
+ node.right.kind === 'literal_address'),
73
+ ...facts.memberships.filter((node) => node.needle.kind === 'call_contract'),
74
+ ];
75
+ for (const _constraint of contractConstraints) {
76
+ const ctx = cloneContext(permitCtx);
77
+ ctx.contract = distinctText(permitCtx.contract, 'contract');
78
+ denies.push({ dimension: 'contract', ctx });
79
+ }
80
+ const functionConstraints = [
81
+ ...facts.comparisons.filter((node) => node.op === 'eq' && node.left.kind === 'call_fn' && node.right.kind === 'literal_symbol'),
82
+ ...facts.memberships.filter((node) => node.needle.kind === 'call_fn'),
83
+ ];
84
+ for (const _constraint of functionConstraints) {
85
+ const ctx = cloneContext(permitCtx);
86
+ ctx.fn = distinctText(permitCtx.fn, 'function');
87
+ denies.push({ dimension: 'function', ctx });
88
+ }
89
+ if (permitCtx.validUntilLedger !== undefined) {
90
+ const ctx = cloneContext(permitCtx);
91
+ ctx.atLedger = permitCtx.validUntilLedger + 1;
92
+ denies.push({ dimension: 'timing', ctx });
93
+ }
94
+ for (const comparison of facts.comparisons) {
95
+ if (comparison.left.kind !== 'window_spent')
96
+ continue;
97
+ const mutated = mutateBigIntRecord(permitCtx, 'windowSpentByToken', comparison.left.token, comparison, false);
98
+ if (mutated)
99
+ denies.push({ dimension: 'time_window', ctx: mutated });
100
+ }
101
+ for (const comparison of facts.comparisons) {
102
+ if (comparison.left.kind !== 'invocation_count_in_window')
103
+ continue;
104
+ if (comparison.right.kind !== 'literal_u32')
105
+ continue;
106
+ const ctx = cloneContext(permitCtx);
107
+ ctx.invocationCountByWindow[comparison.left.windowSecs] = violatingNumber(comparison.op, comparison.right.value);
108
+ denies.push({ dimension: 'invocation_count', ctx });
109
+ }
110
+ // Ordered numeric bound on a call_arg (e.g. a SoroSwap input-amount cap
111
+ // `call_arg[0] <= limit`). A violating deny case pushes the arg past the
112
+ // bound so the leaf is exercised - and, critically, so `minimize` keeps it:
113
+ // a conjunct that no deny case needs is pruned as redundant, which would
114
+ // silently drop a caller-requested restriction.
115
+ for (const comparison of facts.comparisons) {
116
+ if (comparison.op === 'eq' || comparison.left.kind !== 'call_arg')
117
+ continue;
118
+ const violating = violatingArgScVal(comparison.op, comparison.right);
119
+ if (!violating)
120
+ continue;
121
+ const ctx = cloneContext(permitCtx);
122
+ ctx.args[comparison.left.index] = violating;
123
+ denies.push({ dimension: 'arg_amount_bound', ctx });
124
+ }
125
+ const argumentConstraints = [];
126
+ for (const comparison of facts.comparisons) {
127
+ if (comparison.op === 'eq' &&
128
+ comparison.left.kind === 'call_arg' &&
129
+ comparison.right.kind !== 'literal_vec') {
130
+ argumentConstraints.push({ index: comparison.left.index });
131
+ }
132
+ }
133
+ for (const membership of facts.memberships) {
134
+ if (membership.needle.kind === 'call_arg') {
135
+ argumentConstraints.push({ index: membership.needle.index });
136
+ }
137
+ }
138
+ for (const constraint of argumentConstraints) {
139
+ const ctx = cloneContext(permitCtx);
140
+ ctx.args[constraint.index] = { type: 'other', value: 'deny-case-opaque-argument' };
141
+ denies.push({ dimension: 'arg_bound', ctx });
142
+ }
143
+ const scopedArgumentIndices = new Set(argumentConstraints.map(({ index }) => index));
144
+ for (const comparison of facts.comparisons) {
145
+ if (comparison.op === 'eq' &&
146
+ comparison.left.kind === 'call_arg' &&
147
+ comparison.right.kind === 'literal_vec') {
148
+ scopedArgumentIndices.add(comparison.left.index);
149
+ }
150
+ }
151
+ if (contractConstraints.length > 0 &&
152
+ functionConstraints.length > 0 &&
153
+ scopedArgumentIndices.size > 0) {
154
+ const ctx = cloneContext(permitCtx);
155
+ ctx.contract = distinctText(permitCtx.contract, 'authorized-call-contract');
156
+ ctx.fn = distinctText(permitCtx.fn, 'authorized-call-function');
157
+ for (const index of scopedArgumentIndices) {
158
+ ctx.args[index] = { type: 'other', value: 'deny-case-authorized-call-argument' };
159
+ }
160
+ denies.push({ dimension: 'scope_contract_fn_arg', ctx });
161
+ }
162
+ const oracleComparisons = facts.comparisons.filter((node) => node.left.kind === 'oracle_price');
163
+ for (const [dimension, error] of ORACLE_CASES) {
164
+ for (const comparison of oracleComparisons) {
165
+ if (comparison.left.kind !== 'oracle_price')
166
+ continue;
167
+ const ctx = cloneContext(permitCtx);
168
+ ctx.oraclePriceByAsset[comparison.left.asset] = { error };
169
+ denies.push({ dimension, ctx });
170
+ }
171
+ }
172
+ for (const comparison of facts.comparisons) {
173
+ if (comparison.op !== 'eq' ||
174
+ comparison.left.kind !== 'call_arg' ||
175
+ comparison.right.kind !== 'literal_vec') {
176
+ continue;
177
+ }
178
+ const ctx = cloneContext(permitCtx);
179
+ ctx.args[comparison.left.index] = differentVector(ctx.args[comparison.left.index]);
180
+ denies.push({ dimension: 'soroswap_allowed_path', ctx });
181
+ }
182
+ // --- argument_reorder: swap first two address args ---
183
+ // Skipped when dimensions filter is active so the synth pipeline can emit a policy;
184
+ // the over-permissiveness harness then tests this mutation as a FINDING.
185
+ const constrainedArgIndices = new Set();
186
+ for (const comparison of facts.comparisons) {
187
+ if (comparison.left.kind === 'call_arg')
188
+ constrainedArgIndices.add(comparison.left.index);
189
+ }
190
+ for (const membership of facts.memberships) {
191
+ if (membership.needle.kind === 'call_arg')
192
+ constrainedArgIndices.add(membership.needle.index);
193
+ }
194
+ const hasConstrainedArg = constrainedArgIndices.size > 0;
195
+ if ((!dimensions || dimensions.includes('argument_reorder')) &&
196
+ hasConstrainedArg &&
197
+ permitCtx.args.length >= 2 &&
198
+ permitCtx.args[0]?.type === 'address' &&
199
+ permitCtx.args[1]?.type === 'address') {
200
+ const ctx = cloneContext(permitCtx);
201
+ // Guard above guarantees args[0] and args[1] exist and are address-typed.
202
+ const a0 = ctx.args[0];
203
+ const a1 = ctx.args[1];
204
+ ctx.args[0] = a1;
205
+ ctx.args[1] = a0;
206
+ denies.push({ dimension: 'argument_reorder', ctx });
207
+ }
208
+ // Version mismatch, malformed predicates, master authorization, and nonce replay are install-time checks and are intentionally omitted from model-evaluated cases.
209
+ return { permit: cloneContext(permitCtx), denies };
210
+ }
211
+ function inspectPredicate(predicate) {
212
+ const facts = { comparisons: [], memberships: [] };
213
+ visit(predicate, facts);
214
+ return facts;
215
+ }
216
+ function visit(node, facts) {
217
+ switch (node.op) {
218
+ case 'and':
219
+ case 'or':
220
+ for (const child of node.children)
221
+ visit(child, facts);
222
+ return;
223
+ case 'not':
224
+ visit(node.child, facts);
225
+ return;
226
+ case 'in':
227
+ facts.memberships.push(node);
228
+ return;
229
+ case 'eq':
230
+ case 'lt':
231
+ case 'lte':
232
+ case 'gt':
233
+ case 'gte':
234
+ facts.comparisons.push(node);
235
+ }
236
+ }
237
+ function mutateBigIntRecord(permitCtx, recordKey, token, comparison, preferScaledAmount = true) {
238
+ if (comparison.right.kind !== 'literal_i128')
239
+ return null;
240
+ try {
241
+ const current = BigInt(permitCtx[recordKey][token] ?? '0');
242
+ const bound = BigInt(comparison.right.value);
243
+ const value = violatingBigInt(comparison.op, current, bound, preferScaledAmount);
244
+ const ctx = cloneContext(permitCtx);
245
+ ctx[recordKey][token] = value.toString();
246
+ return ctx;
247
+ }
248
+ catch {
249
+ return null;
250
+ }
251
+ }
252
+ function violatingBigInt(op, current, bound, preferScaledAmount) {
253
+ if (preferScaledAmount) {
254
+ const scaledCandidates = [(current * 101n) / 100n, current * 10n];
255
+ for (const candidate of scaledCandidates) {
256
+ if (!bigIntComparison(op, candidate, bound))
257
+ return candidate;
258
+ }
259
+ }
260
+ return boundaryViolatingBigInt(op, bound);
261
+ }
262
+ /** The single value at the boundary that violates `<selector> op bound`:
263
+ * bound+1 for `lte`/`eq`, bound-1 for `gte`, and bound itself for the strict
264
+ * `lt`/`gt`. */
265
+ function boundaryViolatingBigInt(op, bound) {
266
+ switch (op) {
267
+ case 'lt':
268
+ case 'gt':
269
+ return bound;
270
+ case 'lte':
271
+ case 'eq':
272
+ return bound + 1n;
273
+ case 'gte':
274
+ return bound - 1n;
275
+ }
276
+ }
277
+ function violatingNumber(op, bound) {
278
+ switch (op) {
279
+ case 'lt':
280
+ case 'gt':
281
+ return bound;
282
+ case 'lte':
283
+ case 'eq':
284
+ return bound + 1;
285
+ case 'gte':
286
+ return bound - 1;
287
+ }
288
+ }
289
+ /** Build an ScVal that VIOLATES an ordered numeric bound on a call_arg, given
290
+ * the comparison op and its numeric-literal right-hand side. Returns null when
291
+ * the literal is not an integer (the bound is not a numeric compare). The arg
292
+ * is emitted as an i128 - the recorder's numeric args are read via BigInt, so
293
+ * the wire type only needs to be a numeric ScVal to exercise the bound. */
294
+ function violatingArgScVal(op, right) {
295
+ const bound = (0, predicate_literals_ts_1.literalNumericBigInt)(right);
296
+ if (bound === null)
297
+ return null;
298
+ return { type: 'i128', value: boundaryViolatingBigInt(op, bound).toString() };
299
+ }
300
+ function bigIntComparison(op, value, bound) {
301
+ switch (op) {
302
+ case 'eq':
303
+ return value === bound;
304
+ case 'lt':
305
+ return value < bound;
306
+ case 'lte':
307
+ return value <= bound;
308
+ case 'gt':
309
+ return value > bound;
310
+ case 'gte':
311
+ return value >= bound;
312
+ }
313
+ }
314
+ function mutateAsset(predicate, permitCtx, token) {
315
+ const binding = findAddressBinding(predicate, token);
316
+ const adjacent = adjacentAsset(token);
317
+ const ctx = cloneContext(permitCtx);
318
+ moveRecordEntry(ctx.amountByToken, token, adjacent);
319
+ moveRecordEntry(ctx.windowSpentByToken, token, adjacent);
320
+ if (binding.contract && ctx.contract === token)
321
+ ctx.contract = adjacent;
322
+ for (const index of binding.argumentIndices) {
323
+ const value = ctx.args[index];
324
+ if (value)
325
+ ctx.args[index] = replaceAddress(value, token, adjacent);
326
+ }
327
+ return ctx;
328
+ }
329
+ function findAddressBinding(predicate, address) {
330
+ const facts = inspectPredicate(predicate);
331
+ let contract = false;
332
+ const argumentIndices = new Set();
333
+ for (const comparison of facts.comparisons) {
334
+ if (comparison.op !== 'eq' || !leafContainsAddress(comparison.right, address))
335
+ continue;
336
+ if (comparison.left.kind === 'call_contract')
337
+ contract = true;
338
+ if (comparison.left.kind === 'call_arg')
339
+ argumentIndices.add(comparison.left.index);
340
+ }
341
+ for (const membership of facts.memberships) {
342
+ if (!membership.haystack.some((leaf) => leafContainsAddress(leaf, address)))
343
+ continue;
344
+ if (membership.needle.kind === 'call_contract')
345
+ contract = true;
346
+ if (membership.needle.kind === 'call_arg')
347
+ argumentIndices.add(membership.needle.index);
348
+ }
349
+ return { contract, argumentIndices };
350
+ }
351
+ function leafContainsAddress(leaf, address) {
352
+ if (leaf.kind === 'literal_address')
353
+ return leaf.value === address;
354
+ if (leaf.kind === 'literal_vec') {
355
+ return leaf.elements.some((element) => leafContainsAddress(element, address));
356
+ }
357
+ return false;
358
+ }
359
+ function moveRecordEntry(record, from, to) {
360
+ const value = record[from];
361
+ if (value === undefined)
362
+ return;
363
+ delete record[from];
364
+ record[to] = value;
365
+ }
366
+ function replaceAddress(value, from, to) {
367
+ if (value.type === 'address') {
368
+ return value.value === from ? { type: 'address', value: to } : { ...value };
369
+ }
370
+ if (value.type === 'vec') {
371
+ return { type: 'vec', value: value.value.map((item) => replaceAddress(item, from, to)) };
372
+ }
373
+ return { ...value };
374
+ }
375
+ function differentVector(actual) {
376
+ if (actual?.type !== 'vec')
377
+ return { type: 'vec', value: [] };
378
+ if (actual.value.length === 0) {
379
+ return { type: 'vec', value: [{ type: 'other', value: 'deny-case-extra-hop' }] };
380
+ }
381
+ if (actual.value.length > 1) {
382
+ const reversed = actual.value.map(cloneScVal).reverse();
383
+ if (JSON.stringify(reversed) !== JSON.stringify(actual.value)) {
384
+ return { type: 'vec', value: reversed };
385
+ }
386
+ }
387
+ const value = actual.value.map(cloneScVal);
388
+ value[0] = { type: 'other', value: 'deny-case-different-hop' };
389
+ return { type: 'vec', value };
390
+ }
391
+ function adjacentAsset(asset) {
392
+ return asset === ADJACENT_ASSETS[0] ? ADJACENT_ASSETS[1] : ADJACENT_ASSETS[0];
393
+ }
394
+ function distinctText(value, label) {
395
+ return `${value}#${label}`;
396
+ }
397
+ function cloneContext(ctx) {
398
+ const cloned = {
399
+ contract: ctx.contract,
400
+ fn: ctx.fn,
401
+ args: ctx.args.map(cloneScVal),
402
+ atLedger: ctx.atLedger,
403
+ nowSeconds: ctx.nowSeconds,
404
+ amountByToken: { ...ctx.amountByToken },
405
+ windowSpentByToken: { ...ctx.windowSpentByToken },
406
+ invocationCountByWindow: { ...ctx.invocationCountByWindow },
407
+ oraclePriceByAsset: Object.fromEntries(Object.entries(ctx.oraclePriceByAsset).map(([asset, entry]) => [
408
+ asset,
409
+ 'error' in entry ? { error: entry.error } : { ...entry },
410
+ ])),
411
+ };
412
+ if (ctx.validUntilLedger !== undefined)
413
+ cloned.validUntilLedger = ctx.validUntilLedger;
414
+ if (ctx.signerWeights !== undefined)
415
+ cloned.signerWeights = { ...ctx.signerWeights };
416
+ return cloned;
417
+ }
418
+ function cloneScVal(value, depth = 0) {
419
+ // Recursion is bounded by MAX_SCVAL_CLONE_DEPTH so a hand-crafted nested-vec
420
+ // payload cannot RangeError the JS stack during deny-case mutation. Over-depth
421
+ // throws a ToolError-shaped error that the `synthesizeFromRecording` envelope
422
+ // (item 3) converts to a structured `{ok:false, error}`.
423
+ if (value.type === 'vec') {
424
+ if (depth >= types_ts_1.MAX_SCVAL_CLONE_DEPTH) {
425
+ throw cloneDepthError(value);
426
+ }
427
+ return { type: 'vec', value: value.value.map((v) => cloneScVal(v, depth + 1)) };
428
+ }
429
+ return { ...value };
430
+ }
431
+ function cloneDepthError(value) {
432
+ const err = new Error(`ScVal clone depth exceeds MAX_SCVAL_CLONE_DEPTH (${types_ts_1.MAX_SCVAL_CLONE_DEPTH})`);
433
+ err.code = 'SYNTHESIS_ERROR';
434
+ err.severity = 'error';
435
+ err.retryable = false;
436
+ err.depthContext = value.type;
437
+ throw err;
438
+ }
@@ -0,0 +1,39 @@
1
+ import type { PredicateNode, ScVal } from '../types.ts';
2
+ export interface EvalContext {
3
+ /** Contract the interpreter is asked to enforce against. */
4
+ contract: string;
5
+ /** Function name on that contract. */
6
+ fn: string;
7
+ /** Decoded `ScVal[]` of the top-level authorized call. */
8
+ args: ScVal[];
9
+ /** Ledger sequence at which the interpreter is invoked. */
10
+ atLedger: number;
11
+ /** Optional policy expiry; absent -> step 1 is skipped. */
12
+ validUntilLedger?: number;
13
+ /** Unix seconds; used by `now` / `valid_until` leaves. */
14
+ nowSeconds: number;
15
+ /** Per-token amount moved by the current call (i128 decimal string). */
16
+ amountByToken: Record<string, string>;
17
+ /** Per-token window-rolling spend prior to this call (i128 decimal string). */
18
+ windowSpentByToken: Record<string, string>;
19
+ /** Recorded invocation counts keyed by window seconds. */
20
+ invocationCountByWindow: Record<number, number>;
21
+ /** Optional signer-weight map for the threshold gate. Absent -> skip. */
22
+ signerWeights?: Record<string, number>;
23
+ /** Per-asset oracle snapshot. Missing keys default to ORACLE_STALE. */
24
+ oraclePriceByAsset: Record<string, {
25
+ price: string;
26
+ timestampSeconds: number;
27
+ } | {
28
+ error: 'stale' | 'missing' | 'deviation' | 'paused' | 'decimals' | 'fingerprint';
29
+ }>;
30
+ }
31
+ export type EvalResult = {
32
+ permit: true;
33
+ } | {
34
+ permit: false;
35
+ reason: string;
36
+ };
37
+ /** Evaluate a `PredicateNode` against the candidate call described by `ctx`.
38
+ * Pure function. Returns `{ permit: true }` or `{ permit: false; reason }`. */
39
+ export declare function evaluate(predicate: PredicateNode, ctx: EvalContext): EvalResult;