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