@crediolabs/policy-synth 0.2.0 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (249) hide show
  1. package/README.md +4 -4
  2. package/dist/adapters/interpreter/adapter.d.ts +19 -25
  3. package/dist/adapters/interpreter/adapter.js +54 -487
  4. package/dist/errors.d.ts +1 -1
  5. package/dist/index.d.ts +1 -4
  6. package/dist/index.js +1 -4
  7. package/dist/install/build-add-context-rule.d.ts +3 -19
  8. package/dist/install/build-add-context-rule.js +3 -98
  9. package/dist/install/build-install-policy.d.ts +4 -53
  10. package/dist/install/build-install-policy.js +44 -123
  11. package/dist/install/index.d.ts +0 -2
  12. package/dist/install/index.js +0 -7
  13. package/dist/predicate/decode.d.ts +1 -1
  14. package/dist/predicate/decode.js +2 -70
  15. package/dist/predicate/encode.js +45 -229
  16. package/dist/predicate/from-json.js +1 -42
  17. package/dist/record/decode.js +3 -8
  18. package/dist/record/freshness.d.ts +17 -11
  19. package/dist/record/freshness.js +28 -18
  20. package/dist/review-card/builder.d.ts +4 -3
  21. package/dist/review-card/builder.js +18 -162
  22. package/dist/review-card/cross-check.js +2 -105
  23. package/dist/review-card/render-leaf.d.ts +4 -0
  24. package/dist/review-card/render-leaf.js +47 -0
  25. package/dist/run/index.d.ts +36 -139
  26. package/dist/run/index.js +125 -437
  27. package/dist/run/schemas.d.ts +168 -1929
  28. package/dist/run/schemas.js +40 -269
  29. package/dist/{synth → simulate}/deny-cases.d.ts +4 -8
  30. package/dist/simulate/deny-cases.js +282 -0
  31. package/dist/simulate/evaluate.d.ts +18 -0
  32. package/dist/simulate/evaluate.js +271 -0
  33. package/dist/simulate/index.d.ts +4 -0
  34. package/dist/simulate/index.js +8 -0
  35. package/dist/synth/compose-from-recording.d.ts +24 -51
  36. package/dist/synth/compose-from-recording.js +120 -282
  37. package/dist/synth/index.d.ts +0 -5
  38. package/dist/synth/index.js +0 -5
  39. package/dist/synth/synthesize-from-recording.d.ts +1 -29
  40. package/dist/synth/synthesize-from-recording.js +41 -356
  41. package/dist/types.d.ts +8 -60
  42. package/dist/types.js +5 -2
  43. package/dist-cjs/adapters/interpreter/adapter.d.ts +19 -25
  44. package/dist-cjs/adapters/interpreter/adapter.js +56 -489
  45. package/dist-cjs/errors.d.ts +1 -1
  46. package/dist-cjs/index.d.ts +1 -4
  47. package/dist-cjs/index.js +1 -4
  48. package/dist-cjs/install/build-add-context-rule.d.ts +3 -19
  49. package/dist-cjs/install/build-add-context-rule.js +1 -97
  50. package/dist-cjs/install/build-install-policy.d.ts +4 -53
  51. package/dist-cjs/install/build-install-policy.js +44 -122
  52. package/dist-cjs/install/index.d.ts +0 -2
  53. package/dist-cjs/install/index.js +2 -23
  54. package/dist-cjs/predicate/decode.d.ts +1 -1
  55. package/dist-cjs/predicate/decode.js +2 -70
  56. package/dist-cjs/predicate/encode.js +45 -229
  57. package/dist-cjs/predicate/from-json.js +1 -42
  58. package/dist-cjs/record/decode.js +3 -8
  59. package/dist-cjs/record/freshness.d.ts +17 -11
  60. package/dist-cjs/record/freshness.js +28 -18
  61. package/dist-cjs/review-card/builder.d.ts +4 -3
  62. package/dist-cjs/review-card/builder.js +23 -167
  63. package/dist-cjs/review-card/cross-check.js +7 -110
  64. package/dist-cjs/review-card/render-leaf.d.ts +4 -0
  65. package/dist-cjs/review-card/render-leaf.js +52 -0
  66. package/dist-cjs/run/index.d.ts +36 -139
  67. package/dist-cjs/run/index.js +125 -444
  68. package/dist-cjs/run/schemas.d.ts +168 -1929
  69. package/dist-cjs/run/schemas.js +41 -270
  70. package/dist-cjs/{synth → simulate}/deny-cases.d.ts +4 -8
  71. package/dist-cjs/simulate/deny-cases.js +286 -0
  72. package/dist-cjs/simulate/evaluate.d.ts +18 -0
  73. package/dist-cjs/simulate/evaluate.js +274 -0
  74. package/dist-cjs/simulate/index.d.ts +4 -0
  75. package/dist-cjs/simulate/index.js +13 -0
  76. package/dist-cjs/synth/compose-from-recording.d.ts +24 -51
  77. package/dist-cjs/synth/compose-from-recording.js +120 -282
  78. package/dist-cjs/synth/index.d.ts +0 -5
  79. package/dist-cjs/synth/index.js +1 -11
  80. package/dist-cjs/synth/synthesize-from-recording.d.ts +1 -29
  81. package/dist-cjs/synth/synthesize-from-recording.js +39 -354
  82. package/dist-cjs/types.d.ts +8 -60
  83. package/dist-cjs/types.js +6 -3
  84. package/package.json +5 -5
  85. package/src/adapters/interpreter/adapter.ts +76 -572
  86. package/src/errors.ts +0 -19
  87. package/src/index.ts +1 -4
  88. package/src/install/build-add-context-rule.ts +5 -139
  89. package/src/install/build-install-policy.ts +93 -214
  90. package/src/install/index.ts +0 -34
  91. package/src/predicate/decode.ts +2 -70
  92. package/src/predicate/encode.ts +49 -261
  93. package/src/predicate/from-json.ts +1 -43
  94. package/src/record/decode.ts +3 -8
  95. package/src/record/freshness.ts +29 -18
  96. package/src/review-card/builder.ts +19 -168
  97. package/src/review-card/cross-check.ts +3 -105
  98. package/src/review-card/render-leaf.ts +48 -0
  99. package/src/run/index.ts +144 -572
  100. package/src/run/schemas.ts +42 -291
  101. package/src/simulate/deny-cases.ts +334 -0
  102. package/src/simulate/evaluate.ts +284 -0
  103. package/src/simulate/index.ts +11 -0
  104. package/src/synth/compose-from-recording.ts +148 -347
  105. package/src/synth/index.ts +0 -13
  106. package/src/synth/synthesize-from-recording.ts +43 -456
  107. package/src/types.ts +13 -49
  108. package/dist/adapters/interpreter/index.d.ts +0 -1
  109. package/dist/adapters/interpreter/index.js +0 -2
  110. package/dist/adapters/oz/adapter.d.ts +0 -20
  111. package/dist/adapters/oz/adapter.js +0 -295
  112. package/dist/adapters/oz/index.d.ts +0 -1
  113. package/dist/adapters/oz/index.js +0 -2
  114. package/dist/codegen/compile-gate.d.ts +0 -33
  115. package/dist/codegen/compile-gate.js +0 -124
  116. package/dist/codegen/index.d.ts +0 -2
  117. package/dist/codegen/index.js +0 -8
  118. package/dist/codegen/template.d.ts +0 -18
  119. package/dist/codegen/template.js +0 -148
  120. package/dist/install/authority-overlap.d.ts +0 -134
  121. package/dist/install/authority-overlap.js +0 -0
  122. package/dist/install/build-install-predicate.d.ts +0 -96
  123. package/dist/install/build-install-predicate.js +0 -444
  124. package/dist/install/build-merge-policy.d.ts +0 -70
  125. package/dist/install/build-merge-policy.js +0 -130
  126. package/dist/install/plan-merge-policy.d.ts +0 -49
  127. package/dist/install/plan-merge-policy.js +0 -86
  128. package/dist/install/read-account-rules.d.ts +0 -100
  129. package/dist/install/read-account-rules.js +0 -283
  130. package/dist/ir/index.d.ts +0 -1
  131. package/dist/ir/index.js +0 -2
  132. package/dist/ir/types.d.ts +0 -140
  133. package/dist/ir/types.js +0 -11
  134. package/dist/mandate/index.d.ts +0 -2
  135. package/dist/mandate/index.js +0 -2
  136. package/dist/mandate/to-ir.d.ts +0 -3
  137. package/dist/mandate/to-ir.js +0 -60
  138. package/dist/mandate/types.d.ts +0 -20
  139. package/dist/mandate/types.js +0 -8
  140. package/dist/seams/index.d.ts +0 -1
  141. package/dist/seams/index.js +0 -2
  142. package/dist/seams/types.d.ts +0 -66
  143. package/dist/seams/types.js +0 -11
  144. package/dist/synth/deny-cases.js +0 -562
  145. package/dist/synth/evaluate.d.ts +0 -39
  146. package/dist/synth/evaluate.js +0 -551
  147. package/dist/synth/harness.d.ts +0 -28
  148. package/dist/synth/harness.js +0 -47
  149. package/dist/synth/minimize.d.ts +0 -4
  150. package/dist/synth/minimize.js +0 -38
  151. package/dist/synth/permit-context.d.ts +0 -15
  152. package/dist/synth/permit-context.js +0 -116
  153. package/dist/synth/predicate-literals.d.ts +0 -5
  154. package/dist/synth/predicate-literals.js +0 -25
  155. package/dist/synth/synthesize-from-mandate.d.ts +0 -20
  156. package/dist/synth/synthesize-from-mandate.js +0 -59
  157. package/dist/verify/envelope.d.ts +0 -15
  158. package/dist/verify/envelope.js +0 -22
  159. package/dist/verify/index.d.ts +0 -3
  160. package/dist/verify/index.js +0 -3
  161. package/dist/verify/simulate.d.ts +0 -31
  162. package/dist/verify/simulate.js +0 -258
  163. package/dist/verify/verify.d.ts +0 -21
  164. package/dist/verify/verify.js +0 -189
  165. package/dist-cjs/adapters/interpreter/index.d.ts +0 -1
  166. package/dist-cjs/adapters/interpreter/index.js +0 -8
  167. package/dist-cjs/adapters/oz/adapter.d.ts +0 -20
  168. package/dist-cjs/adapters/oz/adapter.js +0 -300
  169. package/dist-cjs/adapters/oz/index.d.ts +0 -1
  170. package/dist-cjs/adapters/oz/index.js +0 -8
  171. package/dist-cjs/codegen/compile-gate.d.ts +0 -33
  172. package/dist-cjs/codegen/compile-gate.js +0 -128
  173. package/dist-cjs/codegen/index.d.ts +0 -2
  174. package/dist-cjs/codegen/index.js +0 -14
  175. package/dist-cjs/codegen/template.d.ts +0 -18
  176. package/dist-cjs/codegen/template.js +0 -151
  177. package/dist-cjs/install/authority-overlap.d.ts +0 -134
  178. package/dist-cjs/install/authority-overlap.js +0 -0
  179. package/dist-cjs/install/build-install-predicate.d.ts +0 -96
  180. package/dist-cjs/install/build-install-predicate.js +0 -479
  181. package/dist-cjs/install/build-merge-policy.d.ts +0 -70
  182. package/dist-cjs/install/build-merge-policy.js +0 -134
  183. package/dist-cjs/install/plan-merge-policy.d.ts +0 -49
  184. package/dist-cjs/install/plan-merge-policy.js +0 -90
  185. package/dist-cjs/install/read-account-rules.d.ts +0 -100
  186. package/dist-cjs/install/read-account-rules.js +0 -296
  187. package/dist-cjs/ir/index.d.ts +0 -1
  188. package/dist-cjs/ir/index.js +0 -3
  189. package/dist-cjs/ir/types.d.ts +0 -140
  190. package/dist-cjs/ir/types.js +0 -12
  191. package/dist-cjs/mandate/index.d.ts +0 -2
  192. package/dist-cjs/mandate/index.js +0 -6
  193. package/dist-cjs/mandate/to-ir.d.ts +0 -3
  194. package/dist-cjs/mandate/to-ir.js +0 -63
  195. package/dist-cjs/mandate/types.d.ts +0 -20
  196. package/dist-cjs/mandate/types.js +0 -9
  197. package/dist-cjs/seams/index.d.ts +0 -1
  198. package/dist-cjs/seams/index.js +0 -3
  199. package/dist-cjs/seams/types.d.ts +0 -66
  200. package/dist-cjs/seams/types.js +0 -12
  201. package/dist-cjs/synth/deny-cases.js +0 -568
  202. package/dist-cjs/synth/evaluate.d.ts +0 -39
  203. package/dist-cjs/synth/evaluate.js +0 -554
  204. package/dist-cjs/synth/harness.d.ts +0 -28
  205. package/dist-cjs/synth/harness.js +0 -50
  206. package/dist-cjs/synth/minimize.d.ts +0 -4
  207. package/dist-cjs/synth/minimize.js +0 -41
  208. package/dist-cjs/synth/permit-context.d.ts +0 -15
  209. package/dist-cjs/synth/permit-context.js +0 -119
  210. package/dist-cjs/synth/predicate-literals.d.ts +0 -5
  211. package/dist-cjs/synth/predicate-literals.js +0 -28
  212. package/dist-cjs/synth/synthesize-from-mandate.d.ts +0 -20
  213. package/dist-cjs/synth/synthesize-from-mandate.js +0 -62
  214. package/dist-cjs/verify/envelope.d.ts +0 -15
  215. package/dist-cjs/verify/envelope.js +0 -23
  216. package/dist-cjs/verify/index.d.ts +0 -3
  217. package/dist-cjs/verify/index.js +0 -8
  218. package/dist-cjs/verify/simulate.d.ts +0 -31
  219. package/dist-cjs/verify/simulate.js +0 -261
  220. package/dist-cjs/verify/verify.d.ts +0 -21
  221. package/dist-cjs/verify/verify.js +0 -192
  222. package/src/adapters/interpreter/index.ts +0 -8
  223. package/src/adapters/oz/adapter.ts +0 -376
  224. package/src/adapters/oz/index.ts +0 -9
  225. package/src/codegen/compile-gate.ts +0 -167
  226. package/src/codegen/index.ts +0 -17
  227. package/src/codegen/template.ts +0 -165
  228. package/src/install/authority-overlap.ts +0 -0
  229. package/src/install/build-merge-policy.ts +0 -219
  230. package/src/install/plan-merge-policy.ts +0 -133
  231. package/src/install/read-account-rules.ts +0 -376
  232. package/src/ir/index.ts +0 -13
  233. package/src/ir/types.ts +0 -132
  234. package/src/mandate/index.ts +0 -4
  235. package/src/mandate/to-ir.ts +0 -71
  236. package/src/mandate/types.ts +0 -21
  237. package/src/seams/index.ts +0 -11
  238. package/src/seams/types.ts +0 -81
  239. package/src/synth/deny-cases.ts +0 -663
  240. package/src/synth/evaluate.ts +0 -613
  241. package/src/synth/harness.ts +0 -68
  242. package/src/synth/minimize.ts +0 -48
  243. package/src/synth/permit-context.ts +0 -136
  244. package/src/synth/predicate-literals.ts +0 -27
  245. package/src/synth/synthesize-from-mandate.ts +0 -82
  246. package/src/verify/envelope.ts +0 -28
  247. package/src/verify/index.ts +0 -5
  248. package/src/verify/simulate.ts +0 -311
  249. package/src/verify/verify.ts +0 -243
