@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
@@ -1,46 +1,37 @@
1
1
  // src/synth/synthesize-from-recording.ts - recording-path orchestrator.
2
2
  //
3
3
  // `synthesizeFromRecording` INFERS a bounded policy from a `RecordedTransaction`
4
- // via the same `PolicyIR` + adapter pair used by the deterministic Mandate path.
4
+ // via the `PolicyIR` + interpreter adapter pair.
5
5
  // Flow: validate -> parseConfidence gate -> lower -> decideScope -> composeFromRecording
6
- // -> OZ compile -> (opt-in) interpreter compile + self-verify + minimise -> merge.
7
- // Same (tx, opts, ozConfig) -> byte-identical ProposedPolicy (no randomness, clock, globals).
6
+ // -> interpreter compile + self-verify.
7
+ // Same (tx, opts) -> byte-identical ProposedPolicy (no randomness, clock, globals).
8
8
 
9
9
  import type { InterpreterAdapterConfig } from '../adapters/interpreter/adapter.ts'
10
10
  import {
11
- createInterpreterAdapter,
11
+ compileInterpreterPolicy,
12
12
  lowerRuleToPredicate,
13
13
  PLACEHOLDER_INTERPRETER_ADDRESS,
14
14
  } from '../adapters/interpreter/adapter.ts'
15
- import type { OzAdapterConfig } from '../adapters/oz/adapter.ts'
16
- import { createOzAdapter } from '../adapters/oz/adapter.ts'
17
15
  import type { ToolError, ToolResponse } from '../errors.ts'
18
16
  import { encodePredicate } from '../predicate/encode.ts'
19
17
  import {
20
- type ContractInvocation,
21
- MAX_SCVAL_CLONE_DEPTH,
18
+ GRAMMAR_VERSION,
22
19
  type Network,
23
20
  OZ_LIMITS,
24
- type PredicateLeaf,
25
21
  type PredicateNode,
26
22
  type ProposedPolicy,
27
23
  type RecordedTransaction,
28
24
  SOROBAN_LIMITS,
29
25
  } from '../types.ts'
30
- import type { SimulationResult } from '../verify/envelope.ts'
31
26
  import {
32
27
  type ComposeOptions,
33
28
  type ComposeUserResponses,
34
29
  composeFromRecording,
35
30
  } from './compose-from-recording.ts'
36
- import { generateCases, ORIGINAL_DIMENSIONS } from './deny-cases.ts'
37
- import { type EvalContext, evaluate } from './evaluate.ts'
38
- import { runHarness } from './harness.ts'
39
31
  import { lower } from './lower.ts'
40
- import { minimize } from './minimize.ts'
41
32
  import { decideScope, type ScopeDecision } from './scope.ts'
42
33
 
43
- const UNCOVERED_PREFIX = 'Not covered by OZ built-in primitives: '
34
+ const UNCOVERED_PREFIX = 'Not expressible as a predicate constraint: '
44
35
 
45
36
  /** Per-policy interpreter adapter config the caller opts into. Absent ->
46
37
  * recording path runs in week-1 mode (OZ adapter only, warnings for the
@@ -52,10 +43,6 @@ export interface InterpreterAdapterOptions {
52
43
  smartAccountAddress: string
53
44
  /** Per-rule install nonce (first install = 1). Defaults to 1. */
54
45
  installNonce?: number
55
- /** Per-policy oracle overrides. Tighten-only vs the wasm defaults
56
- * (maxStalenessSeconds <= 600, maxDeviationBps <= 200); widening is
57
- * rejected at the options boundary. */
58
- oracleParams?: { maxStalenessSeconds?: number; maxDeviationBps?: number }
59
46
  }
60
47
 
61
48
  /** PRIVATE test-only extension of `InterpreterAdapterOptions` for the
@@ -82,8 +69,7 @@ export interface SynthesizeFromRecordingOptions {
82
69
 
83
70
  export function synthesizeFromRecording(
84
71
  tx: RecordedTransaction,
85
- opts: SynthesizeFromRecordingOptions,
86
- ozConfig: OzAdapterConfig
72
+ opts: SynthesizeFromRecordingOptions
87
73
  ): ToolResponse<ProposedPolicy> & {
88
74
  /** Present iff `opts.explain === true` and the synthesis succeeded. The
89
75
  * `predicateTree` is the exact in-memory `PredicateNode` (canonical
@@ -97,16 +83,15 @@ export function synthesizeFromRecording(
97
83
  * builds a minimal honest SimulationResult downstream. */
98
84
  explain?: {
99
85
  predicateTree: PredicateNode | null
100
- simulation: SimulationResult
101
86
  }
