@crediolabs/policy-synth 0.1.3 → 0.1.5

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 (194) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +193 -5
  3. package/dist/adapters/interpreter/adapter.d.ts +38 -0
  4. package/dist/adapters/interpreter/adapter.js +522 -0
  5. package/dist/adapters/interpreter/index.d.ts +1 -0
  6. package/dist/adapters/interpreter/index.js +2 -0
  7. package/dist/adapters/oz/adapter.js +2 -0
  8. package/dist/codegen/compile-gate.d.ts +33 -0
  9. package/dist/codegen/compile-gate.js +119 -0
  10. package/dist/codegen/index.d.ts +2 -0
  11. package/dist/codegen/index.js +8 -0
  12. package/dist/codegen/template.d.ts +18 -0
  13. package/dist/codegen/template.js +131 -0
  14. package/dist/errors.d.ts +1 -1
  15. package/dist/index.d.ts +2 -0
  16. package/dist/index.js +2 -0
  17. package/dist/ir/types.d.ts +13 -2
  18. package/dist/predicate/encode.d.ts +10 -0
  19. package/dist/predicate/encode.js +249 -0
  20. package/dist/predicate/index.d.ts +1 -0
  21. package/dist/predicate/index.js +2 -0
  22. package/dist/record/decode.d.ts +6 -3
  23. package/dist/record/decode.js +13 -3
  24. package/dist/review-card/builder.d.ts +14 -0
  25. package/dist/review-card/builder.js +261 -0
  26. package/dist/review-card/conflict.d.ts +40 -0
  27. package/dist/review-card/conflict.js +111 -0
  28. package/dist/review-card/cross-check.d.ts +11 -0
  29. package/dist/review-card/cross-check.js +148 -0
  30. package/dist/review-card/index.d.ts +3 -0
  31. package/dist/review-card/index.js +4 -0
  32. package/dist/run/index.d.ts +31 -0
  33. package/dist/run/index.js +164 -0
  34. package/dist/run/schemas.d.ts +2003 -0
  35. package/dist/run/schemas.js +215 -0
  36. package/dist/synth/compose-from-recording.d.ts +35 -7
  37. package/dist/synth/compose-from-recording.js +225 -34
  38. package/dist/synth/deny-cases.d.ts +12 -0
  39. package/dist/synth/deny-cases.js +377 -0
  40. package/dist/synth/evaluate.d.ts +39 -0
  41. package/dist/synth/evaluate.js +425 -0
  42. package/dist/synth/harness.d.ts +16 -0
  43. package/dist/synth/harness.js +26 -0
  44. package/dist/synth/index.d.ts +4 -0
  45. package/dist/synth/index.js +4 -0
  46. package/dist/synth/minimize.d.ts +4 -0
  47. package/dist/synth/minimize.js +38 -0
  48. package/dist/synth/predicate-literals.d.ts +5 -0
  49. package/dist/synth/predicate-literals.js +25 -0
  50. package/dist/synth/synthesize-from-recording.d.ts +37 -5
  51. package/dist/synth/synthesize-from-recording.js +550 -18
  52. package/dist/types.d.ts +40 -1
  53. package/dist/types.js +17 -0
  54. package/dist/verify/envelope.d.ts +15 -0
  55. package/dist/verify/envelope.js +22 -0
  56. package/dist/verify/index.d.ts +3 -0
  57. package/dist/verify/index.js +3 -0
  58. package/dist/verify/simulate.d.ts +31 -0
  59. package/dist/verify/simulate.js +258 -0
  60. package/dist/verify/verify.d.ts +21 -0
  61. package/dist/verify/verify.js +189 -0
  62. package/dist-cjs/adapters/interpreter/adapter.d.ts +38 -0
  63. package/dist-cjs/adapters/interpreter/adapter.js +527 -0
  64. package/dist-cjs/adapters/interpreter/index.d.ts +1 -0
  65. package/dist-cjs/adapters/interpreter/index.js +8 -0
  66. package/dist-cjs/adapters/oz/adapter.d.ts +20 -0
  67. package/dist-cjs/adapters/oz/adapter.js +289 -0
  68. package/dist-cjs/adapters/oz/index.d.ts +1 -0
  69. package/dist-cjs/adapters/oz/index.js +8 -0
  70. package/dist-cjs/codegen/compile-gate.d.ts +33 -0
  71. package/dist-cjs/codegen/compile-gate.js +123 -0
  72. package/dist-cjs/codegen/index.d.ts +2 -0
  73. package/dist-cjs/codegen/index.js +14 -0
  74. package/dist-cjs/codegen/template.d.ts +18 -0
  75. package/dist-cjs/codegen/template.js +134 -0
  76. package/dist-cjs/errors.d.ts +37 -0
  77. package/dist-cjs/errors.js +3 -0
  78. package/dist-cjs/index.d.ts +11 -0
  79. package/dist-cjs/index.js +27 -0
  80. package/dist-cjs/ir/index.d.ts +1 -0
  81. package/dist-cjs/ir/index.js +3 -0
  82. package/dist-cjs/ir/types.d.ts +108 -0
  83. package/dist-cjs/ir/types.js +12 -0
  84. package/dist-cjs/mandate/index.d.ts +2 -0
  85. package/dist-cjs/mandate/index.js +6 -0
  86. package/dist-cjs/mandate/to-ir.d.ts +3 -0
  87. package/dist-cjs/mandate/to-ir.js +63 -0
  88. package/dist-cjs/mandate/types.d.ts +20 -0
  89. package/dist-cjs/mandate/types.js +9 -0
  90. package/dist-cjs/package.json +3 -0
  91. package/dist-cjs/predicate/encode.d.ts +10 -0
  92. package/dist-cjs/predicate/encode.js +252 -0
  93. package/dist-cjs/predicate/index.d.ts +1 -0
  94. package/dist-cjs/predicate/index.js +6 -0
  95. package/dist-cjs/record/decode.d.ts +79 -0
  96. package/dist-cjs/record/decode.js +398 -0
  97. package/dist-cjs/record/freshness.d.ts +17 -0
  98. package/dist-cjs/record/freshness.js +55 -0
  99. package/dist-cjs/record/index.d.ts +21 -0
  100. package/dist-cjs/record/index.js +166 -0
  101. package/dist-cjs/record/movements.d.ts +20 -0
  102. package/dist-cjs/record/movements.js +192 -0
  103. package/dist-cjs/record/rpc.d.ts +22 -0
  104. package/dist-cjs/record/rpc.js +74 -0
  105. package/dist-cjs/record/validate.d.ts +22 -0
  106. package/dist-cjs/record/validate.js +63 -0
  107. package/dist-cjs/registry/identify.d.ts +11 -0
  108. package/dist-cjs/registry/identify.js +87 -0
  109. package/dist-cjs/registry/index.d.ts +3 -0
  110. package/dist-cjs/registry/index.js +15 -0
  111. package/dist-cjs/registry/known-addresses.d.ts +16 -0
  112. package/dist-cjs/registry/known-addresses.js +53 -0
  113. package/dist-cjs/registry/protocols.d.ts +38 -0
  114. package/dist-cjs/registry/protocols.js +153 -0
  115. package/dist-cjs/review-card/builder.d.ts +14 -0
  116. package/dist-cjs/review-card/builder.js +264 -0
  117. package/dist-cjs/review-card/conflict.d.ts +40 -0
  118. package/dist-cjs/review-card/conflict.js +114 -0
  119. package/dist-cjs/review-card/cross-check.d.ts +11 -0
  120. package/dist-cjs/review-card/cross-check.js +151 -0
  121. package/dist-cjs/review-card/index.d.ts +3 -0
  122. package/dist-cjs/review-card/index.js +10 -0
  123. package/dist-cjs/run/index.d.ts +31 -0
  124. package/dist-cjs/run/index.js +178 -0
  125. package/dist-cjs/run/schemas.d.ts +2003 -0
  126. package/dist-cjs/run/schemas.js +218 -0
  127. package/dist-cjs/seams/index.d.ts +1 -0
  128. package/dist-cjs/seams/index.js +3 -0
  129. package/dist-cjs/seams/types.d.ts +66 -0
  130. package/dist-cjs/seams/types.js +12 -0
  131. package/dist-cjs/synth/compose-from-recording.d.ts +64 -0
  132. package/dist-cjs/synth/compose-from-recording.js +356 -0
  133. package/dist-cjs/synth/deny-cases.d.ts +12 -0
  134. package/dist-cjs/synth/deny-cases.js +380 -0
  135. package/dist-cjs/synth/evaluate.d.ts +39 -0
  136. package/dist-cjs/synth/evaluate.js +428 -0
  137. package/dist-cjs/synth/harness.d.ts +16 -0
  138. package/dist-cjs/synth/harness.js +29 -0
  139. package/dist-cjs/synth/index.d.ts +9 -0
  140. package/dist-cjs/synth/index.js +23 -0
  141. package/dist-cjs/synth/lower.d.ts +23 -0
  142. package/dist-cjs/synth/lower.js +119 -0
  143. package/dist-cjs/synth/minimize.d.ts +4 -0
  144. package/dist-cjs/synth/minimize.js +41 -0
  145. package/dist-cjs/synth/predicate-literals.d.ts +5 -0
  146. package/dist-cjs/synth/predicate-literals.js +28 -0
  147. package/dist-cjs/synth/scope.d.ts +26 -0
  148. package/dist-cjs/synth/scope.js +81 -0
  149. package/dist-cjs/synth/synthesize-from-mandate.d.ts +5 -0
  150. package/dist-cjs/synth/synthesize-from-mandate.js +37 -0
  151. package/dist-cjs/synth/synthesize-from-recording.d.ts +49 -0
  152. package/dist-cjs/synth/synthesize-from-recording.js +711 -0
  153. package/dist-cjs/types.d.ts +288 -0
  154. package/dist-cjs/types.js +55 -0
  155. package/dist-cjs/verify/envelope.d.ts +15 -0
  156. package/dist-cjs/verify/envelope.js +23 -0
  157. package/dist-cjs/verify/index.d.ts +3 -0
  158. package/dist-cjs/verify/index.js +8 -0
  159. package/dist-cjs/verify/simulate.d.ts +31 -0
  160. package/dist-cjs/verify/simulate.js +261 -0
  161. package/dist-cjs/verify/verify.d.ts +21 -0
  162. package/dist-cjs/verify/verify.js +192 -0
  163. package/package.json +43 -3
  164. package/src/adapters/interpreter/adapter.ts +642 -0
  165. package/src/adapters/interpreter/index.ts +8 -0
  166. package/src/adapters/oz/adapter.ts +2 -0
  167. package/src/codegen/compile-gate.ts +162 -0
  168. package/src/codegen/index.ts +17 -0
  169. package/src/codegen/template.ts +148 -0
  170. package/src/errors.ts +2 -0
  171. package/src/index.ts +2 -0
  172. package/src/ir/types.ts +9 -2
  173. package/src/predicate/encode.ts +307 -0
  174. package/src/predicate/index.ts +3 -0
  175. package/src/record/decode.ts +21 -10
  176. package/src/review-card/builder.ts +303 -0
  177. package/src/review-card/conflict.ts +143 -0
  178. package/src/review-card/cross-check.ts +158 -0
  179. package/src/review-card/index.ts +12 -0
  180. package/src/run/index.ts +213 -0
  181. package/src/run/schemas.ts +242 -0
  182. package/src/synth/compose-from-recording.ts +277 -43
  183. package/src/synth/deny-cases.ts +466 -0
  184. package/src/synth/evaluate.ts +493 -0
  185. package/src/synth/harness.ts +40 -0
  186. package/src/synth/index.ts +12 -0
  187. package/src/synth/minimize.ts +44 -0
  188. package/src/synth/predicate-literals.ts +27 -0
  189. package/src/synth/synthesize-from-recording.ts +655 -19
  190. package/src/types.ts +38 -2
  191. package/src/verify/envelope.ts +28 -0
  192. package/src/verify/index.ts +5 -0
  193. package/src/verify/simulate.ts +311 -0
  194. package/src/verify/verify.ts +243 -0
