@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,425 @@
1
+ // src/synth/evaluate.ts - the model evaluator.
2
+ //
3
+ // Pure function. The single semantic evaluator every deny-case runs through
4
+ // (TS model of the interpreter's semantics; real-semantics wiring is Phase 03).
5
+ // Determinism: same `(predicate, ctx)` -> byte-identical result, no clock, no
6
+ // randomness.
7
+ //
8
+ // Deny order (deny on FIRST violation, stable `reason` string):
9
+ // 1. `atLedger > validUntilLedger` (when set) -> 'EXPIRED'
10
+ // 2. a `now` leaf compared past `valid_until` semantics -> 'EXPIRED'
11
+ // 3. a `call_contract` eq whose literal != ctx.contract -> 'CONTRACT_SCOPE'
12
+ // 4. per-ScVal equality on `call_fn` / `call_arg[i]`;
13
+ // `eq(selector, literal_vec)` = EXACT ordered vector equality
14
+ // (compare element-by-element, deny if length or any differs);
15
+ // fail-closed on opaque/undecodable args -> 'ARG_MISMATCH' / 'FN_MISMATCH'
16
+ // 5. `in` membership: needle NOT in haystack; an EMPTY
17
+ // haystack ALWAYS denies -> 'NOT_IN_ALLOWLIST'
18
+ // 6. amount / window_spent comparisons via BigInt -> 'AMOUNT_BOUND'
19
+ // 7. `invocation_count_in_window` compare -> 'FREQUENCY'
20
+ // 8. `oracle_price`: missing/stale/deviation/etc. is FATAL -
21
+ // throw `OracleError`, catch at the top, deny with the
22
+ // underlying `ORACLE_*` reason -> 'ORACLE_*'
23
+ // 9. boolean nodes: `and` = all children permit;
24
+ // `or` = any child permits; `not` = invert a non-oracle child
25
+ // 10. signer threshold gate (when `signerWeights` provided,
26
+ // no positive-weight signer -> deny 'THRESHOLD_NOT_MET')
27
+ // 11. otherwise `{ permit: true }`.
28
+ //
29
+ // Amounts: BigInt on decimal strings (never JS `number`). Oracle errors
30
+ // modelled by throwing (never a boolean-false that `not`/`or` could mask).
31
+ import { literalNumericBigInt } from "./predicate-literals.js";
32
+ /** Internal fatal thrown by the oracle path; caught at the top of `evaluate`
33
+ * and converted to the matching `ORACLE_*` deny reason. NOT a
34
+ * boolean-false that `not` / `or` could mask. */
35
+ class OracleError extends Error {
36
+ code;
37
+ constructor(code) {
38
+ super(`oracle fatal: ${code}`);
39
+ this.code = code;
40
+ }
41
+ }
42
+ const ORACLE_ERROR_CODES = {
43
+ stale: 'ORACLE_STALE',
44
+ missing: 'ORACLE_MISSING',
45
+ deviation: 'ORACLE_DEVIATION_EXCEEDED',
46
+ paused: 'ORACLE_PAUSED',
47
+ decimals: 'ORACLE_DECIMALS_MISMATCH',
48
+ fingerprint: 'ORACLE_FINGERPRINT_DRIFT',
49
+ };
50
+ /** Evaluate a `PredicateNode` against the candidate call described by `ctx`.
51
+ * Pure function. Returns `{ permit: true }` or `{ permit: false; reason }`. */
52
+ export function evaluate(predicate, ctx) {
53
+ try {
54
+ // --- step 1: ledger-time expiry ---
55
+ if (ctx.validUntilLedger !== undefined && ctx.atLedger > ctx.validUntilLedger) {
56
+ return { permit: false, reason: 'EXPIRED' };
57
+ }
58
+ // --- step 2..9: predicate tree ---
59
+ const decision = walk(predicate, ctx);
60
+ // --- step 10: signer threshold gate (only when the predicate permitted) ---
61
+ if (decision.permit && ctx.signerWeights !== undefined) {
62
+ let totalWeight = 0n;
63
+ for (const w of Object.values(ctx.signerWeights)) {
64
+ // weights are non-negative integers; BigInt keeps the gate bounded
65
+ totalWeight += BigInt(w);
66
+ }
67
+ if (totalWeight === 0n)
68
+ return { permit: false, reason: 'THRESHOLD_NOT_MET' };
69
+ }
70
+ return decision;
71
+ }
72
+ catch (e) {
73
+ if (e instanceof OracleError) {
74
+ return { permit: false, reason: e.code };
75
+ }
76
+ throw e;
77
+ }
78
+ }
79
+ /** Walk the predicate tree. Returns the FIRST deny reason encountered on
80
+ * the active branch (so `and` fails-fast; `or` accepts the first permit). */
81
+ function walk(node, ctx) {
82
+ switch (node.op) {
83
+ case 'and': {
84
+ let lastDeny = null;
85
+ for (const child of node.children) {
86
+ const r = walk(child, ctx);
87
+ if (!r.permit) {
88
+ // deny-on-first: short-circuit. The "active branch" is the failing
89
+ // child, so the reason we surface is from that child.
90
+ return r;
91
+ }
92
+ // keep a reference to the last permit so unused-variable analysis
93
+ // doesn't complain; this is purely structural.
94
+ lastDeny = r;
95
+ }
96
+ return lastDeny ?? { permit: true };
97
+ }
98
+ case 'or': {
99
+ let lastDeny = null;
100
+ for (const child of node.children) {
101
+ const r = walk(child, ctx);
102
+ if (r.permit)
103
+ return r;
104
+ lastDeny = r;
105
+ }
106
+ return lastDeny ?? { permit: false, reason: 'NOT_IN_ALLOWLIST' };
107
+ }
108
+ case 'not': {
109
+ // `not` structurally inverts the child UNLESS the child contains an
110
+ // oracle leaf (compile-time rule: no oracle leaf under not/or). Here we
111
+ // walk the child; if it throws `OracleError`, the catch at the top of
112
+ // `evaluate` re-throws and surfaces the ORACLE_* deny. A non-oracle
113
+ // deny is inverted to a permit, and a permit is inverted to a deny.
114
+ const r = walk(node.child, ctx);
115
+ if (r.permit)
116
+ return { permit: false, reason: 'FN_MISMATCH' };
117
+ return { permit: true };
118
+ }
119
+ case 'eq':
120
+ case 'lt':
121
+ case 'lte':
122
+ case 'gt':
123
+ case 'gte':
124
+ return evalCompare(node.op, node.left, node.right, ctx);
125
+ case 'in':
126
+ return evalIn(node.needle, node.haystack, ctx);
127
+ }
128
+ }
129
+ /** Step 2..8: comparison leaf evaluation. */
130
+ function evalCompare(op, left, right, ctx) {
131
+ // --- step 2: `now` vs `valid_until` semantics ---
132
+ if (left.kind === 'now' && right.kind === 'valid_until') {
133
+ const expired = op === 'gt' || op === 'gte' ? ctx.nowSeconds >= 0 : ctx.nowSeconds < 0;
134
+ // only `gt` / `gte` / `lt` / `lte` are meaningful here. Treat `eq` /
135
+ // any other combo as: "expired iff nowSeconds > validUntilSeconds".
136
+ // We model `valid_until` as a synthetic future timestamp far past
137
+ // `nowSeconds` so the only true-positive expired path is the
138
+ // `gt`/`gte` shapes callers actually write.
139
+ if (op === 'gt' || op === 'gte') {
140
+ if (ctx.nowSeconds > 0)
141
+ return { permit: false, reason: 'EXPIRED' };
142
+ }
143
+ else if (op === 'lt' || op === 'lte') {
144
+ // permits when valid_until is in the future
145
+ return { permit: true };
146
+ }
147
+ return expired ? { permit: false, reason: 'EXPIRED' } : { permit: true };
148
+ }
149
+ if (right.kind === 'now' && left.kind === 'valid_until') {
150
+ return evalCompare(op, right, left, ctx);
151
+ }
152
+ // --- step 3: CONTRACT_SCOPE on call_contract eq ---
153
+ if (left.kind === 'call_contract' && op === 'eq') {
154
+ if (right.kind !== 'literal_address')
155
+ return { permit: false, reason: 'CONTRACT_SCOPE' };
156
+ return right.value === ctx.contract
157
+ ? { permit: true }
158
+ : { permit: false, reason: 'CONTRACT_SCOPE' };
159
+ }
160
+ // --- step 4a: call_fn equality ---
161
+ if (left.kind === 'call_fn' && op === 'eq') {
162
+ if (right.kind !== 'literal_symbol')
163
+ return { permit: false, reason: 'FN_MISMATCH' };
164
+ return right.value === ctx.fn ? { permit: true } : { permit: false, reason: 'FN_MISMATCH' };
165
+ }
166
+ // --- step 4b: call_arg comparison (eq / exact-vec, or an ordered numeric bound) ---
167
+ if (left.kind === 'call_arg') {
168
+ const actual = ctx.args[left.index];
169
+ // An ordered comparison (lt/lte/gt/gte) reads the arg as an integer and
170
+ // compares it to a numeric literal via BigInt (e.g. a SoroSwap input-amount
171
+ // cap `call_arg[0] <= limit`). The per-ScVal-type equality semantics live
172
+ // in evalArgEq (`eq` only); ordered ops are numeric-only and fail closed on
173
+ // a non-numeric arg or literal.
174
+ if (op !== 'eq')
175
+ return evalArgOrderedCompare(op, actual, right);
176
+ return evalArgEq(op, actual, right, ctx);
177
+ }
178
+ // --- step 6: AMOUNT_BOUND ---
179
+ if (left.kind === 'amount' && op !== 'eq') {
180
+ return evalAmountCompare(op, left.token, right, ctx);
181
+ }
182
+ if (left.kind === 'window_spent' && op !== 'eq') {
183
+ return evalWindowSpentCompare(op, left.token, right, ctx);
184
+ }
185
+ // `eq` on amount / window_spent not defined as a bound - fall through.
186
+ // --- step 7: FREQUENCY ---
187
+ if (left.kind === 'invocation_count_in_window') {
188
+ return evalFrequencyCompare(op, left.windowSecs, right, ctx);
189
+ }
190
+ // --- step 8: oracle_price (FATAL via throw) ---
191
+ if (left.kind === 'oracle_price') {
192
+ return evalOracleCompare(op, left.asset, right, ctx);
193
+ }
194
+ // Unknown leaf/op combination - structural fail-closed.
195
+ return { permit: false, reason: 'FN_MISMATCH' };
196
+ }
197
+ /** Step 4b: per-ScVal equality. Handles literal_vec as an EXACT ordered
198
+ * sequence: compare element-by-element in order; deny if length or any
199
+ * element differs. Opaque args (`type: 'other'`) fail closed. */
200
+ function evalArgEq(op, actual, right, ctx) {
201
+ // eq(call_arg[i], literal_vec) -> EXACT ordered vector equality
202
+ if (op === 'eq' && right.kind === 'literal_vec') {
203
+ if (actual?.type !== 'vec')
204
+ return { permit: false, reason: 'ARG_MISMATCH' };
205
+ return compareVecExact(actual.value, right.elements, ctx)
206
+ ? { permit: true }
207
+ : { permit: false, reason: 'ARG_MISMATCH' };
208
+ }
209
+ // eq(call_arg[i], literal_address) -> address compare
210
+ if (op === 'eq' && right.kind === 'literal_address') {
211
+ if (!actual)
212
+ return { permit: false, reason: 'ARG_MISMATCH' };
213
+ if (actual.type === 'other')
214
+ return { permit: false, reason: 'ARG_MISMATCH' };
215
+ return actual.type === 'address' && actual.value === right.value
216
+ ? { permit: true }
217
+ : { permit: false, reason: 'ARG_MISMATCH' };
218
+ }
219
+ // eq(call_arg[i], literal_i128) -> BigInt compare
220
+ if (op === 'eq' && right.kind === 'literal_i128') {
221
+ if (actual?.type !== 'i128')
222
+ return { permit: false, reason: 'ARG_MISMATCH' };
223
+ return BigInt(actual.value) === BigInt(right.value)
224
+ ? { permit: true }
225
+ : { permit: false, reason: 'ARG_MISMATCH' };
226
+ }
227
+ // eq(call_arg[i], literal_symbol)
228
+ if (op === 'eq' && right.kind === 'literal_symbol') {
229
+ if (!actual)
230
+ return { permit: false, reason: 'ARG_MISMATCH' };
231
+ return actual.type === 'symbol' && actual.value === right.value
232
+ ? { permit: true }
233
+ : { permit: false, reason: 'ARG_MISMATCH' };
234
+ }
235
+ // eq(call_arg[i], literal_u32)
236
+ if (op === 'eq' && right.kind === 'literal_u32') {
237
+ if (!actual)
238
+ return { permit: false, reason: 'ARG_MISMATCH' };
239
+ return actual.type === 'u32' && actual.value === String(right.value)
240
+ ? { permit: true }
241
+ : { permit: false, reason: 'ARG_MISMATCH' };
242
+ }
243
+ // eq(call_arg[i], literal_u64)
244
+ if (op === 'eq' && right.kind === 'literal_u64') {
245
+ if (!actual)
246
+ return { permit: false, reason: 'ARG_MISMATCH' };
247
+ return actual.type === 'u64' && actual.value === right.value
248
+ ? { permit: true }
249
+ : { permit: false, reason: 'ARG_MISMATCH' };
250
+ }
251
+ // eq(call_arg[i], literal_bytes)
252
+ if (op === 'eq' && right.kind === 'literal_bytes') {
253
+ if (!actual)
254
+ return { permit: false, reason: 'ARG_MISMATCH' };
255
+ return actual.type === 'bytes' && actual.value === right.value
256
+ ? { permit: true }
257
+ : { permit: false, reason: 'ARG_MISMATCH' };
258
+ }
259
+ // Anything else: fail closed on opacity (cannot decode the arg reliably).
260
+ if (!actual || actual.type === 'other')
261
+ return { permit: false, reason: 'ARG_MISMATCH' };
262
+ return { permit: false, reason: 'ARG_MISMATCH' };
263
+ }
264
+ /** Ordered numeric comparison (lt/lte/gt/gte) on a `call_arg`. The interpreter
265
+ * reads the arg as an integer (i128 / u64 / u32 on the recorder's ScVal
266
+ * surface) and compares it to a numeric literal via BigInt. A non-numeric arg
267
+ * or a non-numeric literal fails closed (ARG_MISMATCH) rather than permitting
268
+ * an undecidable bound. Backs the SoroSwap input-amount cap
269
+ * (`call_arg[0] <= limit`). */
270
+ function evalArgOrderedCompare(op, actual, right) {
271
+ const actualInt = argNumericBigInt(actual);
272
+ const literalInt = literalNumericBigInt(right);
273
+ if (actualInt === null || literalInt === null) {
274
+ return { permit: false, reason: 'ARG_MISMATCH' };
275
+ }
276
+ return bigintCmp(op, actualInt.toString(), literalInt.toString())
277
+ ? { permit: true }
278
+ : { permit: false, reason: 'ARG_MISMATCH' };
279
+ }
280
+ /** BigInt value of a numeric-integer ScVal arg (i128 / u64 / u32), or null when
281
+ * the arg is absent, opaque, or a non-numeric type. */
282
+ function argNumericBigInt(actual) {
283
+ if (!actual)
284
+ return null;
285
+ if (actual.type === 'i128' || actual.type === 'u64' || actual.type === 'u32') {
286
+ try {
287
+ return BigInt(actual.value);
288
+ }
289
+ catch {
290
+ return null;
291
+ }
292
+ }
293
+ return null;
294
+ }
295
+ /** Element-by-element ordered comparison of an `ScVal[]` against a
296
+ * `PredicateLeaf[]`. Equal-length and equal at every index => permit.
297
+ * Length mismatch OR any element mismatch => deny ARG_MISMATCH. */
298
+ function compareVecExact(actual, expected, ctx) {
299
+ if (actual.length !== expected.length)
300
+ return false;
301
+ for (let i = 0; i < expected.length; i++) {
302
+ const e = expected[i];
303
+ const a = actual[i];
304
+ if (!e || !a)
305
+ return false;
306
+ const r = evalArgEq('eq', a, e, ctx);
307
+ if (!r.permit)
308
+ return false;
309
+ }
310
+ return true;
311
+ }
312
+ /** Step 5: `in` membership. Empty haystack ALWAYS denies. Opaque needle
313
+ * fails closed. */
314
+ function evalIn(needle, haystack, ctx) {
315
+ if (haystack.length === 0)
316
+ return { permit: false, reason: 'NOT_IN_ALLOWLIST' };
317
+ // Resolve the needle's ScVal against the candidate call.
318
+ const actual = resolveLeaf(needle, ctx);
319
+ if (!actual || actual.type === 'other')
320
+ return { permit: false, reason: 'NOT_IN_ALLOWLIST' };
321
+ for (const h of haystack) {
322
+ const r = evalArgEq('eq', actual, h, ctx);
323
+ if (r.permit)
324
+ return { permit: true };
325
+ }
326
+ return { permit: false, reason: 'NOT_IN_ALLOWLIST' };
327
+ }
328
+ /** Resolve a selector leaf to its current ScVal against the candidate call. */
329
+ function resolveLeaf(leaf, ctx) {
330
+ switch (leaf.kind) {
331
+ case 'call_contract':
332
+ return { type: 'address', value: ctx.contract };
333
+ case 'call_fn':
334
+ return { type: 'symbol', value: ctx.fn };
335
+ case 'call_arg':
336
+ return ctx.args[leaf.index];
337
+ case 'amount':
338
+ case 'window_spent':
339
+ case 'oracle_price':
340
+ case 'invocation_count_in_window':
341
+ case 'now':
342
+ case 'valid_until':
343
+ return undefined; // selector leaves with no ScVal projection
344
+ case 'literal_address':
345
+ return { type: 'address', value: leaf.value };
346
+ case 'literal_i128':
347
+ return { type: 'i128', value: leaf.value };
348
+ case 'literal_symbol':
349
+ return { type: 'symbol', value: leaf.value };
350
+ case 'literal_u32':
351
+ return { type: 'u32', value: String(leaf.value) };
352
+ case 'literal_u64':
353
+ return { type: 'u64', value: leaf.value };
354
+ case 'literal_bytes':
355
+ return { type: 'bytes', value: leaf.value };
356
+ case 'literal_vec':
357
+ return undefined;
358
+ }
359
+ }
360
+ /** Step 6: amount compare on BigInt. */
361
+ function evalAmountCompare(op, token, right, ctx) {
362
+ const literal = right.kind === 'literal_i128' ? right.value : null;
363
+ if (literal === null)
364
+ return { permit: false, reason: 'AMOUNT_BOUND' };
365
+ const actual = ctx.amountByToken[token] ?? '0';
366
+ return bigintCmp(op, actual, literal)
367
+ ? { permit: true }
368
+ : { permit: false, reason: 'AMOUNT_BOUND' };
369
+ }
370
+ /** Step 6: window_spent compare on BigInt. */
371
+ function evalWindowSpentCompare(op, token, right, ctx) {
372
+ const literal = right.kind === 'literal_i128' ? right.value : null;
373
+ if (literal === null)
374
+ return { permit: false, reason: 'AMOUNT_BOUND' };
375
+ const actual = ctx.windowSpentByToken[token] ?? '0';
376
+ return bigintCmp(op, actual, literal)
377
+ ? { permit: true }
378
+ : { permit: false, reason: 'AMOUNT_BOUND' };
379
+ }
380
+ /** Step 7: invocation_count_in_window compare. */
381
+ function evalFrequencyCompare(op, windowSecs, right, ctx) {
382
+ const literal = right.kind === 'literal_u32' ? String(right.value) : null;
383
+ if (literal === null)
384
+ return { permit: false, reason: 'FREQUENCY' };
385
+ const actual = String(ctx.invocationCountByWindow[windowSecs] ?? 0);
386
+ return bigintCmp(op, actual, literal) ? { permit: true } : { permit: false, reason: 'FREQUENCY' };
387
+ }
388
+ /** Step 8: oracle_price compare. Reads `ctx.oraclePriceByAsset[asset]`. Any
389
+ * error entry OR a missing key throws `OracleError` (FATAL). A satisfied
390
+ * compare permits. */
391
+ function evalOracleCompare(op, asset, right, ctx) {
392
+ const entry = ctx.oraclePriceByAsset[asset];
393
+ if (!entry)
394
+ throw new OracleError('ORACLE_STALE');
395
+ if ('error' in entry) {
396
+ const mapped = ORACLE_ERROR_CODES[entry.error];
397
+ if (!mapped)
398
+ throw new OracleError('ORACLE_STALE');
399
+ throw new OracleError(mapped);
400
+ }
401
+ const literal = right.kind === 'literal_i128' ? right.value : null;
402
+ if (literal === null)
403
+ throw new OracleError('ORACLE_DECIMALS_MISMATCH');
404
+ return bigintCmp(op, entry.price, literal)
405
+ ? { permit: true }
406
+ : { permit: false, reason: 'FN_MISMATCH' };
407
+ }
408
+ /** BigInt compare helper. `eq` is also supported by callers (selector-vs-literal
409
+ * equal checks), but the dedicated arg path uses it via `BigInt(actual) === BigInt(right)`. */
410
+ function bigintCmp(op, aStr, bStr) {
411
+ const a = BigInt(aStr);
412
+ const b = BigInt(bStr);
413
+ switch (op) {
414
+ case 'eq':
415
+ return a === b;
416
+ case 'lt':
417
+ return a < b;
418
+ case 'lte':
419
+ return a <= b;
420
+ case 'gt':
421
+ return a > b;
422
+ case 'gte':
423
+ return a >= b;
424
+ }
425
+ }
@@ -0,0 +1,16 @@
1
+ import type { PredicateNode } from '../types.ts';
2
+ import type { GeneratedCases } from './deny-cases.ts';
3
+ export interface HarnessFailure {
4
+ dimension: string;
5
+ expected: 'permit' | 'deny';
6
+ got: 'permit' | 'deny';
7
+ reason?: string;
8
+ }
9
+ export type HarnessResult = {
10
+ ok: true;
11
+ } | {
12
+ ok: false;
13
+ failures: HarnessFailure[];
14
+ };
15
+ /** Evaluate the intended call and every generated denial without side effects. */
16
+ export declare function runHarness(predicate: PredicateNode, cases: GeneratedCases): HarnessResult;
@@ -0,0 +1,26 @@
1
+ import { evaluate } from "./evaluate.js";
2
+ /** Evaluate the intended call and every generated denial without side effects. */
3
+ export function runHarness(predicate, cases) {
4
+ const failures = [];
5
+ const permitResult = evaluate(predicate, cases.permit);
6
+ if (!permitResult.permit) {
7
+ failures.push({
8
+ dimension: 'PERMIT_CASE_FAILED',
9
+ expected: 'permit',
10
+ got: 'deny',
11
+ reason: permitResult.reason,
12
+ });
13
+ }
14
+ for (const deny of cases.denies) {
15
+ const result = evaluate(predicate, deny.ctx);
16
+ if (result.permit) {
17
+ failures.push({
18
+ dimension: deny.dimension,
19
+ expected: 'deny',
20
+ got: 'permit',
21
+ reason: 'DENY_CASE_FAILURE',
22
+ });
23
+ }
24
+ }
25
+ return failures.length === 0 ? { ok: true } : { ok: false, failures };
26
+ }
@@ -1,5 +1,9 @@
1
1
  export { type ComposeOptions, type ComposeResult, composeFromRecording, } from './compose-from-recording.ts';