@@ -6,23 +6,13 @@
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
@@ -30,14 +20,8 @@
30
20
  // hash. There is no clock; the hash never includes a timestamp.
31
21
 
32
22
  import { createHash } from 'node:crypto'
33
- import type {
34
- ContextRuleDraft,
35
- OZPrimitiveConfig,
36
- PolicyRef,
37
- PredicateLeaf,
38
- PredicateNode,
39
- } from '../types.ts'
40
- import type { SimulationResult } from '../verify/envelope.ts'
23
+ import type { ContextRuleDraft, PredicateNode } from '../types.ts'
24
+ import { comparisonOpText, renderHaystackElement, renderVecElement } from './render-leaf.ts'
41
25
 
42
26
  export interface ReviewCardSummary {
43
27
  ruleName: string
@@ -59,15 +43,11 @@ export interface ReviewCardSummary {
59
43
  * simulation result. Pure: same inputs -> byte-identical output. */
60
44
  export function buildReviewCardSummary(
61
45
  predicate: PredicateNode | null,
62
- policyRefs: PolicyRef[],
63
46
  contextRule: ContextRuleDraft,
64
- simulation: SimulationResult
47
+ /** Evaluator identity folded into the content hash. */
48
+ backend: 'interpreter-v1' | 'ts-model'
65
49
  ): ReviewCardSummary {
66
50
  const constraints: string[] = []
67
- for (const ref of policyRefs) {
68
- const line = renderOzPrimitive(ref)
69
- if (line !== null) constraints.push(line)
70
- }
71
51
  if (predicate !== null) {
72
52
  walkPredicate(predicate, (node) => {
73
53
  const line = renderConstraint(node)
@@ -86,7 +66,6 @@ export function buildReviewCardSummary(
86
66
  contextRule.signers.length >= 2
87
67
  ? 'any ONE signer may authorise a permitted op under this rule (OZ any-of-N semantic)'
88
68
  : null
89
- const backend = simulation.backend
90
69
 
91
70
  const contentHash = computeContentHash({
92
71
  ruleName,
@@ -100,24 +79,6 @@ export function buildReviewCardSummary(
100
79
  return { ruleName, plainEnglish, constraints, expiry, signerNote, backend, contentHash }
101
80
  }
102
81
 
103
- /** Render the OZ built-in primitive summary line. Only `spending_limit` is
104
- * quoted by the review card (it is the only primitive that defines a
105
- * transactional bound). Other primitives (threshold) are signer-config
106
- * concerns handled by the OZ adapter's own `uncovered` machinery.
107
- * Spending_limit takes `period_ledgers` on-chain (~5s/ledger); the card
108
- * states the window in seconds so the user reads it consistently with the
109
- * interpreter templates. */
110
- function renderOzPrimitive(ref: PolicyRef): string | null {
111
- if (ref.kind !== 'oz_builtin') return null
112
- const primitive: OZPrimitiveConfig = ref.primitive
113
- if (primitive.primitive !== 'spending_limit') return null
114
- const params = primitive.params as { spending_limit?: string; period_ledgers?: number }
115
- const limit = params.spending_limit ?? '0'
116
- const periodLedgers = params.period_ledgers ?? 0
117
- const windowSecs = periodLedgers * 5
118
- return `spending_limit(${limit}, ${windowSecs})`
119
- }
120
-
121
82
  /** Walk every comparison / membership node of the predicate and invoke
122
83
  * `visit` on each. The walk is depth-first, left-to-right, so the
123
84
  * constraint list is stable across runs. Pure boolean nodes contribute no
@@ -142,20 +103,13 @@ export function describePredicate(predicate: PredicateNode): string[] {
142
103
  function walkPredicate(node: PredicateNode, visit: (node: PredicateNode) => void): void {
143
104
  switch (node.op) {
144
105
  case 'and':
145
- case 'or':
146
106
  for (const child of node.children) walkPredicate(child, visit)
147
107
  return
148
- case 'not':
149
- walkPredicate(node.child, visit)
150
- return
151
108
  case 'in':
152
109
  visit(node)
153
110
  return
154
111
  case 'eq':
155
- case 'lt':
156
112
  case 'lte':
157
- case 'gt':
158
- case 'gte':
159
113
  visit(node)
160
114
  return
161
115
  }
@@ -164,27 +118,20 @@ function walkPredicate(node: PredicateNode, visit: (node: PredicateNode) => void
164
118
  /** Render ONE constraint sentence for ONE interpreter predicate node. The
165
119
  * shape of the output is pinned by Task 7b so the test suite can assert
166
120
  * byte-for-byte equality. Returns `null` when the node is a structural
167
- * boolean (`and` / `or` / `not`) - those are not constraint leaves. */
121
+ * boolean (`and`) - that is not a constraint leaf. */
168
122
  function renderConstraint(node: PredicateNode): string | null {
169
123
  switch (node.op) {
170
124
  case 'and':
171
- case 'or':
172
- case 'not':
173
125
  return null
174
126
  case 'eq':
175
- case 'lt':
176
127
  case 'lte':
177
- case 'gt':
178
- case 'gte':
179
128
  return renderComparison(node)
180
129
  case 'in':
181
130
  return renderMembership(node)
182
131
  }
183
132
  }
184
133
 
185
- function renderComparison(
186
- node: Extract<PredicateNode, { op: 'eq' | 'lt' | 'lte' | 'gt' | 'gte' }>
187
- ): string | null {
134
+ function renderComparison(node: Extract<PredicateNode, { op: 'eq' | 'lte' }>): string | null {
188
135
  const left = node.left
189
136
  const right = node.right
190
137
 
@@ -213,15 +160,13 @@ function renderComparison(
213
160
 
214
161
  // eq/lte(call_arg_field[i, el, field], <literal>) -> arg[i] element[el].field = <value>
215
162
  // The structured-argument bind pins a scalar field inside a vec element.
216
- // For lt/gt/lte/gte the line reads "OP <value>" so the comparison kind
217
- // is visible; eq reads "= <value>".
163
+ // For lte the line reads "OP <value>" so the comparison kind is visible;
164
+ // eq reads "= <value>".
218
165
  if (left.kind === 'call_arg_field') {
219
166
  const head = `arg[${left.index}] element[${left.element}].${left.field}`
220
167
  const sep = node.op === 'eq' ? '=' : comparisonOpText(node.op)
221
168
  if (right.kind === 'literal_address') return `${head} ${sep} ${right.value}`
222
169
  if (right.kind === 'literal_symbol') return `${head} ${sep} ${right.value}`
223
- if (right.kind === 'literal_bytes') return `${head} ${sep} ${right.value}`
224
- if (right.kind === 'literal_u64') return `${head} ${sep} ${right.value}`
225
170
  if (right.kind === 'literal_i128') return `${head} ${sep} ${right.value}`
226
171
  if (right.kind === 'literal_u32') return `${head} ${sep} ${right.value}`
227
172
  if (right.kind === 'literal_vec') {
@@ -229,23 +174,10 @@ function renderComparison(
229
174
  }
230
175
  }
231
176
 
232
- // The bound is compared against the calls ALREADY made in the window, so
233
- // `< N` permits N of them and `<= N` permits one more. Report how many
234
- // calls the rule allows rather than restating the comparison.
235
- if (left.kind === 'invocation_count_in_window' && right.kind === 'literal_u32') {
236
- const allowed = node.op === 'lt' ? right.value : right.value + 1
237
- return `At most ${allowed} calls per ${left.windowSecs} seconds`
238
- }
239
-
240
- // OP(call_arg[i], <scalar literal>) -> arg[i] OP <value>
241
- //
242
- // The per-call cap. It has to be here because the `amount` template below
243
- // is unreachable for an interpreter policy: `amount` is deliberately not in
244
- // the contract's grammar (dsl.rs - the interpreter sees one authorized
245
- // call, not the transaction's token movements), so a predicate using it is
246
- // refused at install. A bound on the call's own amount ARGUMENT is how a
247
- // cap is actually written, and it was rendering nothing at all - the card
248
- // silently understated the policy.
177
+ // The per-call cap: OP(call_arg[i], <scalar literal>) -> arg[i] OP <value>.
178
+ // The interpreter is passed one authorized call, not the transaction's token
179
+ // movements, so a bound on the call's own amount ARGUMENT is how a cap is
180
+ // written.
249
181
  //
250
182
  // Placed after the literal_vec case above so an exact-sequence `eq` still
251
183
  // reads as a path rather than as a comparison.
@@ -253,25 +185,9 @@ function renderComparison(
253
185
  const head = `arg[${left.index}]`
254
186
  const sep = node.op === 'eq' ? '=' : comparisonOpText(node.op)
255
187
  if (right.kind === 'literal_i128') return `${head} ${sep} ${right.value}`
256
- if (right.kind === 'literal_u64') return `${head} ${sep} ${right.value}`
257
188
  if (right.kind === 'literal_u32') return `${head} ${sep} ${right.value}`
258
189
  if (right.kind === 'literal_address') return `${head} ${sep} ${right.value}`
259
190
  if (right.kind === 'literal_symbol') return `${head} ${sep} ${right.value}`
260
- if (right.kind === 'literal_bytes') return `${head} ${sep} ${right.value}`
261
- // call_arg_scaled is the slippage floor and reads as itself.
262
- if (right.kind === 'call_arg_scaled') {
263
- return `${head} >= arg[${right.index}] * ${right.num}/${right.den}`
264
- }
265
- }
266
-
267
- // amount <= v -> Amount <= v
268
- if (left.kind === 'amount' && right.kind === 'literal_i128') {
269
- return `Amount <= ${right.value}`
270
- }
271
-
272
- // oracle_price(asset) OP price -> Only when oracle_price(asset) OP price
273
- if (left.kind === 'oracle_price' && right.kind === 'oracle_threshold') {
274
- return `Only when oracle_price(${left.asset}) ${comparisonOpText(node.op)} ${right.value} (${right.decimals} dp)`
275
191
  }
276
192
 
277
193
  // Any other comparison shape is a structural fail-closed: do not surface
@@ -291,71 +207,6 @@ function renderMembership(node: Extract<PredicateNode, { op: 'in' }>): string |
291
207
  return null
292
208
  }
293
209
 
294
- function renderVecElement(leaf: PredicateLeaf): string {
295
- switch (leaf.kind) {
296
- case 'literal_address':
297
- return leaf.value
298
- case 'literal_i128':
299
- return leaf.value
300
- case 'literal_symbol':
301
- return leaf.value
302
- case 'literal_u32':
303
- return String(leaf.value)
304
- case 'literal_u64':
305
- return leaf.value
306
- case 'literal_bytes':
307
- return leaf.value
308
- case 'literal_vec':
309
- return `[${leaf.elements.map(renderVecElement).join(', ')}]`
310
- case 'call_contract':
311
- case 'call_fn':
312
- case 'call_arg':
313
- case 'call_arg_len':
314
- case 'call_arg_field':
315
- case 'call_arg_scaled':
316
- case 'amount':
317
- case 'window_spent':
318
- case 'now':
319
- case 'valid_until':
320
- case 'invocation_count_in_window':
321
- case 'oracle_price':
322
- return `<${leaf.kind}>`
323
- // Show the declared basis, not just the digits. A threshold on the wrong
324
- // basis is the one policy error the contract cannot detect, so the review
325
- // card is where a human has to be able to see it.
326
- case 'oracle_threshold':
327
- return `${leaf.value} (${leaf.decimals} dp)`
328
- }
329
- }
330
-
331
- function renderHaystackElement(leaf: PredicateLeaf): string {
332
- if (leaf.kind === 'literal_address') return leaf.value
333
- if (leaf.kind === 'literal_i128') return leaf.value
334
- if (leaf.kind === 'literal_symbol') return leaf.value
335
- if (leaf.kind === 'literal_u32') return String(leaf.value)
336
- if (leaf.kind === 'literal_u64') return leaf.value
337
- if (leaf.kind === 'literal_bytes') return leaf.value
338
- if (leaf.kind === 'literal_vec') {
339
- return `[${leaf.elements.map(renderHaystackElement).join(', ')}]`
340
- }
341
- return `<${leaf.kind}>`
342
- }
343
-
344
- function comparisonOpText(op: 'eq' | 'lt' | 'lte' | 'gt' | 'gte'): string {
345
- switch (op) {
346
- case 'lt':
347
- return '<'
348
- case 'lte':
349
- return '<='
350
- case 'gt':
351
- return '>'
352
- case 'gte':
353
- return '>='
354
- case 'eq':
355
- return '=='
356
- }
357
- }
358
-
359
210
  /** Render the plain-English one-liner. Format: `<ruleName>: <constraints>`,
360
211
  * joined by `; ` so the user reads one sentence per constraint. */
361
212
  function renderPlainEnglish(ruleName: string, constraints: string[]): string {
@@ -15,6 +15,7 @@
15
15
 
16
16
  import type { PredicateLeaf, PredicateNode } from '../types.ts'
17
17
  import type { ReviewCardSummary } from './builder.ts'
18
+ import { comparisonOpText, renderHaystackElement, renderVecElement } from './render-leaf.ts'
18
19
 
19
20
  /** Assert every leaf in the predicate appears as a constraint string in
20
21
  * `summary.constraints`. Returns the missing templates (without the leaf
@@ -43,17 +44,10 @@ export function summaryCrossCheck(
43
44
  function collect(node: PredicateNode, out: string[]): void {
44
45
  switch (node.op) {
45
46
  case 'and':
46
- case 'or':
47
47
  for (const child of node.children) collect(child, out)
48
48
  return
49
- case 'not':
50
- collect(node.child, out)
51
- return
52
49
  case 'eq':
53
- case 'lt':
54
50
  case 'lte':
55
- case 'gt':
56
- case 'gte':
57
51
  pushComparison(node.left, node.right, node.op, out)
58
52
  return
59
53
  case 'in':
@@ -65,7 +59,7 @@ function collect(node: PredicateNode, out: string[]): void {
65
59
  function pushComparison(
66
60
  left: PredicateLeaf,
67
61
  right: PredicateLeaf,
68
- op: 'eq' | 'lt' | 'lte' | 'gt' | 'gte',
62
+ op: 'eq' | 'lte',
69
63
  out: string[]
70
64
  ): void {
71
65
  if (left.kind === 'call_contract' && op === 'eq' && right.kind === 'literal_address') {
@@ -97,14 +91,6 @@ function pushComparison(
97
91
  out.push(`${head} ${sep} ${right.value}`)
98
92
  return
99
93
  }
100
- if (right.kind === 'literal_bytes') {
101
- out.push(`${head} ${sep} ${right.value}`)
102
- return
103
- }
104
- if (right.kind === 'literal_u64') {
105
- out.push(`${head} ${sep} ${right.value}`)
106
- return
107
- }
108
94
  if (right.kind === 'literal_i128') {
109
95
  out.push(`${head} ${sep} ${right.value}`)
110
96
  return
@@ -118,13 +104,6 @@ function pushComparison(
118
104
  return
119
105
  }
120
106
  }
121
- // Mirrors the builder's wording byte-for-byte: how many calls the rule
122
- // permits, since the bound counts the calls already made.
123
- if (left.kind === 'invocation_count_in_window' && right.kind === 'literal_u32') {
124
- const allowed = op === 'lt' ? right.value : right.value + 1
125
- out.push(`At most ${allowed} calls per ${left.windowSecs} seconds`)
126
- return
127
- }
128
107
  // Mirrors the builder's per-call cap line. Both sides must render the same
129
108
  // string: cross-check compares the summary against the predicate, so a
130
109
  // shape only ONE of them renders would read as a dropped constraint.
@@ -133,29 +112,13 @@ function pushComparison(
133
112
  const sep = op === 'eq' ? '=' : comparisonOpText(op)
134
113
  if (
135
114
  right.kind === 'literal_i128' ||
136
- right.kind === 'literal_u64' ||
137
115
  right.kind === 'literal_u32' ||
138
116
  right.kind === 'literal_address' ||
139
- right.kind === 'literal_symbol' ||
140
- right.kind === 'literal_bytes'
117
+ right.kind === 'literal_symbol'
141
118
  ) {
142
119
  out.push(`${head} ${sep} ${right.value}`)
143
120
  return
144
121
  }
145
- if (right.kind === 'call_arg_scaled') {
146
- out.push(`${head} >= arg[${right.index}] * ${right.num}/${right.den}`)
147
- return
148
- }
149
- }
150
- if (left.kind === 'amount' && right.kind === 'literal_i128') {
151
- out.push(`Amount <= ${right.value}`)
152
- return
153
- }
154
- if (left.kind === 'oracle_price' && right.kind === 'oracle_threshold') {
155
- out.push(
156
- `Only when oracle_price(${left.asset}) ${comparisonOpText(op)} ${right.value} (${right.decimals} dp)`
157
- )
158
- return
159
122
  }
160
123
  }
161
124
 
@@ -164,68 +127,3 @@ function pushMembership(needle: PredicateLeaf, haystack: PredicateLeaf[], out: s
164
127
  const list = haystack.map(renderHaystackElement).join(', ')
165
128
  out.push(`Recipient/arg must be one of [${list}]`)
166
129
  }
167
-
168
- function renderVecElement(leaf: PredicateLeaf): string {
169
- switch (leaf.kind) {
170
- case 'literal_address':
171
- return leaf.value
172
- case 'literal_i128':
173
- return leaf.value
174
- case 'literal_symbol':
175
- return leaf.value
176
- case 'literal_u32':
177
- return String(leaf.value)
178
- case 'literal_u64':
179
- return leaf.value
180
- case 'literal_bytes':
181
- return leaf.value
182
- case 'literal_vec':
183
- return `[${leaf.elements.map(renderVecElement).join(', ')}]`
184
- case 'call_contract':
185
- case 'call_fn':
186
- case 'call_arg':
187
- case 'call_arg_len':
188
- case 'call_arg_field':
189
- case 'call_arg_scaled':
190
- case 'amount':
191
- case 'window_spent':
192
- case 'now':
193
- case 'valid_until':
194
- case 'invocation_count_in_window':
195
- case 'oracle_price':
196
- return `<${leaf.kind}>`
197
- // Show the declared basis, not just the digits. A threshold on the wrong
198
- // basis is the one policy error the contract cannot detect, so the review
199
- // card is where a human has to be able to see it.
200
- case 'oracle_threshold':
201
- return `${leaf.value} (${leaf.decimals} dp)`
202
- }
203
- }
204
-
205
- function renderHaystackElement(leaf: PredicateLeaf): string {
206
- if (leaf.kind === 'literal_address') return leaf.value
207
- if (leaf.kind === 'literal_i128') return leaf.value
208
- if (leaf.kind === 'literal_symbol') return leaf.value
209
- if (leaf.kind === 'literal_u32') return String(leaf.value)
210
- if (leaf.kind === 'literal_u64') return leaf.value
211
- if (leaf.kind === 'literal_bytes') return leaf.value
212
- if (leaf.kind === 'literal_vec') {
213
- return `[${leaf.elements.map(renderHaystackElement).join(', ')}]`
214
- }
215
- return `<${leaf.kind}>`
216
- }
217
-
218
- function comparisonOpText(op: 'eq' | 'lt' | 'lte' | 'gt' | 'gte'): string {
219
- switch (op) {
220
- case 'lt':
221
- return '<'
222
- case 'lte':
223
- return '<='
224
- case 'gt':
225
- return '>'
226
- case 'gte':
227
- return '>='
228
- case 'eq':
229
- return '=='
230
- }
231
- }
@@ -0,0 +1,48 @@
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
+
7
+ import type { PredicateLeaf } from '../types.ts'
8
+
9
+ export function renderVecElement(leaf: PredicateLeaf): string {
10
+ switch (leaf.kind) {
11
+ case 'literal_address':
12
+ return leaf.value
13
+ case 'literal_i128':
14
+ return leaf.value
15
+ case 'literal_symbol':
16
+ return leaf.value
17
+ case 'literal_u32':
18
+ return String(leaf.value)
19
+ case 'literal_vec':
20
+ return `[${leaf.elements.map(renderVecElement).join(', ')}]`
21
+ case 'call_contract':
22
+ case 'call_fn':
23
+ case 'call_arg':
24
+ case 'call_arg_len':
25
+ case 'call_arg_field':
26
+ return `<${leaf.kind}>`
27
+ }
28
+ }
29
+
30
+ export function renderHaystackElement(leaf: PredicateLeaf): string {
31
+ if (leaf.kind === 'literal_address') return leaf.value
32
+ if (leaf.kind === 'literal_i128') return leaf.value
33
+ if (leaf.kind === 'literal_symbol') return leaf.value
34
+ if (leaf.kind === 'literal_u32') return String(leaf.value)
35
+ if (leaf.kind === 'literal_vec') {
36
+ return `[${leaf.elements.map(renderHaystackElement).join(', ')}]`
37
+ }
38
+ return `<${leaf.kind}>`
39
+ }
40
+
41
+ export function comparisonOpText(op: 'eq' | 'lte'): string {
42
+ switch (op) {
43
+ case 'lte':
44
+ return '<='
45
+ case 'eq':
46
+ return '=='
47
+ }
48
+ }