@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.
- package/README.md +4 -4
- package/dist/adapters/interpreter/adapter.d.ts +19 -25
- package/dist/adapters/interpreter/adapter.js +54 -487
- package/dist/adapters/interpreter/index.d.ts +1 -1
- package/dist/adapters/interpreter/index.js +1 -1
- package/dist/adapters/oz/adapter.js +1 -15
- package/dist/errors.d.ts +1 -1
- package/dist/index.d.ts +1 -4
- package/dist/index.js +1 -4
- package/dist/install/build-add-context-rule.d.ts +3 -19
- package/dist/install/build-add-context-rule.js +3 -98
- package/dist/install/build-install-policy.d.ts +4 -53
- package/dist/install/build-install-policy.js +44 -123
- package/dist/install/index.d.ts +0 -2
- package/dist/install/index.js +0 -7
- package/dist/ir/index.d.ts +1 -1
- package/dist/ir/types.d.ts +7 -76
- package/dist/ir/types.js +0 -2
- package/dist/predicate/decode.d.ts +1 -1
- package/dist/predicate/decode.js +2 -70
- package/dist/predicate/encode.js +45 -229
- package/dist/predicate/from-json.js +1 -42
- package/dist/record/decode.js +3 -8
- package/dist/review-card/builder.d.ts +4 -3
- package/dist/review-card/builder.js +18 -162
- package/dist/review-card/cross-check.js +2 -105
- package/dist/review-card/render-leaf.d.ts +4 -0
- package/dist/review-card/render-leaf.js +47 -0
- package/dist/run/index.d.ts +36 -139
- package/dist/run/index.js +125 -437
- package/dist/run/schemas.d.ts +168 -1929
- package/dist/run/schemas.js +40 -269
- package/dist/seams/types.d.ts +0 -2
- package/dist/simulate/deny-cases.d.ts +26 -0
- package/dist/simulate/deny-cases.js +282 -0
- package/dist/simulate/evaluate.d.ts +18 -0
- package/dist/simulate/evaluate.js +271 -0
- package/dist/simulate/index.d.ts +4 -0
- package/dist/simulate/index.js +8 -0
- package/dist/synth/compose-from-recording.d.ts +24 -51
- package/dist/synth/compose-from-recording.js +120 -282
- package/dist/synth/deny-cases.d.ts +5 -7
- package/dist/synth/deny-cases.js +23 -373
- package/dist/synth/evaluate.d.ts +0 -21
- package/dist/synth/evaluate.js +56 -336
- package/dist/synth/index.d.ts +0 -5
- package/dist/synth/index.js +0 -5
- package/dist/synth/permit-context.d.ts +2 -9
- package/dist/synth/permit-context.js +1 -82
- package/dist/synth/synthesize-from-recording.d.ts +1 -29
- package/dist/synth/synthesize-from-recording.js +41 -356
- package/dist/types.d.ts +8 -60
- package/dist/types.js +5 -2
- package/dist/verify/simulate.d.ts +1 -14
- package/dist/verify/simulate.js +2 -88
- package/dist/verify/verify.d.ts +1 -7
- package/dist/verify/verify.js +0 -8
- package/dist-cjs/adapters/interpreter/adapter.d.ts +19 -25
- package/dist-cjs/adapters/interpreter/adapter.js +56 -489
- package/dist-cjs/adapters/interpreter/index.d.ts +1 -1
- package/dist-cjs/adapters/interpreter/index.js +1 -2
- package/dist-cjs/adapters/oz/adapter.js +1 -15
- package/dist-cjs/errors.d.ts +1 -1
- package/dist-cjs/index.d.ts +1 -4
- package/dist-cjs/index.js +1 -4
- package/dist-cjs/install/build-add-context-rule.d.ts +3 -19
- package/dist-cjs/install/build-add-context-rule.js +1 -97
- package/dist-cjs/install/build-install-policy.d.ts +4 -53
- package/dist-cjs/install/build-install-policy.js +44 -122
- package/dist-cjs/install/index.d.ts +0 -2
- package/dist-cjs/install/index.js +2 -23
- package/dist-cjs/ir/index.d.ts +1 -1
- package/dist-cjs/ir/types.d.ts +7 -76
- package/dist-cjs/ir/types.js +0 -2
- package/dist-cjs/predicate/decode.d.ts +1 -1
- package/dist-cjs/predicate/decode.js +2 -70
- package/dist-cjs/predicate/encode.js +45 -229
- package/dist-cjs/predicate/from-json.js +1 -42
- package/dist-cjs/record/decode.js +3 -8
- package/dist-cjs/review-card/builder.d.ts +4 -3
- package/dist-cjs/review-card/builder.js +23 -167
- package/dist-cjs/review-card/cross-check.js +7 -110
- package/dist-cjs/review-card/render-leaf.d.ts +4 -0
- package/dist-cjs/review-card/render-leaf.js +52 -0
- package/dist-cjs/run/index.d.ts +36 -139
- package/dist-cjs/run/index.js +125 -444
- package/dist-cjs/run/schemas.d.ts +168 -1929
- package/dist-cjs/run/schemas.js +41 -270
- package/dist-cjs/seams/types.d.ts +0 -2
- package/dist-cjs/simulate/deny-cases.d.ts +26 -0
- package/dist-cjs/simulate/deny-cases.js +286 -0
- package/dist-cjs/simulate/evaluate.d.ts +18 -0
- package/dist-cjs/simulate/evaluate.js +274 -0
- package/dist-cjs/simulate/index.d.ts +4 -0
- package/dist-cjs/simulate/index.js +13 -0
- package/dist-cjs/synth/compose-from-recording.d.ts +24 -51
- package/dist-cjs/synth/compose-from-recording.js +120 -282
- package/dist-cjs/synth/deny-cases.d.ts +5 -7
- package/dist-cjs/synth/deny-cases.js +23 -374
- package/dist-cjs/synth/evaluate.d.ts +0 -21
- package/dist-cjs/synth/evaluate.js +57 -337
- package/dist-cjs/synth/index.d.ts +0 -5
- package/dist-cjs/synth/index.js +1 -11
- package/dist-cjs/synth/permit-context.d.ts +2 -9
- package/dist-cjs/synth/permit-context.js +1 -82
- package/dist-cjs/synth/synthesize-from-recording.d.ts +1 -29
- package/dist-cjs/synth/synthesize-from-recording.js +39 -354
- package/dist-cjs/types.d.ts +8 -60
- package/dist-cjs/types.js +6 -3
- package/dist-cjs/verify/simulate.d.ts +1 -14
- package/dist-cjs/verify/simulate.js +2 -88
- package/dist-cjs/verify/verify.d.ts +1 -7
- package/dist-cjs/verify/verify.js +0 -8
- package/package.json +4 -4
- package/src/adapters/interpreter/adapter.ts +76 -572
- package/src/errors.ts +0 -19
- package/src/index.ts +1 -4
- package/src/install/build-add-context-rule.ts +5 -139
- package/src/install/build-install-policy.ts +93 -214
- package/src/install/index.ts +0 -34
- package/src/predicate/decode.ts +2 -70
- package/src/predicate/encode.ts +49 -261
- package/src/predicate/from-json.ts +1 -43
- package/src/record/decode.ts +3 -8
- package/src/review-card/builder.ts +19 -168
- package/src/review-card/cross-check.ts +3 -105
- package/src/review-card/render-leaf.ts +48 -0
- package/src/run/index.ts +144 -572
- package/src/run/schemas.ts +42 -291
- package/src/simulate/deny-cases.ts +334 -0
- package/src/simulate/evaluate.ts +284 -0
- package/src/simulate/index.ts +11 -0
- package/src/synth/compose-from-recording.ts +148 -347
- package/src/synth/index.ts +0 -13
- package/src/synth/synthesize-from-recording.ts +43 -456
- package/src/types.ts +13 -49
- package/dist/install/authority-overlap.d.ts +0 -134
- package/dist/install/authority-overlap.js +0 -0
- package/dist/install/build-install-predicate.d.ts +0 -96
- package/dist/install/build-install-predicate.js +0 -444
- package/dist/install/build-merge-policy.d.ts +0 -70
- package/dist/install/build-merge-policy.js +0 -130
- package/dist/install/plan-merge-policy.d.ts +0 -49
- package/dist/install/plan-merge-policy.js +0 -86
- package/dist/install/read-account-rules.d.ts +0 -100
- package/dist/install/read-account-rules.js +0 -283
- package/dist-cjs/install/authority-overlap.d.ts +0 -134
- package/dist-cjs/install/authority-overlap.js +0 -0
- package/dist-cjs/install/build-install-predicate.d.ts +0 -96
- package/dist-cjs/install/build-install-predicate.js +0 -479
- package/dist-cjs/install/build-merge-policy.d.ts +0 -70
- package/dist-cjs/install/build-merge-policy.js +0 -134
- package/dist-cjs/install/plan-merge-policy.d.ts +0 -49
- package/dist-cjs/install/plan-merge-policy.js +0 -90
- package/dist-cjs/install/read-account-rules.d.ts +0 -100
- package/dist-cjs/install/read-account-rules.js +0 -296
- package/src/adapters/interpreter/index.ts +0 -8
- package/src/adapters/oz/adapter.ts +0 -376
- package/src/adapters/oz/index.ts +0 -9
- package/src/codegen/compile-gate.ts +0 -167
- package/src/codegen/index.ts +0 -17
- package/src/codegen/template.ts +0 -165
- package/src/install/authority-overlap.ts +0 -0
- package/src/install/build-merge-policy.ts +0 -219
- package/src/install/plan-merge-policy.ts +0 -133
- package/src/install/read-account-rules.ts +0 -376
- package/src/ir/index.ts +0 -13
- package/src/ir/types.ts +0 -132
- package/src/mandate/index.ts +0 -4
- package/src/mandate/to-ir.ts +0 -71
- package/src/mandate/types.ts +0 -21
- package/src/seams/index.ts +0 -11
- package/src/seams/types.ts +0 -81
- package/src/synth/deny-cases.ts +0 -663
- package/src/synth/evaluate.ts +0 -613
- package/src/synth/harness.ts +0 -68
- package/src/synth/minimize.ts +0 -48
- package/src/synth/permit-context.ts +0 -136
- package/src/synth/predicate-literals.ts +0 -27
- package/src/synth/synthesize-from-mandate.ts +0 -82
- package/src/verify/envelope.ts +0 -28
- package/src/verify/index.ts +0 -5
- package/src/verify/simulate.ts +0 -311
- 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
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
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`
|
|
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
|
|
217
|
-
//
|
|
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
|
|
233
|
-
//
|
|
234
|
-
//
|
|
235
|
-
|
|
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' | '
|
|
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
|
+
}
|