@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
@@ -3,7 +3,7 @@
3
3
  //
4
4
  // Pure function. Maps a `PredicateNode` AST to the canonical ScVal wire format:
5
5
  // - every node is a `ScVal::Vec` whose head element is the tag `ScVal::Symbol`
6
- // - children of `and` / `or` are sorted ascending by their canonical XDR bytes
6
+ // - children of `and` are sorted ascending by their canonical XDR bytes
7
7
  // - `in` haystacks are ALWAYS sorted by canonical XDR bytes (pure set
8
8
  // membership); an EXACT ordered sequence is expressed as
9
9
  // `eq(selector, literal_vec)` where the `literal_vec` element order is
@@ -18,12 +18,6 @@
18
18
  // Caps from `PREDICATE_CAPS` are enforced BEFORE returning; a cap breach throws
19
19
  // a `ToolError` with the matching error code and `severity: 'error'`.
20
20
  //
21
- // One gap remains, deliberately: the `amount` / `window_spent` leaf branches
22
- // below are dead ABI - the contract's grammar no longer has those selector
23
- // symbols, so a predicate carrying one is MALFORMED at decode. The interpreter
24
- // adapter reports them as uncovered before lowering, so the product path
25
- // cannot emit one; removing the branches (and the leaf kinds) is a separate
26
- // change.
27
21
  Object.defineProperty(exports, "__esModule", { value: true });
28
22
  exports.encodePredicate = encodePredicate;
29
23
  const node_crypto_1 = require("node:crypto");
@@ -48,27 +42,22 @@ function encodePredicate(node) {
48
42
  throw capError('IN_OPERAND_LIMIT', `\`in\` operand count ${c.count} exceeds MAX_IN_OPERAND_COUNT ${types_ts_1.PREDICATE_CAPS.MAX_IN_OPERAND_COUNT}`);
49
43
  }
50
44
  }
