@crediolabs/policy-synth 0.2.0 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (249) hide show
  1. package/README.md +4 -4
  2. package/dist/adapters/interpreter/adapter.d.ts +19 -25
  3. package/dist/adapters/interpreter/adapter.js +54 -487
  4. package/dist/errors.d.ts +1 -1
  5. package/dist/index.d.ts +1 -4
  6. package/dist/index.js +1 -4
  7. package/dist/install/build-add-context-rule.d.ts +3 -19
  8. package/dist/install/build-add-context-rule.js +3 -98
  9. package/dist/install/build-install-policy.d.ts +4 -53
  10. package/dist/install/build-install-policy.js +44 -123
  11. package/dist/install/index.d.ts +0 -2
  12. package/dist/install/index.js +0 -7
  13. package/dist/predicate/decode.d.ts +1 -1
  14. package/dist/predicate/decode.js +2 -70
  15. package/dist/predicate/encode.js +45 -229
  16. package/dist/predicate/from-json.js +1 -42
  17. package/dist/record/decode.js +3 -8
  18. package/dist/record/freshness.d.ts +17 -11
  19. package/dist/record/freshness.js +28 -18
  20. package/dist/review-card/builder.d.ts +4 -3
  21. package/dist/review-card/builder.js +18 -162
  22. package/dist/review-card/cross-check.js +2 -105
  23. package/dist/review-card/render-leaf.d.ts +4 -0
  24. package/dist/review-card/render-leaf.js +47 -0
  25. package/dist/run/index.d.ts +36 -139
  26. package/dist/run/index.js +125 -437
  27. package/dist/run/schemas.d.ts +168 -1929
  28. package/dist/run/schemas.js +40 -269
  29. package/dist/{synth → simulate}/deny-cases.d.ts +4 -8
  30. package/dist/simulate/deny-cases.js +282 -0
  31. package/dist/simulate/evaluate.d.ts +18 -0
  32. package/dist/simulate/evaluate.js +271 -0
  33. package/dist/simulate/index.d.ts +4 -0
  34. package/dist/simulate/index.js +8 -0
  35. package/dist/synth/compose-from-recording.d.ts +24 -51
  36. package/dist/synth/compose-from-recording.js +120 -282
  37. package/dist/synth/index.d.ts +0 -5
  38. package/dist/synth/index.js +0 -5
  39. package/dist/synth/synthesize-from-recording.d.ts +1 -29
  40. package/dist/synth/synthesize-from-recording.js +41 -356
  41. package/dist/types.d.ts +8 -60
  42. package/dist/types.js +5 -2
  43. package/dist-cjs/adapters/interpreter/adapter.d.ts +19 -25
  44. package/dist-cjs/adapters/interpreter/adapter.js +56 -489
  45. package/dist-cjs/errors.d.ts +1 -1
  46. package/dist-cjs/index.d.ts +1 -4
  47. package/dist-cjs/index.js +1 -4
  48. package/dist-cjs/install/build-add-context-rule.d.ts +3 -19
  49. package/dist-cjs/install/build-add-context-rule.js +1 -97
  50. package/dist-cjs/install/build-install-policy.d.ts +4 -53
  51. package/dist-cjs/install/build-install-policy.js +44 -122
  52. package/dist-cjs/install/index.d.ts +0 -2
  53. package/dist-cjs/install/index.js +2 -23
  54. package/dist-cjs/predicate/decode.d.ts +1 -1
  55. package/dist-cjs/predicate/decode.js +2 -70
  56. package/dist-cjs/predicate/encode.js +45 -229
  57. package/dist-cjs/predicate/from-json.js +1 -42
  58. package/dist-cjs/record/decode.js +3 -8
  59. package/dist-cjs/record/freshness.d.ts +17 -11
  60. package/dist-cjs/record/freshness.js +28 -18
  61. package/dist-cjs/review-card/builder.d.ts +4 -3
  62. package/dist-cjs/review-card/builder.js +23 -167
  63. package/dist-cjs/review-card/cross-check.js +7 -110
  64. package/dist-cjs/review-card/render-leaf.d.ts +4 -0
  65. package/dist-cjs/review-card/render-leaf.js +52 -0
  66. package/dist-cjs/run/index.d.ts +36 -139
  67. package/dist-cjs/run/index.js +125 -444
  68. package/dist-cjs/run/schemas.d.ts +168 -1929
  69. package/dist-cjs/run/schemas.js +41 -270
  70. package/dist-cjs/{synth → simulate}/deny-cases.d.ts +4 -8
  71. package/dist-cjs/simulate/deny-cases.js +286 -0
  72. package/dist-cjs/simulate/evaluate.d.ts +18 -0
  73. package/dist-cjs/simulate/evaluate.js +274 -0
  74. package/dist-cjs/simulate/index.d.ts +4 -0
  75. package/dist-cjs/simulate/index.js +13 -0
  76. package/dist-cjs/synth/compose-from-recording.d.ts +24 -51
  77. package/dist-cjs/synth/compose-from-recording.js +120 -282
  78. package/dist-cjs/synth/index.d.ts +0 -5
  79. package/dist-cjs/synth/index.js +1 -11
  80. package/dist-cjs/synth/synthesize-from-recording.d.ts +1 -29
  81. package/dist-cjs/synth/synthesize-from-recording.js +39 -354
  82. package/dist-cjs/types.d.ts +8 -60
  83. package/dist-cjs/types.js +6 -3
  84. package/package.json +5 -5
  85. package/src/adapters/interpreter/adapter.ts +76 -572
  86. package/src/errors.ts +0 -19
  87. package/src/index.ts +1 -4
  88. package/src/install/build-add-context-rule.ts +5 -139
  89. package/src/install/build-install-policy.ts +93 -214
  90. package/src/install/index.ts +0 -34
  91. package/src/predicate/decode.ts +2 -70
  92. package/src/predicate/encode.ts +49 -261
  93. package/src/predicate/from-json.ts +1 -43
  94. package/src/record/decode.ts +3 -8
  95. package/src/record/freshness.ts +29 -18
  96. package/src/review-card/builder.ts +19 -168
  97. package/src/review-card/cross-check.ts +3 -105
  98. package/src/review-card/render-leaf.ts +48 -0
  99. package/src/run/index.ts +144 -572
  100. package/src/run/schemas.ts +42 -291
  101. package/src/simulate/deny-cases.ts +334 -0
  102. package/src/simulate/evaluate.ts +284 -0
  103. package/src/simulate/index.ts +11 -0
  104. package/src/synth/compose-from-recording.ts +148 -347
  105. package/src/synth/index.ts +0 -13
  106. package/src/synth/synthesize-from-recording.ts +43 -456
  107. package/src/types.ts +13 -49
  108. package/dist/adapters/interpreter/index.d.ts +0 -1
  109. package/dist/adapters/interpreter/index.js +0 -2
  110. package/dist/adapters/oz/adapter.d.ts +0 -20
  111. package/dist/adapters/oz/adapter.js +0 -295
  112. package/dist/adapters/oz/index.d.ts +0 -1
  113. package/dist/adapters/oz/index.js +0 -2
  114. package/dist/codegen/compile-gate.d.ts +0 -33
  115. package/dist/codegen/compile-gate.js +0 -124
  116. package/dist/codegen/index.d.ts +0 -2
  117. package/dist/codegen/index.js +0 -8
  118. package/dist/codegen/template.d.ts +0 -18
  119. package/dist/codegen/template.js +0 -148
  120. package/dist/install/authority-overlap.d.ts +0 -134
  121. package/dist/install/authority-overlap.js +0 -0
  122. package/dist/install/build-install-predicate.d.ts +0 -96
  123. package/dist/install/build-install-predicate.js +0 -444
  124. package/dist/install/build-merge-policy.d.ts +0 -70
  125. package/dist/install/build-merge-policy.js +0 -130
  126. package/dist/install/plan-merge-policy.d.ts +0 -49
  127. package/dist/install/plan-merge-policy.js +0 -86
  128. package/dist/install/read-account-rules.d.ts +0 -100
  129. package/dist/install/read-account-rules.js +0 -283
  130. package/dist/ir/index.d.ts +0 -1
  131. package/dist/ir/index.js +0 -2
  132. package/dist/ir/types.d.ts +0 -140
  133. package/dist/ir/types.js +0 -11
  134. package/dist/mandate/index.d.ts +0 -2
  135. package/dist/mandate/index.js +0 -2
  136. package/dist/mandate/to-ir.d.ts +0 -3
  137. package/dist/mandate/to-ir.js +0 -60
  138. package/dist/mandate/types.d.ts +0 -20
  139. package/dist/mandate/types.js +0 -8
  140. package/dist/seams/index.d.ts +0 -1
  141. package/dist/seams/index.js +0 -2
  142. package/dist/seams/types.d.ts +0 -66
  143. package/dist/seams/types.js +0 -11
  144. package/dist/synth/deny-cases.js +0 -562
  145. package/dist/synth/evaluate.d.ts +0 -39
  146. package/dist/synth/evaluate.js +0 -551
  147. package/dist/synth/harness.d.ts +0 -28
  148. package/dist/synth/harness.js +0 -47
  149. package/dist/synth/minimize.d.ts +0 -4
  150. package/dist/synth/minimize.js +0 -38
  151. package/dist/synth/permit-context.d.ts +0 -15
  152. package/dist/synth/permit-context.js +0 -116
  153. package/dist/synth/predicate-literals.d.ts +0 -5
  154. package/dist/synth/predicate-literals.js +0 -25
  155. package/dist/synth/synthesize-from-mandate.d.ts +0 -20
  156. package/dist/synth/synthesize-from-mandate.js +0 -59
  157. package/dist/verify/envelope.d.ts +0 -15
  158. package/dist/verify/envelope.js +0 -22
  159. package/dist/verify/index.d.ts +0 -3
  160. package/dist/verify/index.js +0 -3
  161. package/dist/verify/simulate.d.ts +0 -31
  162. package/dist/verify/simulate.js +0 -258
  163. package/dist/verify/verify.d.ts +0 -21
  164. package/dist/verify/verify.js +0 -189
  165. package/dist-cjs/adapters/interpreter/index.d.ts +0 -1
  166. package/dist-cjs/adapters/interpreter/index.js +0 -8
  167. package/dist-cjs/adapters/oz/adapter.d.ts +0 -20
  168. package/dist-cjs/adapters/oz/adapter.js +0 -300
  169. package/dist-cjs/adapters/oz/index.d.ts +0 -1
  170. package/dist-cjs/adapters/oz/index.js +0 -8
  171. package/dist-cjs/codegen/compile-gate.d.ts +0 -33
  172. package/dist-cjs/codegen/compile-gate.js +0 -128
  173. package/dist-cjs/codegen/index.d.ts +0 -2
  174. package/dist-cjs/codegen/index.js +0 -14
  175. package/dist-cjs/codegen/template.d.ts +0 -18
  176. package/dist-cjs/codegen/template.js +0 -151
  177. package/dist-cjs/install/authority-overlap.d.ts +0 -134
  178. package/dist-cjs/install/authority-overlap.js +0 -0
  179. package/dist-cjs/install/build-install-predicate.d.ts +0 -96
  180. package/dist-cjs/install/build-install-predicate.js +0 -479
  181. package/dist-cjs/install/build-merge-policy.d.ts +0 -70
  182. package/dist-cjs/install/build-merge-policy.js +0 -134
  183. package/dist-cjs/install/plan-merge-policy.d.ts +0 -49
  184. package/dist-cjs/install/plan-merge-policy.js +0 -90
  185. package/dist-cjs/install/read-account-rules.d.ts +0 -100
  186. package/dist-cjs/install/read-account-rules.js +0 -296
  187. package/dist-cjs/ir/index.d.ts +0 -1
  188. package/dist-cjs/ir/index.js +0 -3
  189. package/dist-cjs/ir/types.d.ts +0 -140
  190. package/dist-cjs/ir/types.js +0 -12
  191. package/dist-cjs/mandate/index.d.ts +0 -2
  192. package/dist-cjs/mandate/index.js +0 -6
  193. package/dist-cjs/mandate/to-ir.d.ts +0 -3
  194. package/dist-cjs/mandate/to-ir.js +0 -63
  195. package/dist-cjs/mandate/types.d.ts +0 -20
  196. package/dist-cjs/mandate/types.js +0 -9
  197. package/dist-cjs/seams/index.d.ts +0 -1
  198. package/dist-cjs/seams/index.js +0 -3
  199. package/dist-cjs/seams/types.d.ts +0 -66
  200. package/dist-cjs/seams/types.js +0 -12
  201. package/dist-cjs/synth/deny-cases.js +0 -568
  202. package/dist-cjs/synth/evaluate.d.ts +0 -39
  203. package/dist-cjs/synth/evaluate.js +0 -554
  204. package/dist-cjs/synth/harness.d.ts +0 -28
  205. package/dist-cjs/synth/harness.js +0 -50
  206. package/dist-cjs/synth/minimize.d.ts +0 -4
  207. package/dist-cjs/synth/minimize.js +0 -41
  208. package/dist-cjs/synth/permit-context.d.ts +0 -15
  209. package/dist-cjs/synth/permit-context.js +0 -119
  210. package/dist-cjs/synth/predicate-literals.d.ts +0 -5
  211. package/dist-cjs/synth/predicate-literals.js +0 -28
  212. package/dist-cjs/synth/synthesize-from-mandate.d.ts +0 -20
  213. package/dist-cjs/synth/synthesize-from-mandate.js +0 -62
  214. package/dist-cjs/verify/envelope.d.ts +0 -15
  215. package/dist-cjs/verify/envelope.js +0 -23
  216. package/dist-cjs/verify/index.d.ts +0 -3
  217. package/dist-cjs/verify/index.js +0 -8
  218. package/dist-cjs/verify/simulate.d.ts +0 -31
  219. package/dist-cjs/verify/simulate.js +0 -261
  220. package/dist-cjs/verify/verify.d.ts +0 -21
  221. package/dist-cjs/verify/verify.js +0 -192
  222. package/src/adapters/interpreter/index.ts +0 -8
  223. package/src/adapters/oz/adapter.ts +0 -376
  224. package/src/adapters/oz/index.ts +0 -9
  225. package/src/codegen/compile-gate.ts +0 -167
  226. package/src/codegen/index.ts +0 -17
  227. package/src/codegen/template.ts +0 -165
  228. package/src/install/authority-overlap.ts +0 -0
  229. package/src/install/build-merge-policy.ts +0 -219
  230. package/src/install/plan-merge-policy.ts +0 -133
  231. package/src/install/read-account-rules.ts +0 -376
  232. package/src/ir/index.ts +0 -13
  233. package/src/ir/types.ts +0 -132
  234. package/src/mandate/index.ts +0 -4
  235. package/src/mandate/to-ir.ts +0 -71
  236. package/src/mandate/types.ts +0 -21
  237. package/src/seams/index.ts +0 -11
  238. package/src/seams/types.ts +0 -81
  239. package/src/synth/deny-cases.ts +0 -663
  240. package/src/synth/evaluate.ts +0 -613
  241. package/src/synth/harness.ts +0 -68
  242. package/src/synth/minimize.ts +0 -48
  243. package/src/synth/permit-context.ts +0 -136
  244. package/src/synth/predicate-literals.ts +0 -27
  245. package/src/synth/synthesize-from-mandate.ts +0 -82
  246. package/src/verify/envelope.ts +0 -28
  247. package/src/verify/index.ts +0 -5
  248. package/src/verify/simulate.ts +0 -311
  249. package/src/verify/verify.ts +0 -243
