@crediolabs/policy-synth 0.2.0 → 0.3.0

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 (184) 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/adapters/interpreter/index.d.ts +1 -1
  5. package/dist/adapters/interpreter/index.js +1 -1
  6. package/dist/adapters/oz/adapter.js +1 -15
  7. package/dist/errors.d.ts +1 -1
  8. package/dist/index.d.ts +1 -4
  9. package/dist/index.js +1 -4
  10. package/dist/install/build-add-context-rule.d.ts +3 -19
  11. package/dist/install/build-add-context-rule.js +3 -98
  12. package/dist/install/build-install-policy.d.ts +4 -53
  13. package/dist/install/build-install-policy.js +44 -123
  14. package/dist/install/index.d.ts +0 -2
  15. package/dist/install/index.js +0 -7
  16. package/dist/ir/index.d.ts +1 -1
  17. package/dist/ir/types.d.ts +7 -76
  18. package/dist/ir/types.js +0 -2
  19. package/dist/predicate/decode.d.ts +1 -1
  20. package/dist/predicate/decode.js +2 -70
  21. package/dist/predicate/encode.js +45 -229
  22. package/dist/predicate/from-json.js +1 -42
  23. package/dist/record/decode.js +3 -8
  24. package/dist/review-card/builder.d.ts +4 -3
  25. package/dist/review-card/builder.js +18 -162
  26. package/dist/review-card/cross-check.js +2 -105
  27. package/dist/review-card/render-leaf.d.ts +4 -0
  28. package/dist/review-card/render-leaf.js +47 -0
  29. package/dist/run/index.d.ts +36 -139
  30. package/dist/run/index.js +125 -437
  31. package/dist/run/schemas.d.ts +168 -1929
  32. package/dist/run/schemas.js +40 -269
  33. package/dist/seams/types.d.ts +0 -2
  34. package/dist/simulate/deny-cases.d.ts +26 -0
  35. package/dist/simulate/deny-cases.js +282 -0
  36. package/dist/simulate/evaluate.d.ts +18 -0
  37. package/dist/simulate/evaluate.js +271 -0
  38. package/dist/simulate/index.d.ts +4 -0
  39. package/dist/simulate/index.js +8 -0
  40. package/dist/synth/compose-from-recording.d.ts +24 -51
  41. package/dist/synth/compose-from-recording.js +120 -282
  42. package/dist/synth/deny-cases.d.ts +5 -7
  43. package/dist/synth/deny-cases.js +23 -373
  44. package/dist/synth/evaluate.d.ts +0 -21
  45. package/dist/synth/evaluate.js +56 -336
  46. package/dist/synth/index.d.ts +0 -5
  47. package/dist/synth/index.js +0 -5
  48. package/dist/synth/permit-context.d.ts +2 -9
  49. package/dist/synth/permit-context.js +1 -82
  50. package/dist/synth/synthesize-from-recording.d.ts +1 -29
  51. package/dist/synth/synthesize-from-recording.js +41 -356
  52. package/dist/types.d.ts +8 -60
  53. package/dist/types.js +5 -2
  54. package/dist/verify/simulate.d.ts +1 -14
  55. package/dist/verify/simulate.js +2 -88
  56. package/dist/verify/verify.d.ts +1 -7
  57. package/dist/verify/verify.js +0 -8
  58. package/dist-cjs/adapters/interpreter/adapter.d.ts +19 -25
  59. package/dist-cjs/adapters/interpreter/adapter.js +56 -489
  60. package/dist-cjs/adapters/interpreter/index.d.ts +1 -1
  61. package/dist-cjs/adapters/interpreter/index.js +1 -2
  62. package/dist-cjs/adapters/oz/adapter.js +1 -15
  63. package/dist-cjs/errors.d.ts +1 -1
  64. package/dist-cjs/index.d.ts +1 -4
  65. package/dist-cjs/index.js +1 -4
  66. package/dist-cjs/install/build-add-context-rule.d.ts +3 -19
  67. package/dist-cjs/install/build-add-context-rule.js +1 -97
  68. package/dist-cjs/install/build-install-policy.d.ts +4 -53
  69. package/dist-cjs/install/build-install-policy.js +44 -122
  70. package/dist-cjs/install/index.d.ts +0 -2
  71. package/dist-cjs/install/index.js +2 -23
  72. package/dist-cjs/ir/index.d.ts +1 -1
  73. package/dist-cjs/ir/types.d.ts +7 -76
  74. package/dist-cjs/ir/types.js +0 -2
  75. package/dist-cjs/predicate/decode.d.ts +1 -1
  76. package/dist-cjs/predicate/decode.js +2 -70
  77. package/dist-cjs/predicate/encode.js +45 -229
  78. package/dist-cjs/predicate/from-json.js +1 -42
  79. package/dist-cjs/record/decode.js +3 -8
  80. package/dist-cjs/review-card/builder.d.ts +4 -3
  81. package/dist-cjs/review-card/builder.js +23 -167
  82. package/dist-cjs/review-card/cross-check.js +7 -110
  83. package/dist-cjs/review-card/render-leaf.d.ts +4 -0
  84. package/dist-cjs/review-card/render-leaf.js +52 -0
  85. package/dist-cjs/run/index.d.ts +36 -139
  86. package/dist-cjs/run/index.js +125 -444
  87. package/dist-cjs/run/schemas.d.ts +168 -1929
  88. package/dist-cjs/run/schemas.js +41 -270
  89. package/dist-cjs/seams/types.d.ts +0 -2
  90. package/dist-cjs/simulate/deny-cases.d.ts +26 -0
  91. package/dist-cjs/simulate/deny-cases.js +286 -0
  92. package/dist-cjs/simulate/evaluate.d.ts +18 -0
  93. package/dist-cjs/simulate/evaluate.js +274 -0
  94. package/dist-cjs/simulate/index.d.ts +4 -0
  95. package/dist-cjs/simulate/index.js +13 -0
  96. package/dist-cjs/synth/compose-from-recording.d.ts +24 -51
  97. package/dist-cjs/synth/compose-from-recording.js +120 -282
  98. package/dist-cjs/synth/deny-cases.d.ts +5 -7
  99. package/dist-cjs/synth/deny-cases.js +23 -374
  100. package/dist-cjs/synth/evaluate.d.ts +0 -21
  101. package/dist-cjs/synth/evaluate.js +57 -337
  102. package/dist-cjs/synth/index.d.ts +0 -5
  103. package/dist-cjs/synth/index.js +1 -11
  104. package/dist-cjs/synth/permit-context.d.ts +2 -9
  105. package/dist-cjs/synth/permit-context.js +1 -82
  106. package/dist-cjs/synth/synthesize-from-recording.d.ts +1 -29
  107. package/dist-cjs/synth/synthesize-from-recording.js +39 -354
  108. package/dist-cjs/types.d.ts +8 -60
  109. package/dist-cjs/types.js +6 -3
  110. package/dist-cjs/verify/simulate.d.ts +1 -14
  111. package/dist-cjs/verify/simulate.js +2 -88
  112. package/dist-cjs/verify/verify.d.ts +1 -7
  113. package/dist-cjs/verify/verify.js +0 -8
  114. package/package.json +4 -4
  115. package/src/adapters/interpreter/adapter.ts +76 -572
  116. package/src/errors.ts +0 -19
  117. package/src/index.ts +1 -4
  118. package/src/install/build-add-context-rule.ts +5 -139
  119. package/src/install/build-install-policy.ts +93 -214
  120. package/src/install/index.ts +0 -34
  121. package/src/predicate/decode.ts +2 -70
  122. package/src/predicate/encode.ts +49 -261
  123. package/src/predicate/from-json.ts +1 -43
  124. package/src/record/decode.ts +3 -8
  125. package/src/review-card/builder.ts +19 -168
  126. package/src/review-card/cross-check.ts +3 -105
  127. package/src/review-card/render-leaf.ts +48 -0
  128. package/src/run/index.ts +144 -572
  129. package/src/run/schemas.ts +42 -291
  130. package/src/simulate/deny-cases.ts +334 -0
  131. package/src/simulate/evaluate.ts +284 -0
  132. package/src/simulate/index.ts +11 -0
  133. package/src/synth/compose-from-recording.ts +148 -347
  134. package/src/synth/index.ts +0 -13
  135. package/src/synth/synthesize-from-recording.ts +43 -456
  136. package/src/types.ts +13 -49
  137. package/dist/install/authority-overlap.d.ts +0 -134
  138. package/dist/install/authority-overlap.js +0 -0
  139. package/dist/install/build-install-predicate.d.ts +0 -96
  140. package/dist/install/build-install-predicate.js +0 -444
  141. package/dist/install/build-merge-policy.d.ts +0 -70
  142. package/dist/install/build-merge-policy.js +0 -130
  143. package/dist/install/plan-merge-policy.d.ts +0 -49
  144. package/dist/install/plan-merge-policy.js +0 -86
  145. package/dist/install/read-account-rules.d.ts +0 -100
  146. package/dist/install/read-account-rules.js +0 -283
  147. package/dist-cjs/install/authority-overlap.d.ts +0 -134
  148. package/dist-cjs/install/authority-overlap.js +0 -0
  149. package/dist-cjs/install/build-install-predicate.d.ts +0 -96
  150. package/dist-cjs/install/build-install-predicate.js +0 -479
  151. package/dist-cjs/install/build-merge-policy.d.ts +0 -70
  152. package/dist-cjs/install/build-merge-policy.js +0 -134
  153. package/dist-cjs/install/plan-merge-policy.d.ts +0 -49
  154. package/dist-cjs/install/plan-merge-policy.js +0 -90
  155. package/dist-cjs/install/read-account-rules.d.ts +0 -100
  156. package/dist-cjs/install/read-account-rules.js +0 -296
  157. package/src/adapters/interpreter/index.ts +0 -8
  158. package/src/adapters/oz/adapter.ts +0 -376
  159. package/src/adapters/oz/index.ts +0 -9
  160. package/src/codegen/compile-gate.ts +0 -167
  161. package/src/codegen/index.ts +0 -17
  162. package/src/codegen/template.ts +0 -165
  163. package/src/install/authority-overlap.ts +0 -0
  164. package/src/install/build-merge-policy.ts +0 -219
  165. package/src/install/plan-merge-policy.ts +0 -133
  166. package/src/install/read-account-rules.ts +0 -376
  167. package/src/ir/index.ts +0 -13
  168. package/src/ir/types.ts +0 -132
  169. package/src/mandate/index.ts +0 -4
  170. package/src/mandate/to-ir.ts +0 -71
  171. package/src/mandate/types.ts +0 -21
  172. package/src/seams/index.ts +0 -11
  173. package/src/seams/types.ts +0 -81
  174. package/src/synth/deny-cases.ts +0 -663
  175. package/src/synth/evaluate.ts +0 -613
  176. package/src/synth/harness.ts +0 -68
  177. package/src/synth/minimize.ts +0 -48
  178. package/src/synth/permit-context.ts +0 -136
  179. package/src/synth/predicate-literals.ts +0 -27
  180. package/src/synth/synthesize-from-mandate.ts +0 -82
  181. package/src/verify/envelope.ts +0 -28
  182. package/src/verify/index.ts +0 -5
  183. package/src/verify/simulate.ts +0 -311
  184. package/src/verify/verify.ts +0 -243
