@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
@@ -6,38 +6,26 @@
6
6
  // summary being REPRODUCIBLE + TESTABLE + NON-HALLUCINABLE, so this module is
7
7
  // pure: same inputs -> byte-identical output, no clock, no randomness, no I/O.
8
8
  //
9
- // The builder walks two inputs and emits ONE constraint string per leaf or
10
- // primitive, in a fixed deterministic order:
11
- //
12
- // 1. The OZ built-in `PolicyRef`s. Each `spending_limit` primitive becomes a
13
- // `spending_limit(token, limitAmount, windowSecs)` line; other OZ
14
- // primitives (threshold) are skipped - the review card does not quote
15
- // them (they are signer-config concerns, not transactional bounds).
16
- //
17
- // 2. The interpreter `PredicateNode`. One string per constraint leaf,
18
- // rendered by enclosing-comparison kind. Templates (Task 7b):
19
- // - invocation_count_in_window < N -> At most N calls per <window> seconds
20
- // - call_arg[i] in [list] -> Recipient/arg must be one of [list]
21
- // - eq(call_arg[i], literal_vec[...]) -> Path must be exactly [list]
22
- // - oracle_price(asset) OP price -> Only when oracle_price(asset) OP price
23
- // - call_fn == x -> Function must be x
24
- // - call_contract == c -> Contract must be c
25
- // - amount <= v -> Amount <= v
9
+ // The builder walks the interpreter `PredicateNode` and emits ONE constraint
10
+ // string per leaf, in a fixed deterministic order. Templates (Task 7b):
11
+ // - call_arg[i] in [list] -> Recipient/arg must be one of [list]
12
+ // - eq(call_arg[i], literal_vec[...]) -> Path must be exactly [list]
13
+ // - call_fn == x -> Function must be x
14
+ // - call_contract == c -> Contract must be c
15
+ // - amount <= v -> Amount <= v
26
16
  //
27
17
  // The content hash is a stable sha256 hex of a canonical JSON of
28
18
  // { ruleName, plainEnglish, constraints, expiry, backend } - identical
29
19
  // inputs (incl. the context-rule expiry and simulation backend) -> identical
30
20
  // hash. There is no clock; the hash never includes a timestamp.
31
21
  import { createHash } from 'node:crypto';
22
+ import { comparisonOpText, renderHaystackElement, renderVecElement } from "./render-leaf.js";
32
23
  /** Build a deterministic review-card summary from a policy + context rule +
33
24
  * simulation result. Pure: same inputs -> byte-identical output. */