@@ -0,0 +1,153 @@
1
+ "use strict";
2
+ // src/registry/protocols.ts - protocol ABI definitions (no addresses).
3
+ //
4
+ // A protocol is identified by EITHER:
5
+ // - by interface (function-name signature + arg shape): a contract calling
6
+ // one of these well-known fns with the expected arg shape IS that
7
+ // protocol, regardless of address. SEP-41 is the canonical case - ANY
8
+ // token contract calling `transfer` with the right arg shape is a
9
+ // SEP-41 token. Blend pool `submit`/`claim` are similarly recognised by
10
+ // interface (the pool factory address doesn't cover per-pool instances).
11
+ // - by address: the contract is in the pinned `known-addresses` set (see
12
+ // known-addresses.ts). SoroSwap uses address recognition because the
13
+ // router is a single deployed contract; Blend uses it to recognise factory
14
+ // calls in addition to its interface recognition.
15
+ //
16
+ // Args entries pin a ScVal subset type (matching the normalised `ScVal`
17
+ // vocabulary in src/types.ts) plus a human meaning string. The registry
18
+ // fails closed: a method call whose decoded args do NOT match the ABI
19
+ // signature is treated as unknown.
20
+ Object.defineProperty(exports, "__esModule", { value: true });
21
+ exports.PROTOCOL_ABIS = exports.SOROSWAP_ABI = exports.BLEND_ABI = exports.SEP41_ABI = void 0;
22
+ exports.getAbi = getAbi;
23
+ /** SEP-41 token interface - pinned from the SEP-41 spec (any token). */
24
+ exports.SEP41_ABI = {
25
+ transfer: {
26
+ args: [
27
+ {
28
+ name: 'from',
29
+ type: 'address',
30
+ meaning: 'source address (the `from` of the token transfer)',
31
+ },
32
+ {
33
+ name: 'to',
34
+ type: 'address',
35
+ meaning: 'destination address (the `to` of the token transfer)',
36
+ },
37
+ {
38
+ name: 'amount',
39
+ type: 'i128',
40
+ meaning: 'amount of tokens to move (i128, signed-decimal string)',
41
+ },
42
+ ],
43
+ },
44
+ mint: {
45
+ args: [
46
+ { name: 'to', type: 'address', meaning: 'recipient of the newly minted tokens' },
47
+ { name: 'amount', type: 'i128', meaning: 'amount to mint (i128, signed-decimal string)' },
48
+ ],
49
+ },
50
+ burn: {
51
+ args: [
52
+ { name: 'from', type: 'address', meaning: 'address whose balance is decremented' },
53
+ { name: 'amount', type: 'i128', meaning: 'amount to burn (i128, signed-decimal string)' },
54
+ ],
55
+ },
56
+ approve: {
57
+ args: [
58
+ { name: 'from', type: 'address', meaning: 'token holder granting the allowance' },
59
+ {
60
+ name: 'spender',
61
+ type: 'address',
62
+ meaning: 'address authorised to move tokens on behalf of `from`',
63
+ },
64
+ { name: 'amount', type: 'i128', meaning: 'allowance amount (i128, signed-decimal string)' },
65
+ {
66
+ name: 'expiration_ledger',
67
+ type: 'u32',
68
+ meaning: 'ledger sequence after which the allowance is invalid',
69
+ },
70
+ ],
71
+ },
72
+ };
73
+ /** Blend pool interface (v2). Pinning the two fns the recorder surfaces.
74
+ * Pool-level recognition is by INTERFACE (real pool calls hit per-pool
75
+ * instances, not the factory). Arg shapes verified against the Blend v2
76
+ * pool contract source (blend-capital/blend-contracts-v2 `pool/src/contract.rs`,
77
+ * fetched 2026-07-23):
78
+ * submit(env, from: Address, spender: Address, to: Address, requests: Vec<Request>)
79
+ * -> Positions
80
+ * claim(env, from: Address, reserve_token_ids: Vec<u32>, to: Address) -> i128
81
+ * Other pool fns (add_reserves, withdraw, etc.) are intentionally omitted -
82
+ * they are too version-sensitive to bind by interface. */
83
+ exports.BLEND_ABI = {
84
+ submit: {
85
+ args: [
86
+ { name: 'from', type: 'address', meaning: 'address supplying collateral or repaying debt' },
87
+ { name: 'spender', type: 'address', meaning: 'address authorising the supply / repay' },
88
+ { name: 'to', type: 'address', meaning: 'address receiving the resulting position shares' },
89
+ { name: 'requests', type: 'vec', meaning: 'vec<Request> describing the action per reserve' },
90
+ ],
91
+ },
92
+ claim: {
93
+ args: [
94
+ { name: 'from', type: 'address', meaning: 'address whose position is being claimed against' },
95
+ {
96
+ name: 'reserve_token_ids',
97
+ type: 'vec',
98
+ meaning: 'vec<u32> reserve token ids to claim emissions for',
99
+ },
100
+ { name: 'to', type: 'address', meaning: 'address receiving the claimed tokens' },
101
+ ],
102
+ },
103
+ };
104
+ /** SoroSwap router interface. Standard Uniswap-V2-style swap fn signatures
105
+ * pinned from the SoroSwap router source. Recognised by address only (the
106
+ * router is a single pinned contract); FIX 4 requires the method to also be
107
+ * in this ABI for address recognition to succeed. The `path` arg is a
108
+ * `vec<address>` - we record it as the outer `vec` subset and leave the
109
+ * per-element shape to the downstream reviewer. */
110
+ exports.SOROSWAP_ABI = {
111
+ swap_exact_tokens_for_tokens: {
112
+ args: [
113
+ { name: 'amount_in', type: 'i128', meaning: 'exact input amount (i128)' },
114
+ { name: 'amount_out_min', type: 'i128', meaning: 'minimum acceptable output (i128)' },
115
+ { name: 'path', type: 'vec', meaning: 'vec<address> hop path through pools' },
116
+ { name: 'to', type: 'address', meaning: 'recipient of the output tokens' },
117
+ {
118
+ name: 'deadline',
119
+ type: 'u64',
120
+ meaning: 'unix-seconds deadline after which the swap is refused',
121
+ },
122
+ ],
123
+ },
124
+ swap_tokens_for_exact_tokens: {
125
+ args: [
126
+ { name: 'amount_out', type: 'i128', meaning: 'exact output amount desired (i128)' },
127
+ { name: 'amount_in_max', type: 'i128', meaning: 'maximum input willing to spend (i128)' },
128
+ { name: 'path', type: 'vec', meaning: 'vec<address> hop path through pools' },
129
+ { name: 'to', type: 'address', meaning: 'recipient of the output tokens' },
130
+ {
131
+ name: 'deadline',
132
+ type: 'u64',
133
+ meaning: 'unix-seconds deadline after which the swap is refused',
134
+ },
135
+ ],
136
+ },
137
+ swap_exact_in_for_tokens: {
138
+ args: [
139
+ { name: 'amount_in', type: 'i128', meaning: 'exact input amount (i128)' },
140
+ { name: 'amount_out_min', type: 'i128', meaning: 'minimum acceptable output (i128)' },
141
+ { name: 'path', type: 'vec', meaning: 'vec<address> hop path through pools' },
142
+ { name: 'to', type: 'address', meaning: 'recipient of the output tokens' },
143
+ ],
144
+ },
145
+ };
146
+ exports.PROTOCOL_ABIS = {
147
+ sep41: exports.SEP41_ABI,
148
+ blend: exports.BLEND_ABI,
149
+ soroswap: exports.SOROSWAP_ABI,
150
+ };
151
+ function getAbi(protocol) {
152
+ return exports.PROTOCOL_ABIS[protocol];
153
+ }
@@ -0,0 +1,14 @@
1
+ import type { ContextRuleDraft, PolicyRef, PredicateNode } from '../types.ts';
2
+ import type { SimulationResult } from '../verify/envelope.ts';
3
+ export interface ReviewCardSummary {
4
+ ruleName: string;
5
+ plainEnglish: string;
6
+ constraints: string[];
7
+ expiry: string;
8
+ backend: 'interpreter-v1' | 'ts-model';
9
+ /** Stable hash of the builder inputs - identical policy + summary = identical hash. */
10
+ contentHash: string;
11
+ }
12
+ /** Build a deterministic review-card summary from a policy + context rule +
13
+ * simulation result. Pure: same inputs -> byte-identical output. */
14
+ export declare function buildReviewCardSummary(predicate: PredicateNode | null, policyRefs: PolicyRef[], contextRule: ContextRuleDraft, simulation: SimulationResult): ReviewCardSummary;
@@ -0,0 +1,264 @@
1
+ "use strict";
2
+ // src/review-card/builder.ts - deterministic, pure review-card summary
3
+ // builder.
4
+ //
5
+ // `buildReviewCardSummary` renders the per-policy text the user-facing review
6
+ // card quotes. The whole DX win of non-engineer reviewability hinges on the
7
+ // summary being REPRODUCIBLE + TESTABLE + NON-HALLUCINABLE, so this module is
8
+ // pure: same inputs -> byte-identical output, no clock, no randomness, no I/O.
9
+ //
10
+ // The builder walks two inputs and emits ONE constraint string per leaf or
11
+ // primitive, in a fixed deterministic order:
12
+ //
13
+ // 1. The OZ built-in `PolicyRef`s. Each `spending_limit` primitive becomes a
14
+ // `spending_limit(token, limitAmount, windowSecs)` line; other OZ
15
+ // primitives (threshold) are skipped - the review card does not quote
16
+ // them (they are signer-config concerns, not transactional bounds).
17
+ //
18
+ // 2. The interpreter `PredicateNode`. One string per constraint leaf,
19
+ // rendered by enclosing-comparison kind. Templates (Task 7b):
20
+ // - invocation_count_in_window <= N -> Invocations <= N per <window> seconds
21
+ // - call_arg[i] in [list] -> Recipient/arg must be one of [list]
22
+ // - eq(call_arg[i], literal_vec[...]) -> Path must be exactly [list]
23
+ // - oracle_price(asset) OP price -> Only when oracle_price(asset) OP price
24
+ // - call_fn == x -> Function must be x
25
+ // - call_contract == c -> Contract must be c
26
+ // - amount <= v -> Amount <= v
27
+ //
28
+ // The content hash is a stable sha256 hex of a canonical JSON of
29
+ // { ruleName, plainEnglish, constraints, expiry, backend } - identical
30
+ // inputs (incl. the context-rule expiry and simulation backend) -> identical
31
+ // hash. There is no clock; the hash never includes a timestamp.
32
+ Object.defineProperty(exports, "__esModule", { value: true });
33
+ exports.buildReviewCardSummary = buildReviewCardSummary;
34
+ const node_crypto_1 = require("node:crypto");
35
+ /** Build a deterministic review-card summary from a policy + context rule +
36
+ * simulation result. Pure: same inputs -> byte-identical output. */
37
+ function buildReviewCardSummary(predicate, policyRefs, contextRule, simulation) {
38
+ const constraints = [];
39
+ for (const ref of policyRefs) {
40
+ const line = renderOzPrimitive(ref);
41
+ if (line !== null)
42
+ constraints.push(line);
43
+ }
44
+ if (predicate !== null) {
45
+ walkPredicate(predicate, (node) => {
46
+ const line = renderConstraint(node);
47
+ if (line !== null)
48
+ constraints.push(line);
49
+ });
50
+ }
51
+ const ruleName = contextRule.name;
52
+ const plainEnglish = renderPlainEnglish(ruleName, constraints);
53
+ const expiry = renderExpiry(contextRule.validUntilLedger);
54
+ const backend = simulation.backend;
55
+ const contentHash = computeContentHash({
56
+ ruleName,
57
+ plainEnglish,
58
+ constraints,
59
+ expiry,
60
+ backend,
61
+ });
62
+ return { ruleName, plainEnglish, constraints, expiry, backend, contentHash };
63
+ }
64
+ /** Render the OZ built-in primitive summary line. Only `spending_limit` is
65
+ * quoted by the review card (it is the only primitive that defines a
66
+ * transactional bound). Other primitives (threshold) are signer-config
67
+ * concerns handled by the OZ adapter's own `uncovered` machinery.
68
+ * Spending_limit takes `period_ledgers` on-chain (~5s/ledger); the card
69
+ * states the window in seconds so the user reads it consistently with the
70
+ * interpreter templates. */
71
+ function renderOzPrimitive(ref) {
72
+ if (ref.kind !== 'oz_builtin')
73
+ return null;
74
+ const primitive = ref.primitive;
75
+ if (primitive.primitive !== 'spending_limit')
76
+ return null;
77
+ const params = primitive.params;
78
+ const limit = params.spending_limit ?? '0';
79
+ const periodLedgers = params.period_ledgers ?? 0;
80
+ const windowSecs = periodLedgers * 5;
81
+ return `spending_limit(${limit}, ${windowSecs})`;
82
+ }
83
+ /** Walk every comparison / membership node of the predicate and invoke
84
+ * `visit` on each. The walk is depth-first, left-to-right, so the
85
+ * constraint list is stable across runs. Pure boolean nodes contribute no
86
+ * constraint lines themselves; their leaf children do, via the visitor. */
87
+ function walkPredicate(node, visit) {
88
+ switch (node.op) {
89
+ case 'and':
90
+ case 'or':
91
+ for (const child of node.children)
92
+ walkPredicate(child, visit);
93
+ return;
94
+ case 'not':
95
+ walkPredicate(node.child, visit);
96
+ return;
97
+ case 'in':
98
+ visit(node);
99
+ return;
100
+ case 'eq':
101
+ case 'lt':
102
+ case 'lte':
103
+ case 'gt':
104
+ case 'gte':
105
+ visit(node);
106
+ return;
107
+ }
108
+ }
109
+ /** Render ONE constraint sentence for ONE interpreter predicate node. The
110
+ * shape of the output is pinned by Task 7b so the test suite can assert
111
+ * byte-for-byte equality. Returns `null` when the node is a structural
112
+ * boolean (`and` / `or` / `not`) - those are not constraint leaves. */
113
+ function renderConstraint(node) {
114
+ switch (node.op) {
115
+ case 'and':
116
+ case 'or':
117
+ case 'not':
118
+ return null;
119
+ case 'eq':
120
+ case 'lt':
121
+ case 'lte':
122
+ case 'gt':
123
+ case 'gte':
124
+ return renderComparison(node);
125
+ case 'in':
126
+ return renderMembership(node);
127
+ }
128
+ }
129
+ function renderComparison(node) {
130
+ const left = node.left;
131
+ const right = node.right;
132
+ // eq(call_contract, literal_address) -> Contract must be <addr>
133
+ if (left.kind === 'call_contract' && node.op === 'eq' && right.kind === 'literal_address') {
134
+ return `Contract must be ${right.value}`;
135
+ }
136
+ // eq(call_fn, literal_symbol) -> Function must be <sym>
137
+ if (left.kind === 'call_fn' && node.op === 'eq' && right.kind === 'literal_symbol') {
138
+ return `Function must be ${right.value}`;
139
+ }
140
+ // eq(call_arg[i], literal_vec) -> Path must be exactly [list]
141
+ if (left.kind === 'call_arg' && node.op === 'eq' && right.kind === 'literal_vec') {
142
+ return `Path must be exactly [${right.elements.map(renderVecElement).join(', ')}]`;
143
+ }
144
+ // invocation_count_in_window <= N -> Invocations <= N per <window> seconds
145
+ if (left.kind === 'invocation_count_in_window' && right.kind === 'literal_u32') {
146
+ return `Invocations <= ${right.value} per ${left.windowSecs} seconds`;
147
+ }
148
+ // amount <= v -> Amount <= v
149
+ if (left.kind === 'amount' && right.kind === 'literal_i128') {
150
+ return `Amount <= ${right.value}`;
151
+ }
152
+ // oracle_price(asset) OP price -> Only when oracle_price(asset) OP price
153
+ if (left.kind === 'oracle_price' && right.kind === 'literal_i128') {
154
+ return `Only when oracle_price(${left.asset}) ${comparisonOpText(node.op)} ${right.value}`;
155
+ }
156
+ // Any other comparison shape is a structural fail-closed: do not surface
157
+ // a misleading line. Cross-check still requires every leaf produce a
158
+ // constraint string; the only leaves we emit lines for are the ones we
159
+ // recognise above, so the test fixtures cover exactly the supported
160
+ // shapes.
161
+ return null;
162
+ }
163
+ function renderMembership(node) {
164
+ // call_arg[i] in [list] -> Recipient/arg must be one of [list]
165
+ if (node.needle.kind === 'call_arg') {
166
+ const list = node.haystack.map(renderHaystackElement).join(', ');
167
+ return `Recipient/arg must be one of [${list}]`;
168
+ }
169
+ return null;
170
+ }
171
+ function renderVecElement(leaf) {
172
+ switch (leaf.kind) {
173
+ case 'literal_address':
174
+ return leaf.value;
175
+ case 'literal_i128':
176
+ return leaf.value;
177
+ case 'literal_symbol':
178
+ return leaf.value;
179
+ case 'literal_u32':
180
+ return String(leaf.value);
181
+ case 'literal_u64':
182
+ return leaf.value;
183
+ case 'literal_bytes':
184
+ return leaf.value;
185
+ case 'literal_vec':
186
+ return `[${leaf.elements.map(renderVecElement).join(', ')}]`;
187
+ case 'call_contract':
188
+ case 'call_fn':
189
+ case 'call_arg':
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
+ }
198
+ }
199
+ function renderHaystackElement(leaf) {
200
+ if (leaf.kind === 'literal_address')
201
+ return leaf.value;
202
+ if (leaf.kind === 'literal_i128')
203
+ return leaf.value;
204
+ if (leaf.kind === 'literal_symbol')
205
+ return leaf.value;
206
+ if (leaf.kind === 'literal_u32')
207
+ return String(leaf.value);
208
+ if (leaf.kind === 'literal_u64')
209
+ return leaf.value;
210
+ if (leaf.kind === 'literal_bytes')
211
+ return leaf.value;
212
+ if (leaf.kind === 'literal_vec') {
213
+ return `[${leaf.elements.map(renderHaystackElement).join(', ')}]`;
214
+ }
215
+ return `<${leaf.kind}>`;
216
+ }
217
+ function comparisonOpText(op) {
218
+ switch (op) {
219
+ case 'lt':
220
+ return '<';
221
+ case 'lte':
222
+ return '<=';
223
+ case 'gt':
224
+ return '>';
225
+ case 'gte':
226
+ return '>=';
227
+ case 'eq':
228
+ return '==';
229
+ }
230
+ }
231
+ /** Render the plain-English one-liner. Format: `<ruleName>: <constraints>`,
232
+ * joined by `; ` so the user reads one sentence per constraint. */
233
+ function renderPlainEnglish(ruleName, constraints) {
234
+ if (constraints.length === 0)
235
+ return `${ruleName}: (no constraints)`;
236
+ return `${ruleName}: ${constraints.join('; ')}`;
237
+ }
238
+ /** Render the expiry line. `null` -> "No expiry"; a ledger sequence -> the
239
+ * ledger number so the user reads it in the same units the OZ context rule
240
+ * applies it. */
241
+ function renderExpiry(validUntilLedger) {
242
+ if (validUntilLedger === null)
243
+ return 'No expiry';
244
+ return `Valid until ledger ${validUntilLedger}`;
245
+ }
246
+ function computeContentHash(input) {
247
+ return (0, node_crypto_1.createHash)('sha256').update(canonicalStringify(input)).digest('hex');
248
+ }
249
+ /** Canonical JSON with recursively sorted object keys (stable across runs). */
250
+ function canonicalStringify(value) {
251
+ return JSON.stringify(sortKeys(value));
252
+ }
253
+ function sortKeys(value) {
254
+ if (Array.isArray(value))
255
+ return value.map(sortKeys);
256
+ if (value && typeof value === 'object') {
257
+ const out = {};
258
+ for (const key of Object.keys(value).sort()) {
259
+ out[key] = sortKeys(value[key]);
260
+ }
261
+ return out;
262
+ }
263
+ return value;
264
+ }
@@ -0,0 +1,40 @@
1
+ /** Minimal rule reference the classifier needs to compute the conflict kind.
2
+ * Carries the rule id (used in the annotation), the scope (contract +
3
+ * method), and an optional spending cap. The cap captures both the
4
+ * amount and the window so the classifier can compare apples to apples. */
5
+ export interface RuleRef {
6
+ id: string;
7
+ scope: {
8
+ contract?: string;
9
+ method?: string;
10
+ };
11
+ /** When set, the rule enforces this per-window spend cap (token +
12
+ * decimal-string amount + seconds window). */
13
+ spendingLimit?: {
14
+ token: string;
15
+ amount: string;
16
+ windowSeconds: number;
17
+ };
18
+ }
19
+ /** Structured annotation a single new-vs-existing rule comparison yields.
20
+ * Four mutually exclusive kinds; the union is exhaustive (the classifier
21
+ * never throws). */
22
+ export type ConflictAnnotation = {
23
+ kind: 'subsume';
24
+ existingRuleId: string;
25
+ detail: string;
26
+ } | {
27
+ kind: 'disjoint';
28
+ } | {
29
+ kind: 'counter_permissive';
30
+ existingRuleId: string;
31
+ newLimit: string;
32
+ existingLimit: string;
33
+ } | {
34
+ kind: 'window_divergent';
35
+ existingRuleId: string;
36
+ effectiveUnion: string;
37
+ };
38
+ /** Classify the relationship between a new rule and one existing rule.
39
+ * Pure: same pair -> same annotation. */
40
+ export declare function classifyConflict(newRule: RuleRef, existingRule: RuleRef): ConflictAnnotation;
@@ -0,0 +1,114 @@
1
+ "use strict";
2
+ // src/review-card/conflict.ts - the ConflictAnnotation 4-kind enum + the
3
+ // `classifyConflict` classifier.
4
+ //
5
+ // When a user installs a new policy alongside an existing rule set, the
6
+ // review card surfaces the relationship between the new rule and each
7
+ // existing rule as a structured annotation. A boolean (conflict / no
8
+ // conflict) is wrong: four qualitatively different shapes are possible, and
9
+ // each one tells the user something different about what would happen if
10
+ // both rules installed together.
11
+ //
12
+ // - subsume new rule's scope is a SUPERSET of an existing
13
+ // rule's. The new rule is broader; installing it on
14
+ // top of the existing rule weakens the existing
15
+ // restriction. The existing rule becomes a no-op.
16
+ // - disjoint new rule and existing rule are unrelated: their
17
+ // scopes do not overlap. Safe to install both.
18
+ // - counter_permissive new rule and existing rule allow CONFLICTING
19
+ // things at the same scope (one caps lower than the
20
+ // other on the same axis). The stricter one wins
21
+ // at evaluate; the looser is dead weight.
22
+ // - window_divergent new rule and existing rule have the same scope
23
+ // and the same per-window limit but DIFFERENT
24
+ // rolling windows. Effective union = sum (the user
25
+ // can spend `limit` per each window, independently).
26
+ //
27
+ // The classifier consumes a minimal `RuleRef` shape: the rule id, scope
28
+ // (contract + method), and an optional spending limit + window. Shapes
29
+ // without a limit fall back to scope-only classification.
30
+ Object.defineProperty(exports, "__esModule", { value: true });
31
+ exports.classifyConflict = classifyConflict;
32
+ /** Classify the relationship between a new rule and one existing rule.
33
+ * Pure: same pair -> same annotation. */
34
+ function classifyConflict(newRule, existingRule) {
35
+ // --- step 1: subsume? (new.scope ⊋ existing.scope) ---
36
+ //
37
+ // Subsume requires existing to be fully scoped (both contract AND method
38
+ // pinned) AND new to be at least partially broader than existing. A new
39
+ // rule with identical scope to existing is NOT a superset - it is the
40
+ // same rule and falls through to the same-scope branch below. If existing
41
+ // is "any contract" / "any method" then it is the wildest rule in the
42
+ // rule set; a new rule covering it is just a duplicate, not a subsume -
43
+ // fall through to disjoint.
44
+ const existingHasContract = existingRule.scope.contract !== undefined;
45
+ const existingHasMethod = existingRule.scope.method !== undefined;
46
+ const newIsBroaderOnContract = newRule.scope.contract === undefined && existingHasContract;
47
+ const newIsBroaderOnMethod = newRule.scope.method === undefined && existingHasMethod;
48
+ const newMatchesOnContract = newRule.scope.contract === undefined ||
49
+ (existingHasContract && newRule.scope.contract === existingRule.scope.contract);
50
+ const newMatchesOnMethod = newRule.scope.method === undefined ||
51
+ (existingHasMethod && newRule.scope.method === existingRule.scope.method);
52
+ if (existingHasContract &&
53
+ existingHasMethod &&
54
+ newMatchesOnContract &&
55
+ newMatchesOnMethod &&
56
+ (newIsBroaderOnContract || newIsBroaderOnMethod)) {
57
+ return {
58
+ kind: 'subsume',
59
+ existingRuleId: existingRule.id,
60
+ detail: `new rule scope (${describeScope(newRule)}) is a superset of existing rule (${describeScope(existingRule)})`,
61
+ };
62
+ }
63
+ // --- step 2: disjoint by scope? ---
64
+ if (existingHasContract && newRule.scope.contract !== undefined) {
65
+ if (newRule.scope.contract !== existingRule.scope.contract) {
66
+ return { kind: 'disjoint' };
67
+ }
68
+ }
69
+ if (existingHasMethod &&
70
+ newRule.scope.method !== undefined &&
71
+ newRule.scope.method !== existingRule.scope.method) {
72
+ return { kind: 'disjoint' };
73
+ }
74
+ // --- step 3: same scope -> compare spend caps (when both sides carry them) ---
75
+ if (newRule.spendingLimit && existingRule.spendingLimit) {
76
+ const a = newRule.spendingLimit;
77
+ const b = existingRule.spendingLimit;
78
+ if (a.token !== b.token) {
79
+ // Same scope, different tokens: each binds a different axis. They
80
+ // do not counter each other; treat as disjoint.
81
+ return { kind: 'disjoint' };
82
+ }
83
+ if (a.windowSeconds === b.windowSeconds) {
84
+ // Same scope, same token, same window: counter-permissive when the
85
+ // amounts disagree (the stricter wins at evaluate; the looser is
86
+ // dead weight). Equal amounts -> duplicate rule, treat as disjoint.
87
+ if (a.amount === b.amount) {
88
+ return { kind: 'disjoint' };
89
+ }
90
+ return {
91
+ kind: 'counter_permissive',
92
+ existingRuleId: existingRule.id,
93
+ newLimit: `${a.amount} ${a.token} / ${a.windowSeconds}s`,
94
+ existingLimit: `${b.amount} ${b.token} / ${b.windowSeconds}s`,
95
+ };
96
+ }
97
+ // Same scope + token, different windows: effective union is the sum
98
+ // of the two per-window caps (each window ticks independently).
99
+ const union = (BigInt(a.amount) + BigInt(b.amount)).toString();
100
+ return {
101
+ kind: 'window_divergent',
102
+ existingRuleId: existingRule.id,
103
+ effectiveUnion: `${union} ${a.token} per ${a.windowSeconds}s (new) + ${b.windowSeconds}s (existing)`,
104
+ };
105
+ }
106
+ // Same scope without comparable spend caps on both sides -> disjoint.
107
+ return { kind: 'disjoint' };
108
+ }
109
+ function describeScope(rule) {
110
+ const parts = [];
111
+ parts.push(rule.scope.contract ? `contract=${rule.scope.contract}` : 'contract=*');
112
+ parts.push(rule.scope.method ? `method=${rule.scope.method}` : 'method=*');
113
+ return parts.join(', ');
114
+ }
@@ -0,0 +1,11 @@
1
+ import type { PredicateNode } from '../types.ts';
2
+ import type { ReviewCardSummary } from './builder.ts';
3
+ /** Assert every leaf in the predicate appears as a constraint string in
4
+ * `summary.constraints`. Returns the missing templates (without the leaf
5
+ * values filled in) when any leaf was dropped. */
6
+ export declare function summaryCrossCheck(predicate: PredicateNode | null, summary: ReviewCardSummary): {
7
+ ok: true;
8
+ } | {
9
+ ok: false;
10
+ missingConstraints: string[];
11
+ };