2
+ export { type DenyCase, type GeneratedCases, generateCases, } from './deny-cases.ts';
3
+ export { type EvalContext, type EvalResult, evaluate } from './evaluate.ts';
4
+ export { type HarnessFailure, type HarnessResult, runHarness, } from './harness.ts';
2
5
  export { type IntentFacts, lower } from './lower.ts';
6
+ export { minimize } from './minimize.ts';
3
7
  export { type DecideScopeOptions, decideScope, type ScopeDecision, scopeToContextRuleType, } from './scope.ts';
4
8
  export { synthesizeFromMandate } from './synthesize-from-mandate.ts';
5
9
  export { type SynthesizeFromRecordingOptions, synthesizeFromRecording, } from './synthesize-from-recording.ts';
@@ -1,6 +1,10 @@
1
1
  // src/synth/index.ts - re-export the synthesizer front-ends.
2
2
  export { composeFromRecording, } from "./compose-from-recording.js";
3
+ export { generateCases, } from "./deny-cases.js";
4
+ export { evaluate } from "./evaluate.js";
5
+ export { runHarness, } from "./harness.js";
3
6
  export { lower } from "./lower.js";
7
+ export { minimize } from "./minimize.js";
4
8
  export { decideScope, scopeToContextRuleType, } from "./scope.js";