@@ -1,161 +1,28 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.OVERPERMISSIVE_DIMENSIONS = exports.ORIGINAL_DIMENSIONS = void 0;
3
+ exports.OVERPERMISSIVE_DIMENSIONS = void 0;
4
4
  exports.generateCases = generateCases;
5
5
  exports.cloneScVal = cloneScVal;
6
6
  const types_ts_1 = require("../types.js");
7
- const predicate_literals_ts_1 = require("./predicate-literals.js");
8
- const ORACLE_CASES = [
9
- ['oracle_stale', 'stale'],
10
- ['oracle_missing', 'missing'],
11
- ['oracle_deviation_exceeded', 'deviation'],
12
- ['oracle_paused', 'paused'],
13
- ];
14
- // Cross-layer reason contract: the TS evaluator's deny reason for each
15
- // oracle error category must match the Rust interpreter's `DenyReason` code
16
- // string (the same strings surface in the conformance fixture). The harness
17
- // asserts them via the per-case `expectedReason`; a future divergence
18
- // surfaces as a REASON_MISMATCH failure.
19
- const ORACLE_ERROR_REASON = {
20
- stale: 'ORACLE_STALE',
21
- missing: 'ORACLE_MISSING',
22
- deviation: 'ORACLE_DEVIATION_EXCEEDED',
23
- paused: 'ORACLE_PAUSED',
24
- };
25
7
  // Deterministic XLM/USDC adjacency fixture; the shared registry can replace this boundary later.