102
87
  } {
103
88
  // Convert any ToolError-shaped throw (object with a string `.code`) to a
104
89
  // structured `{ok:false, error}`; anything else is rethrown so genuine bugs
105
90
  // crash instead of being silently swallowed. Wraps the entire body so
106
- // cap errors (PREDICATE_TOO_DEEP, TOO_MANY_LEAVES) and cloneScVal depth
91
+ // cap errors (PREDICATE_TOO_DEEP, TOO_MANY_LEAVES) and encoder depth
107
92
  // throws surface as structured ToolErrors rather than RangeErrors.
108
93
  try {
109
- return synthesizeFromRecordingInner(tx, opts, ozConfig)
94
+ return synthesizeFromRecordingInner(tx, opts)
110
95
  } catch (e) {
111
96
  if (isToolErrorShape(e)) {
112
97
  return {
@@ -145,12 +130,10 @@ function throwToolError(code: ToolError['code'], message: string): never {
145
130
 
146
131
  function synthesizeFromRecordingInner(
147
132
  tx: RecordedTransaction,
148
- opts: SynthesizeFromRecordingOptions,
149
- ozConfig: OzAdapterConfig
133
+ opts: SynthesizeFromRecordingOptions
150
134
  ): ToolResponse<ProposedPolicy> & {
151
135
  explain?: {
152
136
  predicateTree: PredicateNode | null
153
- simulation: SimulationResult
154
137
  }
155
138
  } {
156
139
  // 0. validate inputs (fail closed - never synthesize from garbage).
@@ -250,7 +233,6 @@ function synthesizeFromRecordingInner(
250
233
  const topLevel = tx.invocations[0] ?? null
251
234
  const composeOpts: ComposeOptions = {
252
235
  network: opts.network,
253
- interpreterEnabled: opts.interpreter !== undefined,
254
236
  ...(opts.userResponses !== undefined ? { userResponses: opts.userResponses } : {}),
255
237
  }
256
238
  const composed = composeFromRecording(facts, scope.contract, topLevel, composeOpts)
@@ -259,24 +241,6 @@ function synthesizeFromRecordingInner(
259
241
  // self-verify verdict (built from the SAME runHarness + evaluate that gated
260
242
  // the synthesis, not a parallel simulation).
261
243
  let explain: PredicateNode | null = null
262
- let explainSim: SimulationResult | null = null
263
-
264
- // 5. OZ compile (always runs).
265
- const ozAdapter = createOzAdapter(ozConfig)
266
- const compileRes = ozAdapter.compile(composed.ir)
267
-
268
- if (!compileRes.proposed) {
269
- return {
270
- ok: false,
271
- error: {
272
- code: 'SYNTHESIS_ERROR',
273
- message: `recording lowered to no installable OZ policy: ${compileRes.uncovered.join('; ')}`,
274
- severity: 'error',
275
- retryable: false,
276
- details: { uncovered: compileRes.uncovered },
277
- },
278
- }
279
- }
280
244
 
281
245
  // 6. Interpreter compile (opt-in; fail-closed when the user routed real
282
246
  // restrictions to the interpreter).
@@ -284,17 +248,13 @@ function synthesizeFromRecordingInner(
284
248
 
285
249
  let interpreterPolicyDocument: ProposedPolicy['policyDocuments'][number] | null = null
286
250
  let interpreterPolicyRef: ProposedPolicy['policyRefs'][number] | null = null
287
- // Cross-layer L3: declared OUTSIDE the `if (interpreterOpts)` block so the
288
- // warnings folded into `proposed.warnings[]` (which lives after that block)
289
- // can read it. The block assigns it; the default is empty.
290
- let permitCtxWarnings: string[] = []
251
+ let interpreterContextRule: ProposedPolicy['contextRule'] | null = null
291
252
 
292
253
  if (interpreterOpts) {
293
254
  const interpreterConfig: InterpreterAdapterConfig = {
294
255
  network: opts.network,
295
256
  installNonce: interpreterOpts.installNonce ?? 1,
296
257
  smartAccountAddress: interpreterOpts.smartAccountAddress,
297
- ...(interpreterOpts.oracleParams ? { oracleParams: interpreterOpts.oracleParams } : {}),
298
258
  }
299
259
 
300
260
  let startingPredicate: PredicateNode | null = null
@@ -313,23 +273,18 @@ function synthesizeFromRecordingInner(
313
273
  }
314
274
  startingPredicate = testSeam
315
275
  } else {
316
- const interpreterAdapter = createInterpreterAdapter(interpreterConfig)
317
-
318
- let interpreterRes: Awaited<ReturnType<typeof interpreterAdapter.compile>>
276
+ let interpreterRes: ReturnType<typeof compileInterpreterPolicy>
319
277
  try {
320
- interpreterRes = interpreterAdapter.compile(composed.interpreterIr)
278
+ interpreterRes = compileInterpreterPolicy(composed.interpreterRule, interpreterConfig)
321
279
  } catch (e) {
322
280
  const code = (e as { code?: ToolError['code'] }).code
323
281
  const allowedCodes: ToolError['code'][] = [
324
282
  'SCOPE_SELF_CALL',
325
- 'ORACLE_LEAF_INVALID_POSITION',
326
- 'ORACLE_PARAMS_OUT_OF_RANGE',
327
283
  'MALFORMED_PREDICATE',
328
284
  'PREDICATE_TOO_LARGE',
329
285
  'PREDICATE_TOO_DEEP',
330
286
  'TOO_MANY_LEAVES',
331
287
  'IN_OPERAND_LIMIT',
332
- 'PREDICATE_ORACLE_OVER_LIMIT',
333
288
  ]
334
289
  const surfacedCode = code && allowedCodes.includes(code) ? code : 'SYNTHESIS_ERROR'
335
290
  return {
@@ -370,6 +325,7 @@ function synthesizeFromRecordingInner(
370
325
 
371
326
  interpreterPolicyDocument = interpreterRes.proposed.policyDocuments[0] ?? null
372
327
  interpreterPolicyRef = interpreterRes.proposed.policyRefs[0] ?? null
328
+ interpreterContextRule = interpreterRes.proposed.contextRule
373
329
  if (!interpreterPolicyDocument || !interpreterPolicyRef) {
374
330
  return {
375
331
  ok: false,
@@ -382,7 +338,7 @@ function synthesizeFromRecordingInner(
382
338
  }
383
339
  }
384
340
 
385
- const firstInterpreterRule = composed.interpreterIr.rules[0]
341
+ const firstInterpreterRule = composed.interpreterRule
386
342
  if (!firstInterpreterRule) {
387
343
  return {
388
344
  ok: false,
@@ -409,7 +365,9 @@ function synthesizeFromRecordingInner(
409
365
  }
410
366
  }
411
367
 
412
- // 6b. Self-verify + minimise.
368
+ // 6b. The predicate is used as compiled. There is no minimisation pass:
369
+ // nothing strips a leaf and re-checks that the reduced predicate still
370
+ // denies, so the result is NOT a proven-minimal policy.
413
371
  if (!topLevel) {
414
372
  return {
415
373
  ok: false,
@@ -432,93 +390,24 @@ function synthesizeFromRecordingInner(
432
390
  },
433
391
  }
434
392
  }
435
- // Cross-layer L3: warnings collected from `buildPermitContext` (currently
436
- // only the oracle-on-right normaliser) and folded into the proposed
437
- // policy's `warnings[]` so the caller sees them on the success envelope.
438
- const permitCtxResult = buildPermitContext(
439
- tx,
440
- scope,
441
- topLevel,
442
- opts.userResponses,
443
- startingPredicate
444
- )
445
- const permitCtx = permitCtxResult.ctx
446
- permitCtxWarnings = permitCtxResult.warnings
447
-
448
- const finalPredicate: PredicateNode =
449
- startingPredicate.op === 'and'
450
- ? minimize(startingPredicate, permitCtx, ORIGINAL_DIMENSIONS)
451
- : startingPredicate
452
-
453
- const harnessCases = generateCases(finalPredicate, permitCtx, ORIGINAL_DIMENSIONS)
454
- const harnessResult = runHarness(finalPredicate, harnessCases)
455
- if (!harnessResult.ok) {
456
- return {
457
- ok: false,
458
- error: {
459
- code: 'DENY_CASE_FAILURE',
460
- message: `self-verify harness failed for the interpreter predicate (${harnessResult.failures.length} failure(s))`,
461
- severity: 'error',
462
- retryable: false,
463
- details: { failures: harnessResult.failures },
464
- },
465
- }
466
- }
467
- const evalResult = evaluate(finalPredicate, permitCtx)
468
- if (!evalResult.permit) {
469
- return {
470
- ok: false,
471
- error: {
472
- code: 'DENY_CASE_FAILURE',
473
- message: `intended recorded call was denied by the interpreter predicate: ${evalResult.reason}`,
474
- severity: 'error',
475
- retryable: false,
476
- details: { reason: evalResult.reason },
477
- },
478
- }
479
- }
393
+ const finalPredicate: PredicateNode = startingPredicate
480
394
 
481
- // --explain capture: quote the SAME verdict that gated the synthesis.
482
- // Re-evaluate each deny case to surface its concrete reason (the harness
483
- // only records whether the got-matches-expected boundary held).
484
395
  if (opts.explain) {
485
396
  explain = finalPredicate
486
- const evaluatedCases: SimulationResult['evaluatedCases'] = [
487
- {
488
- dimension: 'permit',
489
- outcome: evalResult.permit ? 'permit' : 'deny',
490
- reason: 'matches recorded call',
491
- },
492
- ]
493
- for (const deny of harnessCases.denies) {
494
- const r = evaluate(finalPredicate, deny.ctx)
495
- evaluatedCases.push({
496
- dimension: deny.dimension,
497
- outcome: r.permit ? 'permit' : 'deny',
498
- reason: r.permit ? 'no matching deny' : r.reason,
499
- })
500
- }
501
- explainSim = {
502
- permit: { tx: 'permit' },
503
- evaluatedCases,
504
- backend: 'ts-model',
505
- simulatorVersion: 'ts-model-1.0.0',
506
- }
507
397
  }
508
398
 
509
- // 6c. Re-encode the (possibly minimised) PredicateNode and stamp the
510
- // canonical bytes back onto the PolicyDocument + PolicyRef. Cap breaches
399
+ // 6c. Encode the PredicateNode and stamp the canonical bytes back onto
400
+ // the PolicyDocument + PolicyRef. Cap breaches
511
401
  // (PREDICATE_TOO_DEEP, TOO_MANY_LEAVES) throw ToolError-shaped errors;
512
402
  // the outer envelope converts them to structured `{ok:false, error}`.
513
403
  const { encodedPredicate, predicateHash } = encodePredicate(finalPredicate)
514
404
 
515
405
  if (testSeam !== undefined) {
516
406
  interpreterPolicyDocument = {
517
- grammarVersion: 1,
407
+ grammarVersion: GRAMMAR_VERSION,
518
408
  installNonce: interpreterOpts.installNonce ?? 1,
519
409
  encodedPredicate,
520
410
  predicateHash,
521
- ...(interpreterOpts.oracleParams ? { oracleParams: interpreterOpts.oracleParams } : {}),
522
411
  }
523
412
  interpreterPolicyRef = {
524
413
  kind: 'interpreter',
@@ -551,19 +440,25 @@ function synthesizeFromRecordingInner(
551
440
  }
552
441
  }
553
442
 
554
- // When the interpreter succeeds, OZ-side `uncovered` warnings that the
555
- // interpreter actually lowered are misleading: OZ really did not lower them,
556
- // but the interpreter did. Drop those so the user-facing warnings reflect
557
- // what is still UN-enforced, not what OZ alone could not do.
558
- const ozUncovered = interpreterPolicyRef
559
- ? compileRes.uncovered.filter((u) => !INTERPRETER_COVERED_OZ_PATTERN.test(u))
560
- : compileRes.uncovered
443
+ // 7. Assemble the ProposedPolicy. The interpreter is the only backend, so
444
+ // its context rule and policy ref are the whole policy. Without interpreter
445
+ // options nothing installable was produced, which is an error rather than
446
+ // an empty-but-successful policy.
447
+ if (!interpreterContextRule || !interpreterPolicyRef) {
448
+ return {
449
+ ok: false,
450
+ error: {
451
+ code: 'SYNTHESIS_ERROR',
452
+ message:
453
+ 'no installable policy was synthesised: supply `interpreter` options (a smart account address) so the recording can be lowered to an interpreter predicate',
454
+ severity: 'error',
455
+ retryable: false,
456
+ details: { uncovered: composed.warnings },
457
+ },
458
+ }
459
+ }
561
460
 
562
- // 7. Merge into the OZ-shaped ProposedPolicy.
563
- const ozRefs = compileRes.proposed.policyRefs
564
- const mergedRefs: ProposedPolicy['policyRefs'] = []
565
- if (interpreterPolicyRef) mergedRefs.push(interpreterPolicyRef)
566
- for (const r of ozRefs) mergedRefs.push(r)
461
+ const mergedRefs: ProposedPolicy['policyRefs'] = [interpreterPolicyRef]
567
462
 
568
463
  if (mergedRefs.length > OZ_LIMITS.maxPoliciesPerRule) {
569
464
  return {
@@ -578,7 +473,7 @@ function synthesizeFromRecordingInner(
578
473
  }
579
474
 
580
475
  const mergedContextRule = {
581
- ...compileRes.proposed.contextRule,
476
+ ...interpreterContextRule,
582
477
  policies: mergedRefs,
583
478
  }
584
479
 
@@ -598,12 +493,7 @@ function synthesizeFromRecordingInner(
598
493
  policyDocuments: interpreterPolicyDocument ? [interpreterPolicyDocument] : [],
599
494
  policyRefs: mergedRefs,
600
495
  parseConfidence: { ...tx.parseConfidence },
601
- warnings: [
602
- ...zeroPolicyWarning,
603
- ...ozUncovered.map((u) => `${UNCOVERED_PREFIX}${u}`),
604
- ...composed.warnings.map((w) => `${UNCOVERED_PREFIX}${w}`),
605
- ...permitCtxWarnings,
606
- ],
496
+ warnings: [...zeroPolicyWarning, ...composed.warnings.map((w) => `${UNCOVERED_PREFIX}${w}`)],
607
497
  ambiguities: mergeAmbiguities(composed.ambiguities, scope.ambiguities),
608
498
  }
609
499
  // --explain success envelope. When opts.explain is set and the OZ-only
@@ -613,48 +503,16 @@ function synthesizeFromRecordingInner(
613
503
  const envelope: ToolResponse<ProposedPolicy> & {
614
504
  explain?: {
615
505
  predicateTree: PredicateNode | null
616
- simulation: SimulationResult
617
506
  }
618
507
  } = { ok: true, data: proposed }
619
508
  if (opts.explain) {
620
- if (explainSim) {
621
- envelope.explain = { predicateTree: explain, simulation: explainSim }
622
- } else {
623
- envelope.explain = {
624
- predicateTree: null,
625
- simulation: {
626
- permit: {
627
- tx: 'deny',
628
- reason: 'No self-verification was performed (interpreter adapter was not engaged)',
629
- },
630
- evaluatedCases: [],
631
- backend: 'ts-model',
632
- simulatorVersion: 'not-run',
633
- },
634
- }
635
- }
509
+ envelope.explain = { predicateTree: explain }
636
510
  }
637
511
  return envelope
638
512
  }
639
513
 
640
514
  export { throwToolError }
641
515
 
642
- /** OZ-side `uncovered` warning patterns the interpreter adapter actually lowers
643
- * when wired in. When the interpreter adapter succeeds, matching entries are
644
- * dropped from the OZ uncovered list so user-facing warnings reflect what is
645
- * still UN-enforced rather than what OZ alone could not do. Matches the
646
- * exact descriptor strings the OZ adapter emits (see `src/adapters/oz/adapter.ts`
647
- * `describeCondition` / `describeSelector`). */
648
- const INTERPRETER_COVERED_OZ_PATTERN =
649
- /^per-method scoping to|^value allowlist on arg|^exact ordered sequence on arg|^oracle price condition on|^invocation-count window|^spending_limit on token .+ needs a CallContract context scoped to that token/
650
-
651
- /** Reject non-sane inputs before any policy is synthesized. windowSeconds /
652
- * validUntilLedger / invocationLimit must be positive integers; limitAmount a
653
- * positive i128 decimal string; network mainnet|testnet. When the interpreter
654
- * adapter is opted in, `smartAccountAddress` must be a C... contract address
655
- * (the on-chain policy-bound account, NOT the G... source account),
656
- * `installNonce` must fit u32 (default 1), and `oracleParams` must
657
- * tighten-only vs the wasm defaults. */
658
516
  function validateOptions(opts: SynthesizeFromRecordingOptions): ToolError | null {
659
517
  if (opts.network !== 'mainnet' && opts.network !== 'testnet') {
660
518
  return synthesisError(`network must be 'mainnet' or 'testnet', got: ${String(opts.network)}`)
@@ -669,9 +527,6 @@ function validateOptions(opts: SynthesizeFromRecordingOptions): ToolError | null
669
527
  }
670
528
  const ur = opts.userResponses
671
529
  if (ur) {
672
- if (ur.windowSeconds !== undefined && !isPositiveInt(ur.windowSeconds)) {
673
- return synthesisError(`windowSeconds must be a positive integer, got: ${ur.windowSeconds}`)
674
- }
675
530
  if (
676
531
  ur.validUntilLedger !== undefined &&
677
532
  (!isPositiveInt(ur.validUntilLedger) || ur.validUntilLedger > SOROBAN_LIMITS.u32Max)
@@ -680,11 +535,6 @@ function validateOptions(opts: SynthesizeFromRecordingOptions): ToolError | null
680
535
  `validUntilLedger must be a positive u32 ledger sequence (<= ${SOROBAN_LIMITS.u32Max}), got: ${ur.validUntilLedger}`
681
536
  )
682
537
  }
683
- if (ur.invocationLimit !== undefined && !isPositiveInt(ur.invocationLimit)) {
684
- return synthesisError(
685
- `invocationLimit must be a positive integer, got: ${ur.invocationLimit}`
686
- )
687
- }
688
538
  if (ur.limitAmount !== undefined && !isPositiveI128(ur.limitAmount)) {
689
539
  return synthesisError(
690
540
  `limitAmount must be a positive i128 decimal string within [1, ${I128_MAX}] (2^127-1), got: ${ur.limitAmount}`
@@ -718,27 +568,6 @@ function validateOptions(opts: SynthesizeFromRecordingOptions): ToolError | null
718
568
  `interpreter.installNonce must be a positive u32 integer (<= ${SOROBAN_LIMITS.u32Max}), got: ${nonce}`
719
569
  )
720
570
  }
721
- const op = opts.interpreter.oracleParams
722
- if (op) {
723
- const MAX_STALE = 600
724
- const MAX_DEV = 200
725
- if (
726
- op.maxStalenessSeconds !== undefined &&
727
- (!isPositiveInt(op.maxStalenessSeconds) || op.maxStalenessSeconds > MAX_STALE)
728
- ) {
729
- return synthesisError(
730
- `interpreter.oracleParams.maxStalenessSeconds must be a positive integer <= ${MAX_STALE} (tighten-only), got: ${op.maxStalenessSeconds}`
731
- )
732
- }
733
- if (
734
- op.maxDeviationBps !== undefined &&
735
- (!isPositiveInt(op.maxDeviationBps) || op.maxDeviationBps > MAX_DEV)
736
- ) {
737
- return synthesisError(
738
- `interpreter.oracleParams.maxDeviationBps must be a positive integer <= ${MAX_DEV} (tighten-only), got: ${op.maxDeviationBps}`
739
- )
740
- }
741
- }
742
571
  }
743
572
  return null
744
573
  }
@@ -798,245 +627,3 @@ function mergeAmbiguities(
798
627
  }
799
628
  return out
800
629
  }
801
-
802
- /** Build the permit `EvalContext` the self-verify harness drives. Shape
803
- * mirrors the intended recorded call so:
804
- * - `evaluate(predicate, ctx).permit === true` must hold (a failure
805
- * surfaces as DENY_CASE_FAILURE).
806
- * - `generateCases(predicate, ctx)` produces a deny battery that reflects
807
- * the actual recorded move (real amount, args, window start).
808
- * Amounts are summed per-token over all movements (BigInt, never lossy).
809
- * `oraclePriceByAsset` contains a price+timestamp satisfying each
810
- * `oracle_price` leaf so the intended call permits under every bound;
811
- * the harness mutates those entries (stale / missing / deviation / paused)
812
- * to exercise the ORACLE_* deny paths. */
813
- function buildPermitContext(
814
- tx: RecordedTransaction,
815
- scope: Extract<ScopeDecision, { kind: 'call_contract' }>,
816
- topLevel: ContractInvocation,
817
- userResponses: ComposeUserResponses | undefined,
818
- predicate: PredicateNode
819
- ): { ctx: EvalContext; warnings: string[] } {
820
- const amountByToken: Record<string, string> = {}
821
- const totals = new Map<string, bigint>()
822
- for (const m of tx.tokenMovements) {
823
- const current = totals.get(m.token) ?? 0n
824
- totals.set(m.token, current + BigInt(m.amount))
825
- }
826
- for (const [token, total] of totals) {
827
- amountByToken[token] = total.toString()
828
- }
829
-
830
- const warnings: string[] = []
831
- const oraclePriceByAsset: EvalContext['oraclePriceByAsset'] = oracleSatisfyingPrices(
832
- predicate,
833
- tx.fetchedAt,
834
- warnings
835
- )
836
-
837
- const ctx: EvalContext = {
838
- contract: scope.contract,
839
- fn: topLevel.fn,
840
- args: topLevel.args.map(cloneScVal),
841
- atLedger: tx.ledgerSequence,
842
- nowSeconds: tx.fetchedAt,
843
- amountByToken,
844
- windowSpentByToken: {},
845
- invocationCountByWindow: {},
846
- oraclePriceByAsset,
847
- }
848
- if (userResponses?.validUntilLedger !== undefined) {
849
- ctx.validUntilLedger = userResponses.validUntilLedger
850
- }
851
- return { ctx, warnings }
852
- }
853
-
854
- function cloneScVal(
855
- value: { type: string; value: unknown },
856
- depth = 0
857
- ): ContractInvocation['args'][number] {
858
- // Clone top-level shells so the harness can mutate deny cases without
859
- // aliasing the recorded call. Recursion bounded by MAX_SCVAL_CLONE_DEPTH so
860
- // a hand-crafted nested-vec cannot RangeError the JS stack; the over-depth
861
- // branch throws a ToolError-shaped error the envelope converts to
862
- // `{ok:false, error}`.
863
- if (value.type === 'vec') {
864
- if (depth >= MAX_SCVAL_CLONE_DEPTH) {
865
- throw cloneDepthError(value)
866
- }
867
- return {
868
- type: 'vec',
869
- value: (value.value as ContractInvocation['args'][number][]).map((v) =>
870
- cloneScVal(v, depth + 1)
871
- ),
872
- }
873
- }
874
- return { ...value } as ContractInvocation['args'][number]
875
- }
876
-
877
- function cloneDepthError(value: { type: string; value: unknown }): never {
878
- const err = new Error(
879
- `ScVal clone depth exceeds MAX_SCVAL_CLONE_DEPTH (${MAX_SCVAL_CLONE_DEPTH})`
880
- ) as Error & { code: string; severity: string; retryable: boolean; depthContext: unknown }
881
- err.code = 'SYNTHESIS_ERROR'
882
- err.severity = 'error'
883
- err.retryable = false
884
- err.depthContext = value.type
885
- throw err
886
- }
887
-
888
- /** Walk every `oracle_price` leaf and return a price map whose entries satisfy
889
- * the bound. Timestamp pinned to `nowSeconds` (the recorded `fetchedAt`) so
890
- * the fresh-oracle deny case in `generateCases` is the only path that flips
891
- * this map. Negatives clamped at 0 - oracle prices are non-negative on Stellar. */
892
- function oracleSatisfyingPrices(
893
- predicate: PredicateNode,
894
- nowSeconds: number,
895
- warnings: string[]
896
- ): EvalContext['oraclePriceByAsset'] {
897
- const out: EvalContext['oraclePriceByAsset'] = {}
898
- visitOracleLeaves(
899
- predicate,
900
- (asset, op, bound) => {
901
- let price: bigint
902
- switch (op) {
903
- case 'lt':
904
- case 'gt':
905
- price = op === 'lt' ? bound - 1n : bound + 1n
906
- break
907
- case 'lte':
908
- case 'gte':
909
- case 'eq':
910
- price = bound
911
- break
912
- }
913
- if (price < 0n) price = 0n
914
- out[asset] = { price: price.toString(), timestampSeconds: nowSeconds }
915
- },
916
- (warning) => {
917
- // Cross-layer L3: oracle-on-right that cannot be normalised is
918
- // surfaced as a warning, not silently dropped. The Rust interpreter
919
- // would surface this as `UnsupportedNode`; the warning lets the
920
- // caller decide whether to fix the predicate shape or accept the
921
- // over-permissive hole.
922
- warnings.push(warning.message)
923
- }
924
- )
925
- return out
926
- }
927
-
928
- // Cross-layer L3: surface oracle-on-right as a WARNING rather than silently
929
- // dropping it. The Rust interpreter only fires `eval_oracle_compare` when the
930
- // oracle leaf is on the LEFT (`oracle_price op threshold`); a predicate that
931
- // inverts the shape (`threshold op oracle_price`) falls through to
932
- // `UnsupportedNode` instead. The TS model and the lowering path both attempt
933
- // to normalise oracle-on-right by flipping the operator so the oracle ends
934
- // up on the left, but the normalisation is only possible when the other
935
- // side is a literal we can read as a bigint (an `oracle_threshold`, an
936
- // `i128`/`u32`/`u64` literal). When it isn't (e.g. `oracle_price < call_arg[i]`),
937
- // the case cannot be normalised - previously we silently dropped it, which
938
- // means the harness would never exercise the bound and `minimize` could
939
- // prune it. The warning is purely additive: the existing normalisation
940
- // path is preserved, the case is just also reported so callers can decide
941
- // to either fix the predicate shape or accept the over-permissive hole.
942
-
943
- /** Diagnostic reported by `visitOracleLeaves` when an oracle comparison
944
- * cannot be normalised to oracle-on-left. Currently only "oracle-on-right
945
- * with a non-literal LHS" - the other branches already produce a parseable
946
- * threshold and are returned to the caller. */
947
- export interface OracleNormalisationWarning {
948
- /** The dimension name (currently always "oracle_normalisation_dropped"). */
949
- dimension: 'oracle_normalisation_dropped'
950
- /** The operator as written. */
951
- op: 'eq' | 'lt' | 'lte' | 'gt' | 'gte'
952
- /** The asset the oracle leaf is bound to. */
953
- asset: string
954
- /** The non-literal RHS (oracle-on-left) or LHS (oracle-on-right) leaf
955
- * whose shape stopped the normalisation. Surfaced for diagnostics;
956
- * intentionally a partial view - the full leaf is the caller's job. */
957
- otherKind: string
958
- /** Human-readable message (matches the warn-on-drop text). */
959
- message: string
960
- }
961
-
962
- function visitOracleLeaves(
963
- node: PredicateNode,
964
- visit: (asset: string, op: 'eq' | 'lt' | 'lte' | 'gt' | 'gte', bound: bigint) => void,
965
- onWarning?: (warning: OracleNormalisationWarning) => void
966
- ): void {
967
- switch (node.op) {
968
- case 'and':
969
- case 'or':
970
- for (const child of node.children) visitOracleLeaves(child, visit, onWarning)
971
- return
972
- case 'not':
973
- visitOracleLeaves(node.child, visit, onWarning)
974
- return
975
- case 'eq':
976
- case 'lt':
977
- case 'lte':
978
- case 'gt':
979
- case 'gte': {
980
- const leftLeaf: PredicateLeaf = node.left
981
- const rightLeaf: PredicateLeaf = node.right
982
- const leftIsOracle = leftLeaf.kind === 'oracle_price'
983
- const rightIsOracle = rightLeaf.kind === 'oracle_price'
984
- let oracleAsset: string | undefined
985
- let literal: bigint | undefined
986
- if (leftIsOracle) {
987
- oracleAsset = leftLeaf.asset
988
- literal = oracleLiteralFromLeaf(rightLeaf)
989
- } else if (rightIsOracle) {
990
- oracleAsset = rightLeaf.asset
991
- literal = oracleLiteralFromLeaf(leftLeaf)
992
- }
993
- if (oracleAsset === undefined || literal === undefined) {
994
- // Two reasons the case cannot be normalised:
995
- // - neither side is an oracle leaf (visitor has nothing to do)
996
- // - oracle-on-right with a non-literal LHS (Rust dispatch would
997
- // hit UnsupportedNode; the TS model cannot build a permit ctx
998
- // for it)
999
- // Only the second is a meaningful warning; the first is a no-op
1000
- // (the visitor was called for a non-oracle comparison). Emit the
1001
- // warning when an oracle IS present on one side but the other
1002
- // side is not a parseable threshold literal.
1003
- if (rightIsOracle) {
1004
- onWarning?.({
1005
- dimension: 'oracle_normalisation_dropped',
1006
- op: node.op,
1007
- asset: rightLeaf.kind === 'oracle_price' ? rightLeaf.asset : '',
1008
- otherKind: leftLeaf.kind,
1009
- message: `oracle-on-right cannot be normalised: the LHS (kind=${leftLeaf.kind}) is not an oracle_threshold literal; the Rust interpreter would surface UnsupportedNode here`,
1010
- })
1011
- }
1012
- return
1013
- }
1014
- visit(oracleAsset, node.op, literal)
1015
- return
1016
- }
1017
- case 'in':
1018
- // `in` is pure membership; oracle leaves inside haystacks are
1019
- // forbidden by the position rule, so there's nothing to set up here.
1020
- return
1021
- }
1022
- }
1023
-
1024
- /** Restate an oracle threshold on the normalised 9-dp basis prices use.
1025
- * Thresholds carry their own basis, so reading the digits raw would build
1026
- * a permit context off by 10^(decimals-9) and the intended call would not
1027
- * satisfy its own bound. Mirrors NORMALISED_DECIMALS in oracle.rs. */
1028
- function oracleLiteralFromLeaf(leaf: PredicateLeaf): bigint | undefined {
1029
- if (leaf.kind !== 'oracle_threshold') return undefined
1030
- let value: bigint
1031
- try {
1032
- value = BigInt(leaf.value)
1033
- } catch {
1034
- return undefined
1035
- }
1036
- const normalised = 9n
1037
- const decimals = BigInt(leaf.decimals)
1038
- if (decimals <= normalised) return value * 10n ** (normalised - decimals)
1039
- // Floor: a finer-grained threshold has no exact 9-dp representation; the
1040
- // caller offsets by one from here to land on the right side of the bound.
1041
- return value / 10n ** (decimals - normalised)
1042
- }