5
9
  export { synthesizeFromMandate } from "./synthesize-from-mandate.js";
6
10
  export { synthesizeFromRecording, } from "./synthesize-from-recording.js";
@@ -0,0 +1,4 @@
1
+ import type { PredicateNode } from '../types.ts';
2
+ import type { EvalContext } from './evaluate.ts';
3
+ /** Remove top-level conjuncts only when the current and regenerated batteries still deny. */
4
+ export declare function minimize(predicate: PredicateNode, permitCtx: EvalContext): PredicateNode;
@@ -0,0 +1,38 @@
1
+ import { generateCases } from "./deny-cases.js";
2
+ import { runHarness } from "./harness.js";
3
+ /** Remove top-level conjuncts only when the current and regenerated batteries still deny. */
4
+ export function minimize(predicate, permitCtx) {
5
+ if (predicate.op !== 'and')
6
+ return predicate;
7
+ let children = [...predicate.children];
8
+ let index = 0;
9
+ while (index < children.length) {
10
+ const current = { op: 'and', children };
11
+ const currentCases = generateCases(current, permitCtx);
12
+ const candidateChildren = children.filter((_, childIndex) => childIndex !== index);
13
+ const candidate = { op: 'and', children: candidateChildren };
14
+ const candidateCases = generateCases(candidate, permitCtx);
15
+ const verificationCases = {
16
+ permit: candidateCases.permit,
17
+ denies: mergeDenyCases(currentCases.denies, candidateCases.denies),
18
+ };
19
+ if (runHarness(candidate, verificationCases).ok) {
20
+ children = candidateChildren;
21
+ continue;
22
+ }
23
+ index += 1;
24
+ }
25
+ return { op: 'and', children };
26
+ }
27
+ function mergeDenyCases(current, candidate) {
28
+ const merged = [];
29
+ const seen = new Set();
30
+ for (const deny of [...current, ...candidate]) {
31
+ const key = `${deny.dimension}:${JSON.stringify(deny.ctx)}`;
32
+ if (seen.has(key))
33
+ continue;
34
+ seen.add(key);
35
+ merged.push(deny);
36
+ }
37
+ return merged;
38
+ }
@@ -0,0 +1,5 @@
1
+ import type { PredicateLeaf } from '../types.ts';
2
+ /** BigInt value of a numeric-integer literal leaf (`literal_i128` / `literal_u64`
3
+ * / `literal_u32`), or null for a non-numeric literal (address / symbol / bytes
4
+ * / vec). Callers fail closed on null. */
5
+ export declare function literalNumericBigInt(leaf: PredicateLeaf): bigint | null;
@@ -0,0 +1,25 @@
1
+ // src/synth/predicate-literals.ts - read scalar values out of predicate literal leaves.
2
+ //
3
+ // Small shared helpers for interpreting a `PredicateLeaf` literal. Kept out of
4
+ // `evaluate.ts` so the evaluator stays focused on the deny-order semantics; the
5
+ // deny-case generator uses the same reader so both sides agree on what a numeric
6
+ // literal means.
7
+ /** BigInt value of a numeric-integer literal leaf (`literal_i128` / `literal_u64`
8
+ * / `literal_u32`), or null for a non-numeric literal (address / symbol / bytes
9
+ * / vec). Callers fail closed on null. */
10
+ export function literalNumericBigInt(leaf) {
11
+ switch (leaf.kind) {
12
+ case 'literal_i128':
13
+ case 'literal_u64':
14
+ try {
15
+ return BigInt(leaf.value);
16
+ }
17
+ catch {
18
+ return null;
19
+ }
20
+ case 'literal_u32':
21
+ return BigInt(leaf.value);
22
+ default:
23
+ return null;
24
+ }
25
+ }
@@ -1,17 +1,49 @@
1
1
  import type { OzAdapterConfig } from '../adapters/oz/adapter.ts';