51
- // Two-round confirmation costs 2 oracle reads per UNIQUE asset referenced.
52
- const oracleReads = stats.oracleAssets.size * 2;
53
- if (oracleReads > types_ts_1.PREDICATE_CAPS.MAX_ORACLE_READS) {
54
- throw capError('PREDICATE_ORACLE_OVER_LIMIT', `oracle reads ${oracleReads} exceed MAX_ORACLE_READS ${types_ts_1.PREDICATE_CAPS.MAX_ORACLE_READS}`);
45
+ // Minimum constraint. A predicate with no selector leaf compares literals
46
+ // to literals, so it is trivially true or false and would permit everything
47
+ // or nothing forever. The contract refuses it at install with
48
+ // SELECTOR_LEAF_REQUIRED (216); refuse it here too so the failure is caught
49
+ // before a transaction is built.
50
+ if (stats.selectorLeaves === 0) {
51
+ throw capError('MALFORMED_PREDICATE', 'predicate constrains nothing: every comparison has literals on both sides. The contract refuses it at install (SELECTOR_LEAF_REQUIRED). Pin the call itself - contract, method or argument.');
55
52
  }
56
- if (stats.oracleAssets.size > 0 && stats.nonOracleSelectorLeaves === 0) {
57
- throw capError('MALFORMED_PREDICATE', 'predicate constrains nothing but an oracle price: the contract refuses it at install (dsl.rs MissingNonOracleEnvelope). Pin the call itself (contract / method / argument) alongside the price bound.');
58
- }
59
- // --- pass 1.5: leaf-value validation (Rust `validate_scaled_ratios` + the
60
- // broader cap-set gate the contract enforces at install). Defense in depth:
61
- // the TS self-verify pipeline should reject the same shapes Rust install
62
- // refuses, so a hand-crafted predicate that simulate/verify green-lights
63
- // cannot later be refused at the on-chain install step. The checks:
64
- // - u32 fields in range (call_arg index, literal_u32 value, oracle
65
- // threshold decimals, etc.) - the contract decodes as u32
66
- // - i128 positivity where required (literal_i128 for amount/window
67
- // caps; `den`/`num` for scaled ratios) - a negative cap would
68
- // permit everything
69
- // - hex even-length (literal_bytes) - `Buffer.from(v, 'hex')` silently
70
- // drops non-hex chars, so 'zz' becomes empty bytes
71
- // - scaled-ratio num>0 && den>0 - mirrors dsl.rs:661-704
53
+ // --- pass 1.5: leaf-value validation against the cap-set gate the contract
54
+ // enforces at install. Defense in depth: the TS self-verify pipeline should
55
+ // reject the same shapes Rust install refuses, so a hand-crafted predicate
56
+ // that simulate/verify green-lights cannot later be refused at the on-chain
57
+ // install step. The checks are the u32 ranges on the argument selectors
58
+ // (`call_arg.index`, `call_arg_len.index`, `call_arg_field.index` and
59
+ // `.element`) - the contract decodes each as u32, so an out-of-range value
60
+ // would fail there instead.
72
61
  // Throws `MALFORMED_PREDICATE` so the error stays a ToolError shape.
73
62
  validateLeafValues(node);
74
63
  // --- pass 2: build + canonicalise the ScVal ---
@@ -83,48 +72,35 @@ function encodePredicate(node) {
83
72
  }
84
73
  function computeStats(node) {
85
74
  const inCounts = [];
86
- const oracleAssets = new Set();
87
- const counters = { nonOracleSelectorLeaves: 0 };
88
- const { depth, leaves } = walk(node, inCounts, oracleAssets, counters, false);
75
+ const counters = { selectorLeaves: 0 };
76
+ const { depth, leaves } = walk(node, inCounts, counters);
89
77
  return {
90
78
  depth,
91
79
  leaves,
92
80
  inCounts,
93
- oracleAssets,
94
- nonOracleSelectorLeaves: counters.nonOracleSelectorLeaves,
81
+ selectorLeaves: counters.selectorLeaves,
95
82
  };
96
83
  }
97
- function walk(node, inCounts, oracleAssets, counters,
98
- /** True once the walk is under a `not` or an `or`, where an oracle read
99
- * becomes a condition the caller can satisfy by taking the other branch. */
100
- negatedOrDisjunctive) {
84
+ function walk(node, inCounts, counters) {
101
85
  switch (node.op) {
102
- case 'and':
103
- case 'or': {
86
+ case 'and': {
104
87
  if (node.children.length === 0) {
105
88
  throw capError('MALFORMED_PREDICATE', `\`${node.op}\` with no children: the contract refuses it at decode (MALFORMED_PREDICATE), so it can never be installed`);
106
89
  }
107
90
  let maxChildDepth = 0;
108
91
  let totalLeaves = 0;
109
92
  for (const c of node.children) {
110
- const child = walk(c, inCounts, oracleAssets, counters, negatedOrDisjunctive || node.op === 'or');
93
+ const child = walk(c, inCounts, counters);
111
94
  if (child.depth > maxChildDepth)
112
95
  maxChildDepth = child.depth;
113
96
  totalLeaves += child.leaves;
114
97
  }
115
98
  return { depth: maxChildDepth + 1, leaves: totalLeaves };
116
99
  }
117
- case 'not': {
118
- const child = walk(node.child, inCounts, oracleAssets, counters, true);
119
- return { depth: child.depth + 1, leaves: child.leaves };
120
- }
121
100
  case 'eq':
122
- case 'lt':
123
- case 'lte':
124
- case 'gt':
125
- case 'gte': {
126
- collectOracle(node.left, oracleAssets, counters, negatedOrDisjunctive);
127
- collectOracle(node.right, oracleAssets, counters, negatedOrDisjunctive);
101
+ case 'lte': {
102
+ collectSelector(node.left, counters);
103
+ collectSelector(node.right, counters);
128
104
  return { depth: 1, leaves: leafCount(node.left) + leafCount(node.right) };
129
105
  }
130
106
  case 'in': {
@@ -132,10 +108,10 @@ negatedOrDisjunctive) {
132
108
  throw capError('MALFORMED_PREDICATE', '`in` with an empty haystack: the contract refuses it at decode (MALFORMED_PREDICATE), so it can never be installed');
133
109
  }
134
110
  inCounts.push({ count: node.haystack.length });
135
- collectOracle(node.needle, oracleAssets, counters, negatedOrDisjunctive);
111
+ collectSelector(node.needle, counters);
136
112
  let haystackLeaves = 0;
137
113
  for (const h of node.haystack) {
138
- collectOracle(h, oracleAssets, counters, negatedOrDisjunctive);
114
+ collectSelector(h, counters);
139
115
  haystackLeaves += leafCount(h);
140
116
  }
141
117
  return {
@@ -157,45 +133,27 @@ function leafCount(leaf) {
157
133
  }
158
134
  return 1;
159
135
  }
160
- function collectOracle(leaf, oracleAssets, counters, negatedOrDisjunctive) {
161
- // literal_vec is the only nested leaf; recurse so an oracle_price buried in a
162
- // vector literal (which the lowering would forbid, but the cap-walker must
163
- // still see) is counted toward the oracle-read budget.
164
- if (leaf.kind === 'oracle_price') {
165
- if (negatedOrDisjunctive) {
166
- throw capError('ORACLE_LEAF_INVALID_POSITION', `oracle_price(${leaf.asset}) sits under a \`not\`/\`or\`: the contract refuses that position at install (dsl.rs validate_oracle_placement). An oracle bound must be a conjunct the call has to satisfy.`);
167
- }
168
- oracleAssets.add(leaf.asset);
169
- }
170
- else if (leaf.kind === 'literal_vec') {
136
+ function collectSelector(leaf, counters) {
137
+ // literal_vec is the only nested leaf; recurse so a selector buried in a
138
+ // vector literal is still counted. Mirrors `dsl::has_selector_leaf`.
139
+ if (leaf.kind === 'literal_vec') {
171
140
  for (const el of leaf.elements)
172
- collectOracle(el, oracleAssets, counters, negatedOrDisjunctive);
141
+ collectSelector(el, counters);
173
142
  }
174
- else if (!leaf.kind.startsWith('literal_') && leaf.kind !== 'oracle_threshold') {
175
- // A threshold is the oracle compare's operand, not an independent
176
- // constraint. Counting it would let an oracle-only predicate satisfy the
177
- // non-oracle envelope with its own bound (mirrors collect_oracle_leaf in
178
- // dsl.rs, where it sits with the literals).
179
- counters.nonOracleSelectorLeaves += 1;
143
+ else if (!leaf.kind.startsWith('literal_')) {
144
+ counters.selectorLeaves += 1;
180
145
  }
181
146
  }
182
147
  function encodeNode(node) {
183
148
  switch (node.op) {
184
- case 'and':
185
- case 'or': {
149
+ case 'and': {
186
150
  const encoded = node.children.map(encodeNode);
187
151
  // sort children by their canonical XDR bytes ascending.
188
152
  const sorted = sortByCanonicalBytes(encoded);
189
153
  return stellar_sdk_1.xdr.ScVal.scvVec([symbol(node.op), stellar_sdk_1.xdr.ScVal.scvVec(sorted)]);
190
154
  }
191
- case 'not': {
192
- return stellar_sdk_1.xdr.ScVal.scvVec([symbol('not'), encodeNode(node.child)]);
193
- }
194
155
  case 'eq':
195
- case 'lt':
196
- case 'lte':
197
- case 'gt':
198
- case 'gte': {
156
+ case 'lte': {
199
157
  return stellar_sdk_1.xdr.ScVal.scvVec([symbol(node.op), encodeLeaf(node.left), encodeLeaf(node.right)]);
200
158
  }
201
159
  case 'in': {
@@ -226,47 +184,6 @@ function encodeLeaf(leaf) {
226
184
  stellar_sdk_1.xdr.ScVal.scvU32(leaf.element),
227
185
  stellar_sdk_1.xdr.ScVal.scvSymbol(leaf.field),
228
186
  ]);
229
- case 'call_arg_scaled':
230
- // Wire shape `vec[symbol, u32, i128, i128]` mirrors the Rust decoder's
231
- // expectation in dsl.rs SEL_CALL_ARG_SCALED. `scvI128FromDecimal` is
232
- // the same range-checked helper used by `literal_i128`; an out-of-range
233
- // num/den is refused at encode time (rather than at install on chain).
234
- return stellar_sdk_1.xdr.ScVal.scvVec([
235
- symbol('call_arg_scaled'),
236
- stellar_sdk_1.xdr.ScVal.scvU32(leaf.index),
237
- scvI128FromDecimal(leaf.num),
238
- scvI128FromDecimal(leaf.den),
239
- ]);
240
- case 'amount':
241
- return stellar_sdk_1.xdr.ScVal.scvVec([symbol('amount'), scvAddressFromStrkey(leaf.token)]);
242
- case 'window_spent':
243
- return stellar_sdk_1.xdr.ScVal.scvVec([
244
- symbol('window_spent'),
245
- scvAddressFromStrkey(leaf.token),
246
- scvU64FromValue(leaf.windowSeconds),
247
- ]);
248
- case 'now':
249
- return stellar_sdk_1.xdr.ScVal.scvVec([symbol('now')]);
250
- case 'valid_until':
251
- // The interpreter refuses this leaf at install: it never sources
252
- // valid_until_ledger, so a policy built on it would deny forever.
253
- // Expiry belongs to the smart account, through the context rule's
254
- // validUntilLedger. Refuse at synthesis so the author finds out here
255
- // rather than from an install that always fails.
256
- throw new Error('valid_until is not a usable predicate leaf: the interpreter never sources it and ' +
257
- 'refuses it at install. Put expiry on the context rule (validUntilLedger) instead.');
258
- case 'invocation_count_in_window':
259
- return stellar_sdk_1.xdr.ScVal.scvVec([symbol('invocation_count'), scvU64FromValue(leaf.windowSecs)]);
260
- case 'oracle_price':
261
- return stellar_sdk_1.xdr.ScVal.scvVec([symbol('oracle_price'), scvAddressFromStrkey(leaf.asset)]);
262
- case 'oracle_threshold':
263
- // Arity 3, matching SEL_ORACLE_THRESHOLD in dsl.rs. The declared basis
264
- // travels with the value so the contract never has to assume one.
265
- return stellar_sdk_1.xdr.ScVal.scvVec([
266
- symbol('oracle_threshold'),
267
- scvI128FromDecimal(leaf.value),
268
- stellar_sdk_1.xdr.ScVal.scvU32(leaf.decimals),
269
- ]);
270
187
  case 'literal_address':
271
188
  return scvAddressFromStrkey(leaf.value);
272
189
  case 'literal_i128':
@@ -275,10 +192,6 @@ function encodeLeaf(leaf) {
275
192
  return stellar_sdk_1.xdr.ScVal.scvSymbol(leaf.value);
276
193
  case 'literal_u32':
277
194
  return stellar_sdk_1.xdr.ScVal.scvU32(leaf.value);
278
- case 'literal_u64':
279
- return scvU64FromValue(leaf.value);
280
- case 'literal_bytes':
281
- return stellar_sdk_1.xdr.ScVal.scvBytes(Buffer.from(leaf.value, 'hex'));
282
195
  case 'literal_vec':
283
196
  // Bare ScVal::Vec of element encodings - order is preserved verbatim
284
197
  // because the order IS the semantic (exact ordered sequence equality).
@@ -296,11 +209,6 @@ function sortByCanonicalBytes(values) {
296
209
  function scvAddressFromStrkey(strkey) {
297
210
  return stellar_sdk_1.xdr.ScVal.scvAddress(stellar_sdk_1.Address.fromString(strkey).toScAddress());
298
211
  }
299
- function scvU64FromValue(value) {
300
- // The Uint64 (UnsignedHyper) constructor accepts string | bigint | number;
301
- // string is safest for values > 2^53.
302
- return stellar_sdk_1.xdr.ScVal.scvU64(new stellar_sdk_1.xdr.Uint64(String(value)));
303
- }
304
212
  /** Build `ScVal::I128(Int128Parts{hi, lo})` from a signed decimal string.
305
213
  * `Int128Parts` encodes the value as `(hi << 64) + lo` with `hi` a SIGNED
306
214
  * 64-bit int and `lo` an UNSIGNED 64-bit int (this is NOT signed-magnitude).
@@ -316,9 +224,8 @@ function scvI128FromDecimal(decimal) {
316
224
  if (hi < INT64_MIN || hi > INT64_MAX) {
317
225
  throw capError('MALFORMED_PREDICATE', `literal_i128 value ${decimal} is outside the Int128 range`);
318
226
  }
319
- if (lo < 0n || lo > UINT64_MAX) {
320
- throw capError('MALFORMED_PREDICATE', `literal_i128 value ${decimal} has an invalid Int128Parts.lo`);
321
- }
227
+ // `lo` needs no range check: masking with UINT64_MAX puts it in
228
+ // [0, 2^64-1] for every v, negative included.
322
229
  return stellar_sdk_1.xdr.ScVal.scvI128(new stellar_sdk_1.xdr.Int128Parts({
323
230
  hi: new stellar_sdk_1.xdr.Int64(hi),
324
231
  lo: new stellar_sdk_1.xdr.Uint64(lo),
@@ -334,19 +241,13 @@ function capError(code, message) {
334
241
  // u32 boundary - the same constant the contract decodes with. A value above
335
242
  // this either overflows during encode or is refused at install.
336
243
  const U32_MAX = 4294967295;
337
- // The maximum decimal basis an oracle threshold can declare (mirrors
338
- // `MAX_ORACLE_THRESHOLD_DECIMALS` in dsl.rs). A value above this is refused
339
- // at install with `ORACLE_PARAMS_OUT_OF_RANGE`.
340
- const MAX_ORACLE_THRESHOLD_DECIMALS = 18;
341
- /** Walk a `PredicateNode` and fail-closed on any leaf whose cap-set value the
342
- * contract would refuse at install. Mirrors `validate_scaled_ratios` in
343
- * dsl.rs:661-704 plus the broader cap-set gate (`literal_u32`, `literal_i128`
344
- * positivity, `literal_bytes` hex even-length, `oracle_threshold` decimals
345
- * range, `call_arg_scaled` positive-ratio). Defense in depth so the TS
346
- * self-verify pipeline rejects the same shapes Rust install already
347
- * refuses - a hand-crafted predicate that simulate/verify green-lights must
348
- * NOT be installable. Throws `MALFORMED_PREDICATE` so the envelope shapes
349
- * it into a ToolError. */
244
+ /** Walk a `PredicateNode` and fail-closed on any leaf whose value the contract
245
+ * would refuse at install: the argument selectors carry u32 indices, so a
246
+ * value outside that range is refused here rather than on chain. Defense in
247
+ * depth so the TS self-verify pipeline rejects the same shapes Rust install
248
+ * already refuses - a hand-crafted predicate that simulate/verify
249
+ * green-lights must NOT be installable. Throws `MALFORMED_PREDICATE` so the
250
+ * envelope shapes it into a ToolError. */
350
251
  function validateLeafValues(node) {
351
252
  function walkLeaf(leaf, path) {
352
253
  switch (leaf.kind) {
@@ -368,107 +269,22 @@ function validateLeafValues(node) {
368
269
  throw malformed(`call_arg_field.element out of u32 range at ${path}`);
369
270
  }
370
271
  return;
371
- case 'call_arg_scaled': {
372
- if (!Number.isInteger(leaf.index) || leaf.index < 0 || leaf.index > U32_MAX) {
373
- throw malformed(`call_arg_scaled.index out of u32 range at ${path}`);
374
- }
375
- // num / den are i128 on chain, decimal strings on the wire. The
376
- // contract refuses `den == 0` and `num <= 0` / `den <= 0` at install
377
- // (dsl.rs:664-672); mirror that here so a future regression in
378
- // `validate_scaled_ratios` cannot let a divide-by-zero policy reach
379
- // the wire. BigInt throws on non-numeric strings -> malformed.
380
- let num;
381
- let den;
382
- try {
383
- num = BigInt(leaf.num);
384
- den = BigInt(leaf.den);
385
- }
386
- catch {
387
- throw malformed(`call_arg_scaled.num/den not a decimal integer at ${path}`);
388
- }
389
- if (num <= 0n)
390
- throw malformed(`call_arg_scaled.num must be > 0 at ${path}`);
391
- if (den <= 0n)
392
- throw malformed(`call_arg_scaled.den must be > 0 at ${path}`);
393
- return;
394
- }
395
- case 'literal_u32':
396
- if (!Number.isInteger(leaf.value) || leaf.value < 0 || leaf.value > U32_MAX) {
397
- throw malformed(`literal_u32.value out of u32 range at ${path}`);
398
- }
399
- return;
400
- case 'literal_i128':
401
- // literal_i128 is signed; the contract allows negatives (i128
402
- // arithmetic), but caps on a positive quantity (amount / window
403
- // bound) should never be negative - a negative cap is silently
404
- // satisfied by every non-negative amount. The contract gate is
405
- // already on the leaf's ROLE (amount vs equality) not the value;
406
- // here we mirror the value-only invariant the cap-set gate enforces
407
- // by refusing the syntactic shape that would clearly be a bug
408
- // (literal_i128 as a CAP with a leading `-` on a non-equality).
409
- // We do not gate equality i128 - `literal_i128` as an address-by-
410
- // equality is fine (it is just a constant).
411
- // The value itself is always accepted; the structural check
412
- // (non-negative for an amount / window bound) is left to the
413
- // caller-built predicate, not the encoder.
414
- return;
415
- case 'literal_bytes':
416
- // Hex even-length: a non-hex char silently drops, and an odd
417
- // length yields a half-byte Buffer. The contract decodes with a
418
- // strict hex parser and refuses anything that is not even-length
419
- // hex; mirror that here.
420
- if (!/^[0-9a-fA-F]*$/.test(leaf.value) || leaf.value.length % 2 !== 0) {
421
- throw malformed(`literal_bytes.value must be even-length hex at ${path}`);
422
- }
423
- return;
424
- case 'oracle_threshold':
425
- if (!Number.isInteger(leaf.decimals) ||
426
- leaf.decimals < 0 ||
427
- leaf.decimals > MAX_ORACLE_THRESHOLD_DECIMALS) {
428
- throw malformed(`oracle_threshold.decimals out of range (0..${MAX_ORACLE_THRESHOLD_DECIMALS}) at ${path}`);
429
- }
430
- return;
431
- case 'literal_vec':
432
- leaf.elements.forEach((e, i) => {
433
- walkLeaf(e, `${path}.elements[${i}]`);
434
- });
435
- return;
436
- // Selector and other leaves carry no cap-set values; the call_arg
437
- // branches above cover indices, the literal branches cover typed
438
- // constants. amount / window_spent / invocation_count / now /
439
- // valid_until / call_contract / call_fn / literal_address /
440
- // literal_symbol / literal_u64 / oracle_price are all value-free
441
- // at this gate.
442
- case 'amount':
443
- case 'window_spent':
444
- case 'invocation_count_in_window':
445
- case 'now':
446
- case 'valid_until':
447
272
  case 'call_contract':
448
273
  case 'call_fn':
449
274
  case 'literal_address':
450
275
  case 'literal_symbol':
451
- case 'literal_u64':
452
- case 'oracle_price':
453
276
  return;
454
277
  }
455
278
  }
456
279
  function walkNode(n, path) {
457
280
  switch (n.op) {
458
281
  case 'and':
459
- case 'or':
460
282
  n.children.forEach((c, i) => {
461
283
  walkNode(c, `${path}.children[${i}]`);
462
284
  });
463
285
  return;
464
- case 'not':
465
- walkNode(n.child, `${path}.child`);
466
- return;
467
286
  case 'eq':
468
- case 'lt':
469
287
  case 'lte':
470
- case 'gt':
471
- case 'gte':
472
288
  walkLeaf(n.left, `${path}.left`);
473
289
  walkLeaf(n.right, `${path}.right`);
474
290
  return;
@@ -20,15 +20,9 @@ function jsonToAst(value) {
20
20
  const v = value;
21
21
  switch (v.op) {
22
22
  case 'and':
23
- case 'or':
24
- return { op: v.op, children: arrayOf(v.children, jsonToAst) };
25
- case 'not':
26
- return { op: 'not', child: jsonToAst(v.child) };
23
+ return { op: 'and', children: arrayOf(v.children, jsonToAst) };
27
24
  case 'eq':
28
- case 'lt':
29
25
  case 'lte':
30
- case 'gt':
31
- case 'gte':
32
26
  return { op: v.op, left: jsonToLeaf(v.left), right: jsonToLeaf(v.right) };
33
27
  case 'in':
34
28
  return { op: 'in', needle: jsonToLeaf(v.needle), haystack: arrayOf(v.haystack, jsonToLeaf) };
@@ -48,42 +42,11 @@ function jsonToLeaf(value) {
48
42
  switch (v.kind) {
49
43
  case 'call_contract':
50
44
  case 'call_fn':
51
- case 'now':
52
- case 'valid_until':
53
45
  return { kind: v.kind };
54
46
  case 'call_arg':
55
47
  return { kind: 'call_arg', index: numberField(v, 'index') };
56
48
  case 'call_arg_len':
57
49
  return { kind: 'call_arg_len', index: numberField(v, 'index') };
58
- // num/den are strings, not numbers: they are i128 on chain, and a JSON
59
- // number would silently lose precision past 2^53 before the encoder
60
- // ever saw the value.
61
- case 'call_arg_scaled':
62
- return {
63
- kind: 'call_arg_scaled',
64
- index: numberField(v, 'index'),
65
- num: stringField(v, 'num'),
66
- den: stringField(v, 'den'),
67
- };
68
- case 'call_arg_field':
69
- return {
70
- kind: 'call_arg_field',
71
- index: numberField(v, 'index'),
72
- element: numberField(v, 'element'),
73
- field: stringField(v, 'field'),
74
- };
75
- case 'amount':
76
- return { kind: 'amount', token: stringField(v, 'token') };
77
- case 'window_spent':
78
- return {
79
- kind: 'window_spent',
80
- token: stringField(v, 'token'),
81
- windowSeconds: numberField(v, 'windowSeconds'),
82
- };
83
- case 'invocation_count_in_window':
84
- return { kind: 'invocation_count_in_window', windowSecs: numberField(v, 'windowSecs') };
85
- case 'oracle_price':
86
- return { kind: 'oracle_price', asset: stringField(v, 'asset') };
87
50
  case 'literal_address':
88
51
  return { kind: 'literal_address', value: stringField(v, 'value') };
89
52
  case 'literal_i128':
@@ -92,10 +55,6 @@ function jsonToLeaf(value) {
92
55
  return { kind: 'literal_symbol', value: stringField(v, 'value') };
93
56
  case 'literal_u32':
94
57
  return { kind: 'literal_u32', value: numberField(v, 'value') };
95
- case 'literal_u64':
96
- return { kind: 'literal_u64', value: stringField(v, 'value') };
97
- case 'literal_bytes':
98
- return { kind: 'literal_bytes', value: stringField(v, 'value') };
99
58
  case 'literal_vec':
100
59
  return { kind: 'literal_vec', elements: arrayOf(v.elements, jsonToLeaf) };
101
60
  default:
@@ -340,14 +340,9 @@ function recordInvocation(contract, fn, args, network, knownSet, knownContractSe
340
340
  // parseConfidence denominator isn't inflated by repeat calls), but each
341
341
  // unrecognised invocation is recorded separately so the numerator
342
342
  // reflects every fail-closed hit.
343
- if (knownSet.has(contract)) {
344
- if (!knownContractSet.has(contract)) {
345
- knownContractSet.add(contract);
346
- knownOut.push(contract);
347
- }
348
- return;
349
- }
350
- if ((0, identify_ts_1.identifyProtocol)(contract, fn, args, network ?? undefined)) {
343
+ // `||` short-circuits, so `identifyProtocol` is still only consulted when the
344
+ // contract is not already in `knownSet` - the same call order as before.
345
+ if (knownSet.has(contract) || (0, identify_ts_1.identifyProtocol)(contract, fn, args, network ?? undefined)) {
351
346
  if (!knownContractSet.has(contract)) {
352
347
  knownContractSet.add(contract);
353
348
  knownOut.push(contract);
@@ -1,5 +1,4 @@
1
- import type { ContextRuleDraft, PolicyRef, PredicateNode } from '../types.ts';
2
- import type { SimulationResult } from '../verify/envelope.ts';
1
+ import type { ContextRuleDraft, PredicateNode } from '../types.ts';
3
2
  export interface ReviewCardSummary {
4
3
  ruleName: string;
5
4
  plainEnglish: string;
@@ -17,7 +16,9 @@ export interface ReviewCardSummary {
17
16
  }
18
17
  /** Build a deterministic review-card summary from a policy + context rule +
19
18
  * simulation result. Pure: same inputs -> byte-identical output. */
20
- export declare function buildReviewCardSummary(predicate: PredicateNode | null, policyRefs: PolicyRef[], contextRule: ContextRuleDraft, simulation: SimulationResult): ReviewCardSummary;
19
+ export declare function buildReviewCardSummary(predicate: PredicateNode | null, contextRule: ContextRuleDraft,
20
+ /** Evaluator identity folded into the content hash. */
21
+ backend: 'interpreter-v1' | 'ts-model'): ReviewCardSummary;
21
22
  /** Walk every comparison / membership node of the predicate and invoke
22
23
  * `visit` on each. The walk is depth-first, left-to-right, so the
23
24
  * constraint list is stable across runs. Pure boolean nodes contribute no