34
- export function buildReviewCardSummary(predicate, policyRefs, contextRule, simulation) {
25
+ export function buildReviewCardSummary(predicate, contextRule,
26
+ /** Evaluator identity folded into the content hash. */
27
+ backend) {
35
28
  const constraints = [];
36
- for (const ref of policyRefs) {
37
- const line = renderOzPrimitive(ref);
38
- if (line !== null)
39
- constraints.push(line);
40
- }
41
29
  if (predicate !== null) {
42
30
  walkPredicate(predicate, (node) => {
43
31
  const line = renderConstraint(node);
@@ -55,7 +43,6 @@ export function buildReviewCardSummary(predicate, policyRefs, contextRule, simul
55
43
  const signerNote = contextRule.signers.length >= 2
56
44
  ? 'any ONE signer may authorise a permitted op under this rule (OZ any-of-N semantic)'
57
45
  : null;
58
- const backend = simulation.backend;
59
46
  const contentHash = computeContentHash({
60
47
  ruleName,
61
48
  plainEnglish,
@@ -66,25 +53,6 @@ export function buildReviewCardSummary(predicate, policyRefs, contextRule, simul
66
53
  });
67
54
  return { ruleName, plainEnglish, constraints, expiry, signerNote, backend, contentHash };
68
55
  }
69
- /** Render the OZ built-in primitive summary line. Only `spending_limit` is
70
- * quoted by the review card (it is the only primitive that defines a
71
- * transactional bound). Other primitives (threshold) are signer-config
72
- * concerns handled by the OZ adapter's own `uncovered` machinery.
73
- * Spending_limit takes `period_ledgers` on-chain (~5s/ledger); the card
74
- * states the window in seconds so the user reads it consistently with the
75
- * interpreter templates. */
76
- function renderOzPrimitive(ref) {
77
- if (ref.kind !== 'oz_builtin')
78
- return null;
79
- const primitive = ref.primitive;
80
- if (primitive.primitive !== 'spending_limit')
81
- return null;
82
- const params = primitive.params;
83
- const limit = params.spending_limit ?? '0';
84
- const periodLedgers = params.period_ledgers ?? 0;
85
- const windowSecs = periodLedgers * 5;
86
- return `spending_limit(${limit}, ${windowSecs})`;
87
- }
88
56
  /** Walk every comparison / membership node of the predicate and invoke
89
57
  * `visit` on each. The walk is depth-first, left-to-right, so the
90
58
  * constraint list is stable across runs. Pure boolean nodes contribute no
@@ -109,21 +77,14 @@ export function describePredicate(predicate) {
109
77
  function walkPredicate(node, visit) {
110
78
  switch (node.op) {
111
79
  case 'and':
112
- case 'or':
113
80
  for (const child of node.children)
114
81
  walkPredicate(child, visit);
115
82
  return;
116
- case 'not':
117
- walkPredicate(node.child, visit);
118
- return;
119
83
  case 'in':
120
84
  visit(node);
121
85
  return;
122
86
  case 'eq':
123
- case 'lt':
124
87
  case 'lte':
125
- case 'gt':
126
- case 'gte':
127
88
  visit(node);
128
89
  return;
129
90
  }
@@ -131,18 +92,13 @@ function walkPredicate(node, visit) {
131
92
  /** Render ONE constraint sentence for ONE interpreter predicate node. The
132
93
  * shape of the output is pinned by Task 7b so the test suite can assert
133
94
  * byte-for-byte equality. Returns `null` when the node is a structural
134
- * boolean (`and` / `or` / `not`) - those are not constraint leaves. */
95
+ * boolean (`and`) - that is not a constraint leaf. */
135
96
  function renderConstraint(node) {
136
97
  switch (node.op) {
137
98
  case 'and':
138
- case 'or':
139
- case 'not':
140
99
  return null;
141
100
  case 'eq':
142
- case 'lt':
143
101
  case 'lte':
144
- case 'gt':
145
- case 'gte':
146
102
  return renderComparison(node);
147
103
  case 'in':
148
104
  return renderMembership(node);
@@ -172,8 +128,8 @@ function renderComparison(node) {
172
128
  }
173
129
  // eq/lte(call_arg_field[i, el, field], <literal>) -> arg[i] element[el].field = <value>
174
130
  // The structured-argument bind pins a scalar field inside a vec element.
175
- // For lt/gt/lte/gte the line reads "OP <value>" so the comparison kind
176
- // is visible; eq reads "= <value>".
131
+ // For lte the line reads "OP <value>" so the comparison kind is visible;
132
+ // eq reads "= <value>".
177
133
  if (left.kind === 'call_arg_field') {
178
134
  const head = `arg[${left.index}] element[${left.element}].${left.field}`;
179
135
  const sep = node.op === 'eq' ? '=' : comparisonOpText(node.op);
@@ -181,10 +137,6 @@ function renderComparison(node) {
181
137
  return `${head} ${sep} ${right.value}`;
182
138
  if (right.kind === 'literal_symbol')
183
139
  return `${head} ${sep} ${right.value}`;
184
- if (right.kind === 'literal_bytes')
185
- return `${head} ${sep} ${right.value}`;
186
- if (right.kind === 'literal_u64')
187
- return `${head} ${sep} ${right.value}`;
188
140
  if (right.kind === 'literal_i128')
189
141
  return `${head} ${sep} ${right.value}`;
190
142
  if (right.kind === 'literal_u32')
@@ -193,22 +145,10 @@ function renderComparison(node) {
193
145
  return `${head} ${sep} [${right.elements.map(renderVecElement).join(', ')}]`;
194
146
  }
195
147
  }
196
- // The bound is compared against the calls ALREADY made in the window, so
197
- // `< N` permits N of them and `<= N` permits one more. Report how many
198
- // calls the rule allows rather than restating the comparison.
199
- if (left.kind === 'invocation_count_in_window' && right.kind === 'literal_u32') {
200
- const allowed = node.op === 'lt' ? right.value : right.value + 1;
201
- return `At most ${allowed} calls per ${left.windowSecs} seconds`;
202
- }
203
- // OP(call_arg[i], <scalar literal>) -> arg[i] OP <value>
204
- //
205
- // The per-call cap. It has to be here because the `amount` template below
206
- // is unreachable for an interpreter policy: `amount` is deliberately not in
207
- // the contract's grammar (dsl.rs - the interpreter sees one authorized
208
- // call, not the transaction's token movements), so a predicate using it is
209
- // refused at install. A bound on the call's own amount ARGUMENT is how a
210
- // cap is actually written, and it was rendering nothing at all - the card
211
- // silently understated the policy.
148
+ // The per-call cap: OP(call_arg[i], <scalar literal>) -> arg[i] OP <value>.
149
+ // The interpreter is passed one authorized call, not the transaction's token
150
+ // movements, so a bound on the call's own amount ARGUMENT is how a cap is
151
+ // written.
212
152
  //
213
153
  // Placed after the literal_vec case above so an exact-sequence `eq` still
214
154
  // reads as a path rather than as a comparison.
@@ -217,28 +157,12 @@ function renderComparison(node) {
217
157
  const sep = node.op === 'eq' ? '=' : comparisonOpText(node.op);
218
158
  if (right.kind === 'literal_i128')
219
159
  return `${head} ${sep} ${right.value}`;
220
- if (right.kind === 'literal_u64')
221
- return `${head} ${sep} ${right.value}`;
222
160
  if (right.kind === 'literal_u32')
223
161
  return `${head} ${sep} ${right.value}`;
224
162
  if (right.kind === 'literal_address')
225
163
  return `${head} ${sep} ${right.value}`;
226
164
  if (right.kind === 'literal_symbol')
227
165
  return `${head} ${sep} ${right.value}`;
228
- if (right.kind === 'literal_bytes')
229
- return `${head} ${sep} ${right.value}`;
230
- // call_arg_scaled is the slippage floor and reads as itself.
231
- if (right.kind === 'call_arg_scaled') {
232
- return `${head} >= arg[${right.index}] * ${right.num}/${right.den}`;
233
- }
234
- }
235
- // amount <= v -> Amount <= v
236
- if (left.kind === 'amount' && right.kind === 'literal_i128') {
237
- return `Amount <= ${right.value}`;
238
- }
239
- // oracle_price(asset) OP price -> Only when oracle_price(asset) OP price
240
- if (left.kind === 'oracle_price' && right.kind === 'oracle_threshold') {
241
- return `Only when oracle_price(${left.asset}) ${comparisonOpText(node.op)} ${right.value} (${right.decimals} dp)`;
242
166
  }
243
167
  // Any other comparison shape is a structural fail-closed: do not surface
244
168
  // a misleading line. Cross-check still requires every leaf produce a
@@ -255,74 +179,6 @@ function renderMembership(node) {
255
179
  }
256
180
  return null;
257
181
  }
258
- function renderVecElement(leaf) {
259
- switch (leaf.kind) {
260
- case 'literal_address':
261
- return leaf.value;
262
- case 'literal_i128':
263
- return leaf.value;
264
- case 'literal_symbol':
265
- return leaf.value;
266
- case 'literal_u32':
267
- return String(leaf.value);
268
- case 'literal_u64':
269
- return leaf.value;
270
- case 'literal_bytes':
271
- return leaf.value;
272
- case 'literal_vec':
273
- return `[${leaf.elements.map(renderVecElement).join(', ')}]`;
274
- case 'call_contract':
275
- case 'call_fn':
276
- case 'call_arg':
277
- case 'call_arg_len':
278
- case 'call_arg_field':
279
- case 'call_arg_scaled':
280
- case 'amount':
281
- case 'window_spent':
282
- case 'now':
283
- case 'valid_until':
284
- case 'invocation_count_in_window':
285
- case 'oracle_price':
286
- return `<${leaf.kind}>`;
287
- // Show the declared basis, not just the digits. A threshold on the wrong
288
- // basis is the one policy error the contract cannot detect, so the review
289
- // card is where a human has to be able to see it.
290
- case 'oracle_threshold':
291
- return `${leaf.value} (${leaf.decimals} dp)`;
292
- }
293
- }
294
- function renderHaystackElement(leaf) {
295
- if (leaf.kind === 'literal_address')
296
- return leaf.value;
297
- if (leaf.kind === 'literal_i128')
298
- return leaf.value;
299
- if (leaf.kind === 'literal_symbol')
300
- return leaf.value;
301
- if (leaf.kind === 'literal_u32')
302
- return String(leaf.value);
303
- if (leaf.kind === 'literal_u64')
304
- return leaf.value;
305
- if (leaf.kind === 'literal_bytes')
306
- return leaf.value;
307
- if (leaf.kind === 'literal_vec') {
308
- return `[${leaf.elements.map(renderHaystackElement).join(', ')}]`;
309
- }
310
- return `<${leaf.kind}>`;
311
- }
312
- function comparisonOpText(op) {
313
- switch (op) {
314
- case 'lt':
315
- return '<';
316
- case 'lte':
317
- return '<=';
318
- case 'gt':
319
- return '>';
320
- case 'gte':
321
- return '>=';
322
- case 'eq':
323
- return '==';
324
- }
325
- }
326
182
  /** Render the plain-English one-liner. Format: `<ruleName>: <constraints>`,
327
183
  * joined by `; ` so the user reads one sentence per constraint. */
328
184
  function renderPlainEnglish(ruleName, constraints) {
@@ -12,6 +12,7 @@
12
12
  // demands the summary carries the corresponding string. A naive summary
13
13
  // that drops a leaf (or fabricates a string not in the predicate) trips the
14
14
  // guard.
15
+ import { comparisonOpText, renderHaystackElement, renderVecElement } from "./render-leaf.js";
15
16
  /** Assert every leaf in the predicate appears as a constraint string in
16
17
  * `summary.constraints`. Returns the missing templates (without the leaf
17
18
  * values filled in) when any leaf was dropped. */
@@ -35,18 +36,11 @@ export function summaryCrossCheck(predicate, summary) {
35
36
  function collect(node, out) {
36
37
  switch (node.op) {
37
38
  case 'and':
38
- case 'or':
39
39
  for (const child of node.children)
40
40
  collect(child, out);
41
41
  return;
42
- case 'not':
43
- collect(node.child, out);
44
- return;
45
42
  case 'eq':
46
- case 'lt':
47
43
  case 'lte':
48
- case 'gt':
49
- case 'gte':
50
44
  pushComparison(node.left, node.right, node.op, out);
51
45
  return;
52
46
  case 'in':
@@ -84,14 +78,6 @@ function pushComparison(left, right, op, out) {
84
78
  out.push(`${head} ${sep} ${right.value}`);
85
79
  return;
86
80
  }
87
- if (right.kind === 'literal_bytes') {
88
- out.push(`${head} ${sep} ${right.value}`);
89
- return;
90
- }
91
- if (right.kind === 'literal_u64') {
92
- out.push(`${head} ${sep} ${right.value}`);
93
- return;
94
- }
95
81
  if (right.kind === 'literal_i128') {
96
82
  out.push(`${head} ${sep} ${right.value}`);
97
83
  return;
@@ -105,13 +91,6 @@ function pushComparison(left, right, op, out) {
105
91
  return;
106
92
  }
107
93
  }
108
- // Mirrors the builder's wording byte-for-byte: how many calls the rule
109
- // permits, since the bound counts the calls already made.
110
- if (left.kind === 'invocation_count_in_window' && right.kind === 'literal_u32') {
111
- const allowed = op === 'lt' ? right.value : right.value + 1;
112
- out.push(`At most ${allowed} calls per ${left.windowSecs} seconds`);
113
- return;
114
- }
115
94
  // Mirrors the builder's per-call cap line. Both sides must render the same
116
95
  // string: cross-check compares the summary against the predicate, so a
117
96
  // shape only ONE of them renders would read as a dropped constraint.
@@ -119,26 +98,12 @@ function pushComparison(left, right, op, out) {
119
98
  const head = `arg[${left.index}]`;
120
99
  const sep = op === 'eq' ? '=' : comparisonOpText(op);
121
100
  if (right.kind === 'literal_i128' ||
122
- right.kind === 'literal_u64' ||
123
101
  right.kind === 'literal_u32' ||
124
102
  right.kind === 'literal_address' ||
125
- right.kind === 'literal_symbol' ||
126
- right.kind === 'literal_bytes') {
103
+ right.kind === 'literal_symbol') {
127
104
  out.push(`${head} ${sep} ${right.value}`);
128
105
  return;
129
106
  }
130
- if (right.kind === 'call_arg_scaled') {
131
- out.push(`${head} >= arg[${right.index}] * ${right.num}/${right.den}`);
132
- return;
133
- }
134
- }
135
- if (left.kind === 'amount' && right.kind === 'literal_i128') {
136
- out.push(`Amount <= ${right.value}`);
137
- return;
138
- }
139
- if (left.kind === 'oracle_price' && right.kind === 'oracle_threshold') {
140
- out.push(`Only when oracle_price(${left.asset}) ${comparisonOpText(op)} ${right.value} (${right.decimals} dp)`);
141
- return;
142
107
  }
143
108
  }
144
109
  function pushMembership(needle, haystack, out) {
@@ -147,71 +112,3 @@ function pushMembership(needle, haystack, out) {
147
112
  const list = haystack.map(renderHaystackElement).join(', ');
148
113
  out.push(`Recipient/arg must be one of [${list}]`);
149
114
  }
150
- function renderVecElement(leaf) {
151
- switch (leaf.kind) {
152
- case 'literal_address':
153
- return leaf.value;
154
- case 'literal_i128':
155
- return leaf.value;
156
- case 'literal_symbol':
157
- return leaf.value;
158
- case 'literal_u32':
159
- return String(leaf.value);
160
- case 'literal_u64':
161
- return leaf.value;
162
- case 'literal_bytes':
163
- return leaf.value;
164
- case 'literal_vec':
165
- return `[${leaf.elements.map(renderVecElement).join(', ')}]`;
166
- case 'call_contract':
167
- case 'call_fn':
168
- case 'call_arg':
169
- case 'call_arg_len':
170
- case 'call_arg_field':
171
- case 'call_arg_scaled':
172
- case 'amount':
173
- case 'window_spent':
174
- case 'now':
175
- case 'valid_until':
176
- case 'invocation_count_in_window':
177
- case 'oracle_price':
178
- return `<${leaf.kind}>`;
179
- // Show the declared basis, not just the digits. A threshold on the wrong
180
- // basis is the one policy error the contract cannot detect, so the review
181
- // card is where a human has to be able to see it.
182
- case 'oracle_threshold':
183
- return `${leaf.value} (${leaf.decimals} dp)`;
184
- }
185
- }
186
- function renderHaystackElement(leaf) {
187
- if (leaf.kind === 'literal_address')
188
- return leaf.value;
189
- if (leaf.kind === 'literal_i128')
190
- return leaf.value;
191
- if (leaf.kind === 'literal_symbol')
192
- return leaf.value;
193
- if (leaf.kind === 'literal_u32')
194
- return String(leaf.value);
195
- if (leaf.kind === 'literal_u64')
196
- return leaf.value;
197
- if (leaf.kind === 'literal_bytes')
198
- return leaf.value;
199
- if (leaf.kind === 'literal_vec') {
200
- return `[${leaf.elements.map(renderHaystackElement).join(', ')}]`;
201
- }
202
- return `<${leaf.kind}>`;
203
- }
204
- function comparisonOpText(op) {
205
- switch (op) {
206
- case 'lt':
207
- return '<';
208
- case 'lte':
209
- return '<=';
210
- case 'gt':
211
- return '>';
212
- case 'gte':
213
- return '>=';
214
- case 'eq':
215
- return '==';
216
- }
217
- }
@@ -0,0 +1,4 @@
1
+ import type { PredicateLeaf } from '../types.ts';
2
+ export declare function renderVecElement(leaf: PredicateLeaf): string;
3
+ export declare function renderHaystackElement(leaf: PredicateLeaf): string;
4
+ export declare function comparisonOpText(op: 'eq' | 'lte'): string;
@@ -0,0 +1,47 @@
1
+ // Leaf and operator rendering shared by the review-card builder and the
2
+ // summary cross-check. Both turn the SAME predicate into human-readable text -
3
+ // the cross-check exists to confirm the builder's summary matches the predicate
4
+ // - so rendering a leaf differently in the two would make the check compare a
5
+ // string against itself under a different spelling and pass on a real mismatch.
6
+ export function renderVecElement(leaf) {
7
+ switch (leaf.kind) {
8
+ case 'literal_address':
9
+ return leaf.value;
10
+ case 'literal_i128':
11
+ return leaf.value;
12
+ case 'literal_symbol':
13
+ return leaf.value;
14
+ case 'literal_u32':
15
+ return String(leaf.value);
16
+ case 'literal_vec':
17
+ return `[${leaf.elements.map(renderVecElement).join(', ')}]`;
18
+ case 'call_contract':
19
+ case 'call_fn':
20
+ case 'call_arg':
21
+ case 'call_arg_len':
22
+ case 'call_arg_field':
23
+ return `<${leaf.kind}>`;
24
+ }
25
+ }
26
+ export function renderHaystackElement(leaf) {
27
+ if (leaf.kind === 'literal_address')
28
+ return leaf.value;
29
+ if (leaf.kind === 'literal_i128')
30
+ return leaf.value;
31
+ if (leaf.kind === 'literal_symbol')
32
+ return leaf.value;
33
+ if (leaf.kind === 'literal_u32')
34
+ return String(leaf.value);
35
+ if (leaf.kind === 'literal_vec') {
36
+ return `[${leaf.elements.map(renderHaystackElement).join(', ')}]`;
37
+ }
38
+ return `<${leaf.kind}>`;
39
+ }
40
+ export function comparisonOpText(op) {
41
+ switch (op) {
42
+ case 'lte':
43
+ return '<=';
44
+ case 'eq':
45
+ return '==';
46
+ }
47
+ }