@crediolabs/policy-synth 0.1.3 → 0.1.5

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