@@ -1,551 +0,0 @@
1
- // src/synth/evaluate.ts - the model evaluator (TS model of interpreter semantics).
2
- //
3
- // Pure function. Determinism: same `(predicate, ctx)` -> byte-identical result,
4
- // no clock, no randomness. Deny order (deny on FIRST violation, stable reason):
5
- // 1. ledger expiry -> 'EXPIRED'
6
- // 2. `now` past valid_until -> 'EXPIRED'
7
- // 3. contract mismatch -> 'CONTRACT_SCOPE'
8
- // 4. per-ScVal equality on fn/args; EXACT ordered vector
9
- // equality; fail-closed on opaque args -> 'ARG_MISMATCH' / 'FN_MISMATCH'
10
- // 5. `in` membership; empty haystack ALWAYS denies -> 'NOT_IN_ALLOWLIST'
11
- // 6. amount / window_spent via BigInt -> 'AMOUNT_BOUND'
12
- // 7. invocation_count_in_window -> 'FREQUENCY'
13
- // 8. oracle_price: fatal -> throw OracleError, deny with
14
- // underlying ORACLE_* reason -> 'ORACLE_*'
15
- // 9. boolean nodes (and/or/not)
16
- // 10. signer threshold gate -> 'THRESHOLD_NOT_MET'
17
- // 11. otherwise permit.
18
- //
19
- // Amounts: BigInt on decimal strings. Oracle errors thrown (never
20
- // boolean-false `not`/`or` could mask).
21
- import { literalNumericBigInt } from "./predicate-literals.js";
22
- /** Oracle prices normalise to this many decimals; mirrors NORMALISED_DECIMALS
23
- * in oracle.rs. A threshold on any other basis must say so. */
24
- const NORMALISED_DECIMALS = 9;
25
- /** Mirrors MAX_ORACLE_THRESHOLD_DECIMALS in dsl.rs. */
26
- const MAX_ORACLE_THRESHOLD_DECIMALS = 18;
27
- /** Internal fatal thrown by the oracle path; caught at the top of `evaluate`
28
- * and converted to the matching `ORACLE_*` deny reason. NOT a boolean-false
29
- * that `not` / `or` could mask. */
30
- class OracleError extends Error {
31
- code;
32
- constructor(code) {
33
- super(`oracle fatal: ${code}`);
34
- this.code = code;
35
- }
36
- }
37
- const ORACLE_ERROR_CODES = {
38
- stale: 'ORACLE_STALE',
39
- missing: 'ORACLE_MISSING',
40
- deviation: 'ORACLE_DEVIATION_EXCEEDED',
41
- paused: 'ORACLE_PAUSED',
42
- decimals: 'ORACLE_DECIMALS_MISMATCH',
43
- fingerprint: 'ORACLE_FINGERPRINT_DRIFT',
44
- };
45
- /** Evaluate a `PredicateNode` against the candidate call described by `ctx`.
46
- * Pure function. Returns `{ permit: true }` or `{ permit: false; reason }`. */
47
- export function evaluate(predicate, ctx) {
48
- try {
49
- // --- step 1: ledger-time expiry ---
50
- if (ctx.validUntilLedger !== undefined && ctx.atLedger > ctx.validUntilLedger) {
51
- return { permit: false, reason: 'EXPIRED' };
52
- }
53
- // --- step 2..9: predicate tree ---
54
- const decision = walk(predicate, ctx);
55
- // --- step 10: signer threshold gate (only when the predicate permitted) ---
56
- if (decision.permit && ctx.signerWeights !== undefined) {
57
- let totalWeight = 0n;
58
- for (const w of Object.values(ctx.signerWeights)) {
59
- // weights are non-negative integers; BigInt keeps the gate bounded
60
- totalWeight += BigInt(w);
61
- }
62
- if (totalWeight === 0n)
63
- return { permit: false, reason: 'THRESHOLD_NOT_MET' };
64
- }
65
- return decision;
66
- }
67
- catch (e) {
68
- if (e instanceof OracleError) {
69
- return { permit: false, reason: e.code };
70
- }
71
- throw e;
72
- }
73
- }
74
- /** Walk the predicate tree. Returns the FIRST deny reason encountered on
75
- * the active branch (so `and` fails-fast; `or` accepts the first permit). */
76
- function walk(node, ctx) {
77
- switch (node.op) {
78
- case 'and': {
79
- let lastDeny = null;
80
- for (const child of node.children) {
81
- const r = walk(child, ctx);
82
- if (!r.permit) {
83
- // deny-on-first: short-circuit on the failing child.
84
- return r;
85
- }
86
- lastDeny = r;
87
- }
88
- return lastDeny ?? { permit: true };
89
- }
90
- case 'or': {
91
- let lastDeny = null;
92
- for (const child of node.children) {
93
- const r = walk(child, ctx);
94
- if (r.permit)
95
- return r;
96
- lastDeny = r;
97
- }
98
- return lastDeny ?? { permit: false, reason: 'NOT_IN_ALLOWLIST' };
99
- }
100
- case 'not': {
101
- // `not` structurally inverts the child unless the child contains an
102
- // oracle leaf (compile-time rule: no oracle leaf under not/or). A
103
- // child `OracleError` is re-thrown to the catch at the top of `evaluate`.
104
- const r = walk(node.child, ctx);
105
- if (r.permit)
106
- return { permit: false, reason: 'FN_MISMATCH' };
107
- return { permit: true };
108
- }
109
- case 'eq':
110
- case 'lt':
111
- case 'lte':
112
- case 'gt':
113
- case 'gte':
114
- return evalCompare(node.op, node.left, node.right, ctx);
115
- case 'in':
116
- return evalIn(node.needle, node.haystack, ctx);
117
- }
118
- }
119
- /** Step 2..8: comparison leaf evaluation. */
120
- function evalCompare(op, left, right, ctx) {
121
- // --- step 2: `now` vs `valid_until` semantics ---
122
- if (left.kind === 'now' && right.kind === 'valid_until') {
123
- const expired = op === 'gt' || op === 'gte' ? ctx.nowSeconds >= 0 : ctx.nowSeconds < 0;
124
- // `valid_until` is modelled as a synthetic future timestamp far past
125
- // `nowSeconds` so the only true-positive expired path is the `gt`/`gte`
126
- // shapes callers actually write.
127
- if (op === 'gt' || op === 'gte') {
128
- if (ctx.nowSeconds > 0)
129
- return { permit: false, reason: 'EXPIRED' };
130
- }
131
- else if (op === 'lt' || op === 'lte') {
132
- return { permit: true };
133
- }
134
- return expired ? { permit: false, reason: 'EXPIRED' } : { permit: true };
135
- }
136
- if (right.kind === 'now' && left.kind === 'valid_until') {
137
- return evalCompare(op, right, left, ctx);
138
- }
139
- // --- step 3: CONTRACT_SCOPE on call_contract eq ---
140
- if (left.kind === 'call_contract' && op === 'eq') {
141
- if (right.kind !== 'literal_address')
142
- return { permit: false, reason: 'CONTRACT_SCOPE' };
143
- return right.value === ctx.contract
144
- ? { permit: true }
145
- : { permit: false, reason: 'CONTRACT_SCOPE' };
146
- }
147
- // --- step 4a: call_fn equality ---
148
- if (left.kind === 'call_fn' && op === 'eq') {
149
- if (right.kind !== 'literal_symbol')
150
- return { permit: false, reason: 'FN_MISMATCH' };
151
- return right.value === ctx.fn ? { permit: true } : { permit: false, reason: 'FN_MISMATCH' };
152
- }
153
- // --- step 4b: call_arg comparison (eq / exact-vec, or an ordered numeric bound) ---
154
- if (left.kind === 'call_arg') {
155
- // The swap's canonical form is `call_arg[out] >= call_arg_scaled(in, num, den)`.
156
- // Dispatched here so the floor's dedicated reason codes (ARITHMETIC_OVERFLOW
157
- // -> SLIPPAGE_FLOOR) reach the user; routing the scaled RHS through the
158
- // generic `evalArgOrderedCompare` would mask overflow as ARG_MISMATCH.
159
- if (right.kind === 'call_arg_scaled') {
160
- return evalScaledArgCompare(op, left, right, ctx);
161
- }
162
- const actual = ctx.args[left.index];
163
- if (op !== 'eq')
164
- return evalArgOrderedCompare(op, actual, right);
165
- return evalArgEq(op, actual, right, ctx);
166
- }
167
- // --- step 4b': scaled leaf on the LEFT (`call_arg_scaled(in, num, den) <= call_arg[out]`).
168
- // Symmetric form so a policy that phrases the floor either way works.
169
- if (left.kind === 'call_arg_scaled') {
170
- return evalScaledArgCompare(op, right, left, ctx);
171
- }
172
- // --- step 4c: call_arg_len: length of a vec-typed argument as u32.
173
- // Fails closed on a non-vec / absent arg or a non-u32 literal.
174
- if (left.kind === 'call_arg_len') {
175
- const actual = ctx.args[left.index];
176
- if (actual?.type !== 'vec')
177
- return { permit: false, reason: 'ARG_MISMATCH' };
178
- if (right.kind !== 'literal_u32')
179
- return { permit: false, reason: 'ARG_MISMATCH' };
180
- return actual.value.length === right.value
181
- ? { permit: true }
182
- : { permit: false, reason: 'ARG_MISMATCH' };
183
- }
184
- // --- step 4d: call_arg_field: value of a field in the map at element i of
185
- // the vec at argument index. Fails closed on shape / type / range issues.
186
- if (left.kind === 'call_arg_field') {
187
- const actual = ctx.args[left.index];
188
- if (actual?.type !== 'vec')
189
- return { permit: false, reason: 'ARG_MISMATCH' };
190
- const element = actual.value[left.element];
191
- if (element?.type !== 'map')
192
- return { permit: false, reason: 'ARG_MISMATCH' };
193
- if (!Array.isArray(element.value))
194
- return { permit: false, reason: 'ARG_MISMATCH' };
195
- const entry = element.value.find((e) => e.key === left.field);
196
- if (!entry)
197
- return { permit: false, reason: 'ARG_MISMATCH' };
198
- if (op === 'eq')
199
- return evalArgEq(op, entry.val, right, ctx);
200
- return evalArgOrderedCompare(op, entry.val, right);
201
- }
202
- // --- step 6: AMOUNT_BOUND ---
203
- if (left.kind === 'amount' && op !== 'eq') {
204
- return evalAmountCompare(op, left.token, right, ctx, 'amountByToken');
205
- }
206
- if (left.kind === 'window_spent' && op !== 'eq') {
207
- return evalWindowSpentCompare(op, left.token, right, ctx);
208
- }
209
- // `eq` on amount / window_spent not defined as a bound - fall through.
210
- // --- step 7: FREQUENCY ---
211
- if (left.kind === 'invocation_count_in_window') {
212
- return evalFrequencyCompare(op, left.windowSecs, right, ctx);
213
- }
214
- // --- step 8: oracle_price (FATAL via throw) ---
215
- if (left.kind === 'oracle_price') {
216
- return evalOracleCompare(op, left.asset, right, ctx);
217
- }
218
- // Unknown leaf/op combination - structural fail-closed.
219
- return { permit: false, reason: 'FN_MISMATCH' };
220
- }
221
- /** Step 4b: per-ScVal equality. Handles literal_vec as an EXACT ordered
222
- * sequence: compare element-by-element in order; deny if length or any
223
- * element differs. Opaque args (`type: 'other'`) fail closed. */
224
- function evalArgEq(op, actual, right, ctx) {
225
- // eq(call_arg[i], literal_vec) -> EXACT ordered vector equality.
226
- if (op === 'eq' && right.kind === 'literal_vec') {
227
- if (actual?.type !== 'vec')
228
- return { permit: false, reason: 'ARG_MISMATCH' };
229
- return compareVecExact(actual.value, right.elements, ctx)
230
- ? { permit: true }
231
- : { permit: false, reason: 'ARG_MISMATCH' };
232
- }
233
- if (op === 'eq' && right.kind === 'literal_address') {
234
- if (!actual)
235
- return { permit: false, reason: 'ARG_MISMATCH' };
236
- if (actual.type === 'other')
237
- return { permit: false, reason: 'ARG_MISMATCH' };
238
- return actual.type === 'address' && actual.value === right.value
239
- ? { permit: true }
240
- : { permit: false, reason: 'ARG_MISMATCH' };
241
- }
242
- if (op === 'eq' && right.kind === 'literal_i128') {
243
- if (actual?.type !== 'i128')
244
- return { permit: false, reason: 'ARG_MISMATCH' };
245
- return BigInt(actual.value) === BigInt(right.value)
246
- ? { permit: true }
247
- : { permit: false, reason: 'ARG_MISMATCH' };
248
- }
249
- if (op === 'eq' && right.kind === 'literal_symbol') {
250
- if (!actual)
251
- return { permit: false, reason: 'ARG_MISMATCH' };
252
- return actual.type === 'symbol' && actual.value === right.value
253
- ? { permit: true }
254
- : { permit: false, reason: 'ARG_MISMATCH' };
255
- }
256
- if (op === 'eq' && right.kind === 'literal_u32') {
257
- if (!actual)
258
- return { permit: false, reason: 'ARG_MISMATCH' };
259
- return actual.type === 'u32' && actual.value === String(right.value)
260
- ? { permit: true }
261
- : { permit: false, reason: 'ARG_MISMATCH' };
262
- }
263
- if (op === 'eq' && right.kind === 'literal_u64') {
264
- if (!actual)
265
- return { permit: false, reason: 'ARG_MISMATCH' };
266
- return actual.type === 'u64' && actual.value === right.value
267
- ? { permit: true }
268
- : { permit: false, reason: 'ARG_MISMATCH' };
269
- }
270
- if (op === 'eq' && right.kind === 'literal_bytes') {
271
- if (!actual)
272
- return { permit: false, reason: 'ARG_MISMATCH' };
273
- return actual.type === 'bytes' && actual.value === right.value
274
- ? { permit: true }
275
- : { permit: false, reason: 'ARG_MISMATCH' };
276
- }
277
- // Anything else: fail closed on opacity (cannot decode the arg reliably).
278
- if (!actual || actual.type === 'other')
279
- return { permit: false, reason: 'ARG_MISMATCH' };
280
- return { permit: false, reason: 'ARG_MISMATCH' };
281
- }
282
- /** Step 4b': slippage-floor comparison. Mirrors the Rust `eval_scaled_arg_compare`:
283
- * the scaled leaf is `args[index] * num / den` (truncating toward zero). On
284
- * overflow or divide-by-zero deny with `ARITHMETIC_OVERFLOW`; a failed bound
285
- * denies with `SLIPPAGE_FLOOR` (the dedicated reason) rather than the generic
286
- * `ARG_MISMATCH`. A scaled-on-scaled compare denies `ARG_MISMATCH`
287
- * (no definable semantics). The non-scaled operand must be numeric
288
- * (`call_arg` carrying a number, or a numeric literal); else `ARG_MISMATCH`. */
289
- function evalScaledArgCompare(op, left, right, ctx) {
290
- let scaled;
291
- let other;
292
- let scaledOnRight;
293
- if (left.kind === 'call_arg_scaled') {
294
- scaled = left;
295
- other = right;
296
- scaledOnRight = false;
297
- }
298
- else if (right.kind === 'call_arg_scaled') {
299
- scaled = right;
300
- other = left;
301
- scaledOnRight = true;
302
- }
303
- else {
304
- // Programming error in the dispatcher; fail closed.
305
- return { permit: false, reason: 'ARG_MISMATCH' };
306
- }
307
- // Out-of-bounds or non-numeric arg fails closed as ARG_MISMATCH (not
308
- // SLIPPAGE_FLOOR) - a violated floor is the wrong code when the operand
309
- // itself could not be read.
310
- if (scaled.index >= ctx.args.length) {
311
- return { permit: false, reason: 'ARG_MISMATCH' };
312
- }
313
- const input = argNumericBigInt(ctx.args[scaled.index]);
314
- if (input === null)
315
- return { permit: false, reason: 'ARG_MISMATCH' };
316
- let num;
317
- let den;
318
- try {
319
- num = BigInt(scaled.num);
320
- den = BigInt(scaled.den);
321
- }
322
- catch {
323
- return { permit: false, reason: 'ARG_MISMATCH' };
324
- }
325
- // Install refuses `den == 0` and `num <= 0` / `den <= 0`. The runtime
326
- // check is defensive belt-and-braces so a validator regression cannot
327
- // panic the frame on a divide-by-zero.
328
- if (den === 0n)
329
- return { permit: false, reason: 'ARITHMETIC_OVERFLOW' };
330
- const product = input * num;
331
- let scaledValue;
332
- try {
333
- // BigInt division truncates toward zero (matches Rust `i128::checked_div`).
334
- scaledValue = product / den;
335
- }
336
- catch {
337
- return { permit: false, reason: 'ARITHMETIC_OVERFLOW' };
338
- }
339
- // i128 range check: the contract would have wrapped on i128 arithmetic;
340
- // surface the same deny.
341
- const I128_MAX = (1n << 127n) - 1n;
342
- const I128_MIN = -(1n << 127n);
343
- if (scaledValue > I128_MAX || scaledValue < I128_MIN) {
344
- return { permit: false, reason: 'ARITHMETIC_OVERFLOW' };
345
- }
346
- let otherVal;
347
- if (other.kind === 'call_arg') {
348
- otherVal = argNumericBigInt(ctx.args[other.index]);
349
- }
350
- else {
351
- otherVal = literalNumericBigInt(other);
352
- }
353
- if (otherVal === null)
354
- return { permit: false, reason: 'ARG_MISMATCH' };
355
- const pass = scaledOnRight
356
- ? bigintCmp(op, otherVal.toString(), scaledValue.toString())
357
- : bigintCmp(op, scaledValue.toString(), otherVal.toString());
358
- return pass ? { permit: true } : { permit: false, reason: 'SLIPPAGE_FLOOR' };
359
- }
360
- /** Ordered numeric comparison (lt/lte/gt/gte) on a `call_arg`. The interpreter
361
- * reads the arg as an integer (i128 / u64 / u32 on the recorder's ScVal
362
- * surface) and compares it to a numeric literal via BigInt. A non-numeric arg
363
- * or a non-numeric literal fails closed (ARG_MISMATCH) rather than permitting
364
- * an undecidable bound. Backs the SoroSwap input-amount cap
365
- * (`call_arg[0] <= limit`). */
366
- function evalArgOrderedCompare(op, actual, right) {
367
- const actualInt = argNumericBigInt(actual);
368
- const literalInt = literalNumericBigInt(right);
369
- if (actualInt === null || literalInt === null) {
370
- return { permit: false, reason: 'ARG_MISMATCH' };
371
- }
372
- return bigintCmp(op, actualInt.toString(), literalInt.toString())
373
- ? { permit: true }
374
- : { permit: false, reason: 'ARG_MISMATCH' };
375
- }
376
- /** BigInt value of a numeric-integer ScVal arg (i128 / u64 / u32), or null when
377
- * the arg is absent, opaque, or a non-numeric type. */
378
- function argNumericBigInt(actual) {
379
- if (!actual)
380
- return null;
381
- if (actual.type === 'i128' || actual.type === 'u64' || actual.type === 'u32') {
382
- try {
383
- return BigInt(actual.value);
384
- }
385
- catch {
386
- return null;
387
- }
388
- }
389
- return null;
390
- }
391
- /** Element-by-element ordered comparison of an `ScVal[]` against a
392
- * `PredicateLeaf[]`. Equal-length and equal at every index => permit.
393
- * Length mismatch OR any element mismatch => deny ARG_MISMATCH. */
394
- function compareVecExact(actual, expected, ctx) {
395
- if (actual.length !== expected.length)
396
- return false;
397
- for (let i = 0; i < expected.length; i++) {
398
- const e = expected[i];
399
- const a = actual[i];
400
- if (!e || !a)
401
- return false;
402
- const r = evalArgEq('eq', a, e, ctx);
403
- if (!r.permit)
404
- return false;
405
- }
406
- return true;
407
- }
408
- /** Step 5: `in` membership. Empty haystack ALWAYS denies. Opaque needle
409
- * fails closed. */
410
- function evalIn(needle, haystack, ctx) {
411
- if (haystack.length === 0)
412
- return { permit: false, reason: 'NOT_IN_ALLOWLIST' };
413
- const actual = resolveLeaf(needle, ctx);
414
- if (!actual || actual.type === 'other')
415
- return { permit: false, reason: 'NOT_IN_ALLOWLIST' };
416
- for (const h of haystack) {
417
- const r = evalArgEq('eq', actual, h, ctx);
418
- if (r.permit)
419
- return { permit: true };
420
- }
421
- return { permit: false, reason: 'NOT_IN_ALLOWLIST' };
422
- }
423
- /** Resolve a selector leaf to its current ScVal against the candidate call. */
424
- function resolveLeaf(leaf, ctx) {
425
- switch (leaf.kind) {
426
- case 'call_contract':
427
- return { type: 'address', value: ctx.contract };
428
- case 'call_fn':
429
- return { type: 'symbol', value: ctx.fn };
430
- case 'call_arg':
431
- return ctx.args[leaf.index];
432
- case 'call_arg_len':
433
- // No direct ScVal projection: the length is an integer the comparator
434
- // resolves against the right-hand literal. Returning undefined keeps
435
- // the `in` membership path structurally informed (no haystack match).
436
- return undefined;
437
- case 'call_arg_field': {
438
- const actual = ctx.args[leaf.index];
439
- if (actual?.type !== 'vec')
440
- return undefined;
441
- const element = actual.value[leaf.element];
442
- if (element?.type !== 'map')
443
- return undefined;
444
- if (!Array.isArray(element.value))
445
- return undefined;
446
- const entry = element.value.find((e) => e.key === leaf.field);
447
- return entry ? entry.val : undefined;
448
- }
449
- case 'amount':
450
- case 'window_spent':
451
- case 'oracle_price':
452
- case 'invocation_count_in_window':
453
- case 'now':
454
- case 'valid_until':
455
- case 'call_arg_scaled':
456
- return undefined; // selector leaves with no ScVal projection
457
- case 'literal_address':
458
- return { type: 'address', value: leaf.value };
459
- case 'literal_i128':
460
- return { type: 'i128', value: leaf.value };
461
- case 'literal_symbol':
462
- return { type: 'symbol', value: leaf.value };
463
- case 'literal_u32':
464
- return { type: 'u32', value: String(leaf.value) };
465
- case 'literal_u64':
466
- return { type: 'u64', value: leaf.value };
467
- case 'literal_bytes':
468
- return { type: 'bytes', value: leaf.value };
469
- case 'literal_vec':
470
- return undefined;
471
- }
472
- }
473
- /** Step 6: amount / window_spent compare on BigInt. Both leaves compare a
474
- * per-token BigInt record (current vs. rolling) to an i128 literal; the
475
- * compare + denial reason are identical, so a single helper handles both. */
476
- function evalAmountCompare(op, token, right, ctx, record) {
477
- const literal = right.kind === 'literal_i128' ? right.value : null;
478
- if (literal === null)
479
- return { permit: false, reason: 'AMOUNT_BOUND' };
480
- const actual = ctx[record][token] ?? '0';
481
- return bigintCmp(op, actual, literal)
482
- ? { permit: true }
483
- : { permit: false, reason: 'AMOUNT_BOUND' };
484
- }
485
- /** Step 6: window_spent compare on BigInt. */
486
- function evalWindowSpentCompare(op, token, right, ctx) {
487
- return evalAmountCompare(op, token, right, ctx, 'windowSpentByToken');
488
- }
489
- /** Step 7: invocation_count_in_window compare. */
490
- function evalFrequencyCompare(op, windowSecs, right, ctx) {
491
- const literal = right.kind === 'literal_u32' ? String(right.value) : null;
492
- if (literal === null)
493
- return { permit: false, reason: 'FREQUENCY' };
494
- const actual = String(ctx.invocationCountByWindow[windowSecs] ?? 0);
495
- return bigintCmp(op, actual, literal) ? { permit: true } : { permit: false, reason: 'FREQUENCY' };
496
- }
497
- /** Step 8: oracle_price compare. Reads `ctx.oraclePriceByAsset[asset]`. Any
498
- * error entry OR a missing key throws `OracleError` (FATAL). A satisfied
499
- * compare permits. */
500
- function evalOracleCompare(op, asset, right, ctx) {
501
- const entry = ctx.oraclePriceByAsset[asset];
502
- if (!entry)
503
- throw new OracleError('ORACLE_STALE');
504
- if ('error' in entry) {
505
- const mapped = ORACLE_ERROR_CODES[entry.error];
506
- if (!mapped)
507
- throw new OracleError('ORACLE_STALE');
508
- throw new OracleError(mapped);
509
- }
510
- // Mirrors eval_oracle_compare in dsl.rs. The threshold MUST declare its
511
- // decimal basis: prices are on the normalised 9-dp basis, and a bare literal
512
- // is refused (not assumed) because a raw 14-dp threshold that assumed the
513
- // 9-dp basis would permit everything.
514
- if (right.kind !== 'oracle_threshold')
515
- throw new OracleError('ORACLE_DECIMALS_MISMATCH');
516
- if (right.decimals > MAX_ORACLE_THRESHOLD_DECIMALS) {
517
- throw new OracleError('ORACLE_THRESHOLD_DECIMALS_OUT_OF_RANGE');
518
- }
519
- // Scale BOTH sides up to the wider basis; dividing the threshold down
520
- // truncates and moves the permit boundary.
521
- const decimals = BigInt(right.decimals);
522
- const normalised = BigInt(NORMALISED_DECIMALS);
523
- const common = decimals > normalised ? decimals : normalised;
524
- const priceScaled = BigInt(entry.price) * 10n ** (common - normalised);
525
- const literalScaled = BigInt(right.value) * 10n ** (common - decimals);
526
- // A violated oracle threshold is a stateful bound (Rust dsl.rs:276 returns
527
- // `DenyReason::StatefulBound`, deny code #104). The TS evaluator must mirror
528
- // the same reason so a future TS/Rust reason divergence fails CI; the
529
- // cross-layer harness in this file now asserts reason codes match.
530
- return bigintCmp(op, String(priceScaled), String(literalScaled))
531
- ? { permit: true }
532
- : { permit: false, reason: 'STATEFUL_BOUND' };
533
- }
534
- /** BigInt compare helper. `eq` is also supported by callers (selector-vs-literal
535
- * equal checks), but the dedicated arg path uses it via `BigInt(actual) === BigInt(right)`. */
536
- function bigintCmp(op, aStr, bStr) {
537
- const a = BigInt(aStr);
538
- const b = BigInt(bStr);
539
- switch (op) {
540
- case 'eq':
541
- return a === b;
542
- case 'lt':
543
- return a < b;
544
- case 'lte':
545
- return a <= b;
546
- case 'gt':
547
- return a > b;
548
- case 'gte':
549
- return a >= b;
550
- }
551
- }
@@ -1,28 +0,0 @@
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
- /** When `expectedReason` is set on the deny case and the case actually
9
- * denies, this carries the canonical reason the TS evaluator MUST emit
10
- * to stay in lockstep with the Rust interpreter (the cross-layer
11
- * reason-code contract). A future TS/Rust reason divergence surfaces
12
- * here in CI. */
13
- expectedReason?: string;
14
- actualReason?: string;
15
- }
16
- export type HarnessResult = {
17
- ok: true;
18
- } | {
19
- ok: false;
20
- failures: HarnessFailure[];
21
- };
22
- /** Evaluate the intended call and every generated denial without side effects.
23
- * Asserts both the boolean decision AND, for deny cases that carry an
24
- * `expectedReason`, the concrete reason the TS evaluator emits. The reason
25
- * is what the Rust interpreter returns; the cross-layer conformance test in
26
- * `contracts/policy-interpreter/tests/conformance` mirrors the same contract
27
- * for the verifyLive path. */
28
- export declare function runHarness(predicate: PredicateNode, cases: GeneratedCases): HarnessResult;
@@ -1,47 +0,0 @@
1
- import { evaluate } from "./evaluate.js";
2
- /** Evaluate the intended call and every generated denial without side effects.
3
- * Asserts both the boolean decision AND, for deny cases that carry an
4
- * `expectedReason`, the concrete reason the TS evaluator emits. The reason
5
- * is what the Rust interpreter returns; the cross-layer conformance test in
6
- * `contracts/policy-interpreter/tests/conformance` mirrors the same contract
7
- * for the verifyLive path. */
8
- export function runHarness(predicate, cases) {
9
- const failures = [];
10
- const permitResult = evaluate(predicate, cases.permit);
11
- if (!permitResult.permit) {
12
- failures.push({
13
- dimension: 'PERMIT_CASE_FAILED',
14
- expected: 'permit',
15
- got: 'deny',
16
- reason: permitResult.reason,
17
- });
18
- }
19
- for (const deny of cases.denies) {
20
- const result = evaluate(predicate, deny.ctx);
21
- if (result.permit) {
22
- failures.push({
23
- dimension: deny.dimension,
24
- expected: 'deny',
25
- got: 'permit',
26
- reason: 'DENY_CASE_FAILURE',
27
- });
28
- continue;
29
- }
30
- // Reason-code assertion (cross-layer contract). When a deny case carries
31
- // an expected reason, the TS evaluator MUST emit that exact string; a
32
- // divergence means the TS model and the Rust interpreter disagree on
33
- // what the user sees in CI - the harness fails so the diff is visible
34
- // here, not at runtime in production.
35
- if (deny.expectedReason !== undefined && result.reason !== deny.expectedReason) {
36
- failures.push({
37
- dimension: deny.dimension,
38
- expected: 'deny',
39
- got: 'deny',
40
- reason: 'REASON_MISMATCH',
41
- expectedReason: deny.expectedReason,
42
- actualReason: result.reason,
43
- });
44
- }
45
- }
46
- return failures.length === 0 ? { ok: true } : { ok: false, failures };
47
- }
@@ -1,4 +0,0 @@
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, dimensions?: string[]): PredicateNode;
@@ -1,38 +0,0 @@
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, dimensions) {
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, dimensions);
12
- const candidateChildren = children.filter((_, childIndex) => childIndex !== index);
13
- const candidate = { op: 'and', children: candidateChildren };
14
- const candidateCases = generateCases(candidate, permitCtx, dimensions);
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
- }