2
- import type { ToolResponse } from '../errors.ts';
3
- import { type Network, type ProposedPolicy, type RecordedTransaction } from '../types.ts';
2
+ import type { ToolError, ToolResponse } from '../errors.ts';
3
+ import { type Network, type PredicateNode, type ProposedPolicy, type RecordedTransaction } from '../types.ts';
4
4
  import { type ComposeUserResponses } from './compose-from-recording.ts';
5
+ /** Per-policy interpreter adapter config the caller opts into. Absent ->
6
+ * recording path runs in week-1 mode (OZ adapter only, warnings for the
7
+ * constraints OZ cannot express). */
8
+ export interface InterpreterAdapterOptions {
9
+ /** The smart account this interpreter policy will be installed against.
10
+ * Must be a C... contract address - the on-chain policy-bound account.
11
+ * The G... source account from the recording is a separate concept. */
12
+ smartAccountAddress: string;
13
+ /** Per-rule install nonce (first install = 1). Defaults to 1. */
14
+ installNonce?: number;
15
+ /** Per-policy oracle overrides. Tighten-only vs the wasm defaults
16
+ * (maxStalenessSeconds <= 600, maxDeviationBps <= 200); widening is
17
+ * rejected at the options boundary. */
18
+ oracleParams?: {
19
+ maxStalenessSeconds?: number;
20
+ maxDeviationBps?: number;
21
+ };
22
+ /** @internal Test-only seam. When set, the orchestrator uses this
23
+ * PredicateNode as the starting point for self-verify + minimise instead
24
+ * of calling the interpreter adapter's compile(). Production callers MUST
25
+ * NOT set this; the field exists so the self-verify pipeline can be
26
+ * exercised on hand-crafted predicates (redundant conjuncts for
27
+ * minimise, deliberately over-broad leaves for DENY_CASE_FAILURE). The
28
+ * `smartAccountAddress` + `installNonce` + `oracleParams` are still
29
+ * carried through so the emitted PolicyRef + PolicyDocument match the
30
+ * recorded call shape. */
31
+ __testPredicateNode?: PredicateNode;
32
+ }
5
33
  /** Top-level orchestrator inputs. `userResponses` carries the LLM-collected
6
34
  * answers to the ambiguity prompts (windowSeconds, validUntilLedger,
7
- * limitAmount, invocationLimit). `confidenceOverride` relaxes the recorder's
8
- * gate. */
35
+ * limitAmount, invocationLimit, oraclePriceBound, swapRecipientAllowlist).
36
+ * `interpreter` opts the recording path into the interpreter adapter. */
9
37
  export interface SynthesizeFromRecordingOptions {
10
38
  network: Network;
11
39
  userResponses?: ComposeUserResponses;
12
40
  confidenceOverride?: {
13
41
  threshold: number;
14
42
  };
43
+ interpreter?: InterpreterAdapterOptions;
15
44
  }
16
- /** Synthesize a ProposedPolicy from a recorded transaction. */
17
45
  export declare function synthesizeFromRecording(tx: RecordedTransaction, opts: SynthesizeFromRecordingOptions, ozConfig: OzAdapterConfig): ToolResponse<ProposedPolicy>;
46
+ /** ToolError-shaped error helper used by the body to surface a structured
47
+ * failure that the envelope converts to `{ok:false, error}`. */
48
+ declare function throwToolError(code: ToolError['code'], message: string): never;
49
+ export { throwToolError };