26
- const ADJACENT_ASSETS = [
27
- 'CAS3J7GYLGXMF6TDJ5WQ2PEN4GRVNXJUIQ2TZU3ZB3OQ2V4DRCWI7WPF',
28
- 'CCWCLTASNDT57N3BCHOSVB5QWMV5URK4BXLDDF6ZZQYMBQ4OKZA3ZB2N',
29
- ];
30
- // Property-harness mutation dimensions excluded from the synth pipeline's
31
- // self-verify call so existing fixtures still emit policies. The harness tests
32
- // them as FINDINGS against the already-emitted policy.
33
8
  const OVERPERMISSIVE_DIMENSIONS = ['argument_reorder'];
34
9
  exports.OVERPERMISSIVE_DIMENSIONS = OVERPERMISSIVE_DIMENSIONS;
35
- // The dimensions the synth pipeline uses for self-verify and minimise. No count
36
- // is written here on purpose: this comment said 15 while the array held 17, and
37
- // anyone checking the number should read the array rather than the prose.
38
- // Phase 1 grammar extension: `vec_append` and `map_field_flip` are listed below
39
- // alongside the existing dimensions so the per-element binds emitted for
40
- // Blend `submit` (call_arg_len + 3 call_arg_field per element) survive
41
- // minimise. Without these, no deny case exercises the new leaves and the
42
- // minimise pass drops them as "redundant" - reopening the element-append
43
- // hole they were added to close. Both dimensions are no-ops for any fixture
44
- // whose predicate has no `call_arg_len` / `call_arg_field` leaves (the
45
- // generator short-circuits on leaf-kind), so they cannot make production
46
- // synthesis refuse for fixtures that do not exercise the new grammar.
47
- const ORIGINAL_DIMENSIONS = [
48
- 'amount',
49
- 'asset',
50
- 'contract',
51
- 'function',
52
- 'timing',
53
- 'time_window',
54
- 'invocation_count',
55
- 'arg_amount_bound',
56
- 'arg_bound',
57
- 'scope_contract_fn_arg',
58
- 'oracle_stale',
59
- 'oracle_missing',
60
- 'oracle_deviation_exceeded',
61
- 'oracle_paused',
62
- 'soroswap_allowed_path',
63
- 'vec_append',
64
- 'map_field_flip',
65
- ];
66
- exports.ORIGINAL_DIMENSIONS = ORIGINAL_DIMENSIONS;
67
10
  /** Build deterministic model-evaluated alternatives without mutating the intended call.
68
11
  *
69
12
  * @param predicate - the synthesized predicate
70
- * @param permitCtx - EvalContext for the intended (permitted) call
71
- * @param dimensions - optional whitelist of dimension names to emit; when omitted
72
- * all known dimensions (including over-permissiveness mutations)
73
- * are emitted. The synth pipeline passes ORIGINAL_DIMENSIONS so
74
- * existing fixtures do not regress. */
13
+ * @param permitCtx - EvalContext for the intended (permitted) call
14
+ * @param dimensions - optional whitelist of dimension names to emit; when omitted
15
+ * all known dimensions (including over-permissiveness mutations)
16
+ * are emitted. */
75
17
  function generateCases(predicate, permitCtx, dimensions) {
76
18
  const facts = inspectPredicate(predicate);
77
19
  const denies = [];
78
- for (const comparison of facts.comparisons) {
79
- if (comparison.left.kind !== 'amount')
80
- continue;
81
- const mutated = mutateBigIntRecord(permitCtx, 'amountByToken', comparison.left.token, comparison);
82
- if (mutated)
83
- denies.push({ dimension: 'amount', ctx: mutated, expectedReason: 'AMOUNT_BOUND' });
84
- }
85
- const movedTokens = new Set();
86
- for (const comparison of facts.comparisons) {
87
- if (comparison.left.kind === 'amount' || comparison.left.kind === 'window_spent') {
88
- movedTokens.add(comparison.left.token);
89
- }
90
- }
91
- for (const token of movedTokens) {
92
- // The `asset` deny case mutates BOTH the amount record AND the contract
93
- // binding (token's value moved to an adjacent asset; the same swap applied
94
- // to any address-typed `call_arg` / `call_contract` literal). The actual
95
- // deny reason is therefore predicate-dependent - the order of evaluation
96
- // is CONTRACT_SCOPE (step 3) before AMOUNT_BOUND (step 6), so a policy
97
- // that has both bindings denies with CONTRACT_SCOPE, while a policy with
98
- // only an amount binding denies with AMOUNT_BOUND. The reason assertion
99
- // would be brittle here; the boolean decision is what we strictly need
100
- // to pin. Leave `expectedReason` unset for this dimension.
101
- denies.push({ dimension: 'asset', ctx: mutateAsset(predicate, permitCtx, token) });
102
- }
103
- const contractConstraints = [
104
- ...facts.comparisons.filter((node) => node.op === 'eq' &&
105
- node.left.kind === 'call_contract' &&
106
- node.right.kind === 'literal_address'),
107
- ...facts.memberships.filter((node) => node.needle.kind === 'call_contract'),
108
- ];
109
- for (const _constraint of contractConstraints) {
110
- const ctx = cloneContext(permitCtx);
111
- ctx.contract = distinctText(permitCtx.contract, 'contract');
112
- denies.push({ dimension: 'contract', ctx, expectedReason: 'CONTRACT_SCOPE' });
113
- }
114
- const functionConstraints = [
115
- ...facts.comparisons.filter((node) => node.op === 'eq' && node.left.kind === 'call_fn' && node.right.kind === 'literal_symbol'),
116
- ...facts.memberships.filter((node) => node.needle.kind === 'call_fn'),
117
- ];
118
- for (const _constraint of functionConstraints) {
119
- const ctx = cloneContext(permitCtx);
120
- ctx.fn = distinctText(permitCtx.fn, 'function');
121
- denies.push({ dimension: 'function', ctx, expectedReason: 'FN_MISMATCH' });
122
- }
123
- if (permitCtx.validUntilLedger !== undefined) {
124
- const ctx = cloneContext(permitCtx);
125
- ctx.atLedger = permitCtx.validUntilLedger + 1;
126
- denies.push({ dimension: 'timing', ctx, expectedReason: 'EXPIRED' });
127
- }
128
- for (const comparison of facts.comparisons) {
129
- if (comparison.left.kind !== 'window_spent')
130
- continue;
131
- const mutated = mutateBigIntRecord(permitCtx, 'windowSpentByToken', comparison.left.token, comparison, false);
132
- if (mutated)
133
- denies.push({ dimension: 'time_window', ctx: mutated, expectedReason: 'AMOUNT_BOUND' });
134
- }
135
- for (const comparison of facts.comparisons) {
136
- if (comparison.left.kind !== 'invocation_count_in_window')
137
- continue;
138
- if (comparison.right.kind !== 'literal_u32')
139
- continue;
140
- const ctx = cloneContext(permitCtx);
141
- ctx.invocationCountByWindow[comparison.left.windowSecs] = violatingNumber(comparison.op, comparison.right.value);
142
- denies.push({ dimension: 'invocation_count', ctx, expectedReason: 'FREQUENCY' });
143
- }
144
- // Ordered numeric bound on a call_arg (e.g. a SoroSwap input-amount cap
145
- // `call_arg[0] <= limit`). A violating deny case pushes the arg past the
146
- // bound so the leaf is exercised - and, critically, so `minimize` keeps it:
147
- // a conjunct that no deny case needs is pruned as redundant, which would
148
- // silently drop a caller-requested restriction.
149
- for (const comparison of facts.comparisons) {
150
- if (comparison.op === 'eq' || comparison.left.kind !== 'call_arg')
151
- continue;
152
- const violating = violatingArgScVal(comparison.op, comparison.right);
153
- if (!violating)
154
- continue;
155
- const ctx = cloneContext(permitCtx);
156
- ctx.args[comparison.left.index] = violating;
157
- denies.push({ dimension: 'arg_amount_bound', ctx, expectedReason: 'ARG_MISMATCH' });
158
- }
20
+ // arg_bound: set a constrained arg to an opaque ScVal.
21
+ // The reason depends on the predicate shape:
22
+ // - an `eq(call_arg[i], literal)` denies with ARG_MISMATCH
23
+ // - an `in(call_arg[i], [literals])` denies with NOT_IN_ALLOWLIST
24
+ // Both are correct in the TS evaluator; the reason is predicate-dependent
25
+ // so we cannot pin a single canonical reason here.
159
26
  const argumentConstraints = [];
160
27
  for (const comparison of facts.comparisons) {
161
28
  if (comparison.op === 'eq' &&
@@ -172,54 +39,10 @@ function generateCases(predicate, permitCtx, dimensions) {
172
39
  for (const constraint of argumentConstraints) {
173
40
  const ctx = cloneContext(permitCtx);
174
41
  ctx.args[constraint.index] = { type: 'other', value: 'deny-case-opaque-argument' };
175
- // The `arg_bound` case sets the arg to an opaque ScVal. The deny reason
176
- // depends on the predicate shape:
177
- // - an `eq(call_arg[i], literal)` denies with ARG_MISMATCH
178
- // - an `in(call_arg[i], [literals])` denies with NOT_IN_ALLOWLIST
179
- // (opaque needles fail-closed at the `in` membership gate, step 5)
180
- // Both are correct in the TS evaluator; the reason is predicate-dependent
181
- // so we cannot pin a single canonical reason here. The boolean decision
182
- // is what we strictly need to assert; the reason is recorded (not asserted)
183
- // when the harness runs this case.
184
42
  denies.push({ dimension: 'arg_bound', ctx });
185
43
  }
186
- const scopedArgumentIndices = new Set(argumentConstraints.map(({ index }) => index));
187
- for (const comparison of facts.comparisons) {
188
- if (comparison.op === 'eq' &&
189
- comparison.left.kind === 'call_arg' &&
190
- comparison.right.kind === 'literal_vec') {
191
- scopedArgumentIndices.add(comparison.left.index);
192
- }
193
- }
194
- if (contractConstraints.length > 0 &&
195
- functionConstraints.length > 0 &&
196
- scopedArgumentIndices.size > 0) {
197
- const ctx = cloneContext(permitCtx);
198
- ctx.contract = distinctText(permitCtx.contract, 'authorized-call-contract');
199
- ctx.fn = distinctText(permitCtx.fn, 'authorized-call-function');
200
- for (const index of scopedArgumentIndices) {
201
- ctx.args[index] = { type: 'other', value: 'deny-case-authorized-call-argument' };
202
- }
203
- // `scope_contract_fn_arg` changes contract, fn, AND args simultaneously.
204
- // The first failing child of the AND decides the reason, and the contract
205
- // check (step 3) does fire first in the evaluator. The reason is therefore
206
- // CONTRACT_SCOPE for the canonical case, but a predicate that lists the
207
- // call_fn leaf first (or that has a `call_arg_field` for that arg index)
208
- // can flip the order. Pin the assertion here only when no `in` /
209
- // call_arg_field binds the same arg index exist - the canonical blend
210
- // case does have those binds, so the assertion would fire there.
211
- denies.push({ dimension: 'scope_contract_fn_arg', ctx });
212
- }
213
- const oracleComparisons = facts.comparisons.filter((node) => node.left.kind === 'oracle_price');
214
- for (const [dimension, error] of ORACLE_CASES) {
215
- for (const comparison of oracleComparisons) {
216
- if (comparison.left.kind !== 'oracle_price')
217
- continue;
218
- const ctx = cloneContext(permitCtx);
219
- ctx.oraclePriceByAsset[comparison.left.asset] = { error };
220
- denies.push({ dimension, ctx, expectedReason: ORACLE_ERROR_REASON[error] });
221
- }
222
- }
44
+ // soroswap_allowed_path: replace a bound vec arg with a different vector.
45
+ // Still valid for eq comparisons on literal_vec leaves.
223
46
  for (const comparison of facts.comparisons) {
224
47
  if (comparison.op !== 'eq' ||
225
48
  comparison.left.kind !== 'call_arg' ||
@@ -230,7 +53,7 @@ function generateCases(predicate, permitCtx, dimensions) {
230
53
  ctx.args[comparison.left.index] = differentVector(ctx.args[comparison.left.index]);
231
54
  denies.push({ dimension: 'soroswap_allowed_path', ctx });
232
55
  }
233
- // --- argument_reorder: swap first two address args ---
56
+ // argument_reorder: swap first two address args.
234
57
  // Skipped when dimensions filter is active so the synth pipeline can emit a policy;
235
58
  // the over-permissiveness harness then tests this mutation as a FINDING.
236
59
  const constrainedArgIndices = new Set();
@@ -258,12 +81,10 @@ function generateCases(predicate, permitCtx, dimensions) {
258
81
  ctx.args[1] = a0;
259
82
  denies.push({ dimension: 'argument_reorder', ctx });
260
83
  }
261
- // --- map_field_flip: flip a bound map field to a different valid value of
262
- // the same type. Mirrors argument_reorder: OPT-IN only, never ORIGINAL.
84
+ // map_field_flip: flip a bound map field to a different valid value of
85
+ // the same type. OPT-IN only, never ORIGINAL.
263
86
  // Targets each `call_arg_field` leaf and produces a ctx whose vec element
264
- // has a different value of the recorded field (different request_type,
265
- // different amount, different address). The blend-submit case uses this
266
- // to detect a missing request_type pin. ---
87
+ // has a different value of the recorded field. ---
267
88
  if (!dimensions || dimensions.includes('map_field_flip')) {
268
89
  for (const comparison of facts.comparisons) {
269
90
  const sel = comparison.left;
@@ -292,9 +113,9 @@ function generateCases(predicate, permitCtx, dimensions) {
292
113
  denies.push({ dimension: 'map_field_flip', ctx });
293
114
  }
294
115
  }
295
- // --- vec_append: append a new element to a bound vec. Mirrors map_field_flip:
296
- // OPT-IN only, never ORIGINAL. Targets every `call_arg_len` leaf; without the
297
- // length pin a caller can append an extra element to defeat per-element binds. ---
116
+ // vec_append: append a new element to a bound vec. OPT-IN only, never ORIGINAL.
117
+ // Targets every `call_arg_len` leaf; without the length pin a caller can
118
+ // append an extra element to defeat per-element binds. ---
298
119
  if (!dimensions || dimensions.includes('vec_append')) {
299
120
  for (const comparison of facts.comparisons) {
300
121
  const sel = comparison.left;
@@ -311,7 +132,6 @@ function generateCases(predicate, permitCtx, dimensions) {
311
132
  denies.push({ dimension: 'vec_append', ctx });
312
133
  }
313
134
  }
314
- // Version mismatch, malformed predicates, master authorization, and nonce replay are install-time checks and are intentionally omitted from model-evaluated cases.
315
135
  return { permit: cloneContext(permitCtx), denies };
316
136
  }
317
137
  function inspectPredicate(predicate) {
@@ -322,162 +142,17 @@ function inspectPredicate(predicate) {
322
142
  function visit(node, facts) {
323
143
  switch (node.op) {
324
144
  case 'and':
325
- case 'or':
326
145
  for (const child of node.children)
327
146
  visit(child, facts);
328
147
  return;
329
- case 'not':
330
- visit(node.child, facts);
331
- return;
332
148
  case 'in':
333
149
  facts.memberships.push(node);
334
150
  return;
335
151
  case 'eq':
336
- case 'lt':
337
152
  case 'lte':
338
- case 'gt':
339
- case 'gte':
340
153
  facts.comparisons.push(node);
341
154
  }
342
155
  }
343
- function mutateBigIntRecord(permitCtx, recordKey, token, comparison, preferScaledAmount = true) {
344
- if (comparison.right.kind !== 'literal_i128')
345
- return null;
346
- try {
347
- const current = BigInt(permitCtx[recordKey][token] ?? '0');
348
- const bound = BigInt(comparison.right.value);
349
- const value = violatingBigInt(comparison.op, current, bound, preferScaledAmount);
350
- const ctx = cloneContext(permitCtx);
351
- ctx[recordKey][token] = value.toString();
352
- return ctx;
353
- }
354
- catch {
355
- return null;
356
- }
357
- }
358
- function violatingBigInt(op, current, bound, preferScaledAmount) {
359
- if (preferScaledAmount) {
360
- const scaledCandidates = [(current * 101n) / 100n, current * 10n];
361
- for (const candidate of scaledCandidates) {
362
- if (!bigIntComparison(op, candidate, bound))
363
- return candidate;
364
- }
365
- }
366
- return boundaryViolatingBigInt(op, bound);
367
- }
368
- /** The single value at the boundary that violates `<selector> op bound`:
369
- * bound+1 for `lte`/`eq`, bound-1 for `gte`, and bound itself for the strict
370
- * `lt`/`gt`. */
371
- function boundaryViolatingBigInt(op, bound) {
372
- switch (op) {
373
- case 'lt':
374
- case 'gt':
375
- return bound;
376
- case 'lte':
377
- case 'eq':
378
- return bound + 1n;
379
- case 'gte':
380
- return bound - 1n;
381
- }
382
- }
383
- function violatingNumber(op, bound) {
384
- switch (op) {
385
- case 'lt':
386
- case 'gt':
387
- return bound;
388
- case 'lte':
389
- case 'eq':
390
- return bound + 1;
391
- case 'gte':
392
- return bound - 1;
393
- }
394
- }
395
- /** Build an ScVal that VIOLATES an ordered numeric bound on a call_arg, given
396
- * the comparison op and its numeric-literal right-hand side. Returns null when
397
- * the literal is not an integer (the bound is not a numeric compare). The arg
398
- * is emitted as an i128 - the recorder's numeric args are read via BigInt, so
399
- * the wire type only needs to be a numeric ScVal to exercise the bound. */
400
- function violatingArgScVal(op, right) {
401
- const bound = (0, predicate_literals_ts_1.literalNumericBigInt)(right);
402
- if (bound === null)
403
- return null;
404
- return { type: 'i128', value: boundaryViolatingBigInt(op, bound).toString() };
405
- }
406
- function bigIntComparison(op, value, bound) {
407
- switch (op) {
408
- case 'eq':
409
- return value === bound;
410
- case 'lt':
411
- return value < bound;
412
- case 'lte':
413
- return value <= bound;
414
- case 'gt':
415
- return value > bound;
416
- case 'gte':
417
- return value >= bound;
418
- }
419
- }
420
- function mutateAsset(predicate, permitCtx, token) {
421
- const binding = findAddressBinding(predicate, token);
422
- const adjacent = adjacentAsset(token);
423
- const ctx = cloneContext(permitCtx);
424
- moveRecordEntry(ctx.amountByToken, token, adjacent);
425
- moveRecordEntry(ctx.windowSpentByToken, token, adjacent);
426
- if (binding.contract && ctx.contract === token)
427
- ctx.contract = adjacent;
428
- for (const index of binding.argumentIndices) {
429
- const value = ctx.args[index];
430
- if (value)
431
- ctx.args[index] = replaceAddress(value, token, adjacent);
432
- }
433
- return ctx;
434
- }
435
- function findAddressBinding(predicate, address) {
436
- const facts = inspectPredicate(predicate);
437
- let contract = false;
438
- const argumentIndices = new Set();
439
- for (const comparison of facts.comparisons) {
440
- if (comparison.op !== 'eq' || !leafContainsAddress(comparison.right, address))
441
- continue;
442
- if (comparison.left.kind === 'call_contract')
443
- contract = true;
444
- if (comparison.left.kind === 'call_arg')
445
- argumentIndices.add(comparison.left.index);
446
- }
447
- for (const membership of facts.memberships) {
448
- if (!membership.haystack.some((leaf) => leafContainsAddress(leaf, address)))
449
- continue;
450
- if (membership.needle.kind === 'call_contract')
451
- contract = true;
452
- if (membership.needle.kind === 'call_arg')
453
- argumentIndices.add(membership.needle.index);
454
- }
455
- return { contract, argumentIndices };
456
- }
457
- function leafContainsAddress(leaf, address) {
458
- if (leaf.kind === 'literal_address')
459
- return leaf.value === address;
460
- if (leaf.kind === 'literal_vec') {
461
- return leaf.elements.some((element) => leafContainsAddress(element, address));
462
- }
463
- return false;
464
- }
465
- function moveRecordEntry(record, from, to) {
466
- const value = record[from];
467
- if (value === undefined)
468
- return;
469
- delete record[from];
470
- record[to] = value;
471
- }
472
- function replaceAddress(value, from, to) {
473
- if (value.type === 'address') {
474
- return value.value === from ? { type: 'address', value: to } : { ...value };
475
- }
476
- if (value.type === 'vec') {
477
- return { type: 'vec', value: value.value.map((item) => replaceAddress(item, from, to)) };
478
- }
479
- return { ...value };
480
- }
481
156
  function differentVector(actual) {
482
157
  if (actual?.type !== 'vec')
483
158
  return { type: 'vec', value: [] };
@@ -494,40 +169,16 @@ function differentVector(actual) {
494
169
  value[0] = { type: 'other', value: 'deny-case-different-hop' };
495
170
  return { type: 'vec', value };
496
171
  }
497
- function adjacentAsset(asset) {
498
- return asset === ADJACENT_ASSETS[0] ? ADJACENT_ASSETS[1] : ADJACENT_ASSETS[0];
499
- }
500
- function distinctText(value, label) {
501
- return `${value}#${label}`;
502
- }
503
172
  function cloneContext(ctx) {
504
- const cloned = {
173
+ return {
505
174
  contract: ctx.contract,
506
175
  fn: ctx.fn,
507
176
  args: ctx.args.map(cloneScVal),
508
- atLedger: ctx.atLedger,
509
- nowSeconds: ctx.nowSeconds,
510
- amountByToken: { ...ctx.amountByToken },
511
- windowSpentByToken: { ...ctx.windowSpentByToken },
512
- invocationCountByWindow: { ...ctx.invocationCountByWindow },
513
- oraclePriceByAsset: Object.fromEntries(Object.entries(ctx.oraclePriceByAsset).map(([asset, entry]) => [
514
- asset,
515
- 'error' in entry ? { error: entry.error } : { ...entry },
516
- ])),
517
177
  };
518
- if (ctx.validUntilLedger !== undefined)
519
- cloned.validUntilLedger = ctx.validUntilLedger;
520
- if (ctx.signerWeights !== undefined)
521
- cloned.signerWeights = { ...ctx.signerWeights };
522
- return cloned;
523
178
  }
524
179
  /** Deep-copy an ScVal so a mutation cannot alias the recorded call.
525
180
  * Exported so the permit-context builder shares this one implementation. */
526
181
  function cloneScVal(value, depth = 0) {
527
- // Recursion is bounded by MAX_SCVAL_CLONE_DEPTH so a hand-crafted nested-vec
528
- // payload cannot RangeError the JS stack during deny-case mutation. Over-depth
529
- // throws a ToolError-shaped error that the `synthesizeFromRecording` envelope
530
- // (item 3) converts to a structured `{ok:false, error}`.
531
182
  if (value.type === 'vec') {
532
183
  if (depth >= types_ts_1.MAX_SCVAL_CLONE_DEPTH) {
533
184
  throw cloneDepthError(value);
@@ -546,9 +197,8 @@ function cloneDepthError(value) {
546
197
  }
547
198
  /** Build a value of the SAME ScVal type that differs from the recorded one,
548
199
  * used by the `map_field_flip` mutation to defeat a per-element pin without
549
- * changing the wire type (a type/arity change is rejected by host dispatch
550
- * before Policy::enforce, so it is not the predicate's job). Returns null
551
- * when the ScVal type has no obvious different value (e.g. opaque/other). */
200
+ * changing the wire type. Returns null when the ScVal type has no obvious
201
+ * different value (e.g. opaque/other). */
552
202
  function flipFieldValue(val) {
553
203
  switch (val.type) {
554
204
  case 'address': {
@@ -557,7 +207,6 @@ function flipFieldValue(val) {
557
207
  return { type: 'address', value: val.value === a ? b : a };
558
208
  }
559
209
  case 'i128':
560
- case 'u64':
561
210
  case 'u32':
562
211
  return { type: val.type, value: String(BigInt(val.value) + 1n) };
563
212
  case 'symbol':
@@ -6,27 +6,6 @@ export interface EvalContext {
6
6
  fn: string;
7
7
  /** Decoded `ScVal[]` of the top-level authorized call. */
8
8
  args: ScVal[];
9
- /** Ledger sequence at which the interpreter is invoked. */
10
- atLedger: number;
11
- /** Optional policy expiry; absent -> step 1 is skipped. */
12
- validUntilLedger?: number;
13
- /** Unix seconds; used by `now` / `valid_until` leaves. */
14
- nowSeconds: number;
15
- /** Per-token amount moved by the current call (i128 decimal string). */
16
- amountByToken: Record<string, string>;
17
- /** Per-token window-rolling spend prior to this call (i128 decimal string). */
18
- windowSpentByToken: Record<string, string>;
19
- /** Recorded invocation counts keyed by window seconds. */
20
- invocationCountByWindow: Record<number, number>;
21
- /** Optional signer-weight map for the threshold gate. Absent -> skip. */
22
- signerWeights?: Record<string, number>;
23
- /** Per-asset oracle snapshot. Missing keys default to ORACLE_STALE. */
24
- oraclePriceByAsset: Record<string, {
25
- price: string;
26
- timestampSeconds: number;
27
- } | {
28
- error: 'stale' | 'missing' | 'deviation' | 'paused' | 'decimals' | 'fingerprint';
29
- }>;
30
9
  }
31
10
  export type EvalResult = {
32
11
  permit: true;