@crediolabs/policy-synth 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (184) hide show
  1. package/README.md +4 -4
  2. package/dist/adapters/interpreter/adapter.d.ts +19 -25
  3. package/dist/adapters/interpreter/adapter.js +54 -487
  4. package/dist/adapters/interpreter/index.d.ts +1 -1
  5. package/dist/adapters/interpreter/index.js +1 -1
  6. package/dist/adapters/oz/adapter.js +1 -15
  7. package/dist/errors.d.ts +1 -1
  8. package/dist/index.d.ts +1 -4
  9. package/dist/index.js +1 -4
  10. package/dist/install/build-add-context-rule.d.ts +3 -19
  11. package/dist/install/build-add-context-rule.js +3 -98
  12. package/dist/install/build-install-policy.d.ts +4 -53
  13. package/dist/install/build-install-policy.js +44 -123
  14. package/dist/install/index.d.ts +0 -2
  15. package/dist/install/index.js +0 -7
  16. package/dist/ir/index.d.ts +1 -1
  17. package/dist/ir/types.d.ts +7 -76
  18. package/dist/ir/types.js +0 -2
  19. package/dist/predicate/decode.d.ts +1 -1
  20. package/dist/predicate/decode.js +2 -70
  21. package/dist/predicate/encode.js +45 -229
  22. package/dist/predicate/from-json.js +1 -42
  23. package/dist/record/decode.js +3 -8
  24. package/dist/review-card/builder.d.ts +4 -3
  25. package/dist/review-card/builder.js +18 -162
  26. package/dist/review-card/cross-check.js +2 -105
  27. package/dist/review-card/render-leaf.d.ts +4 -0
  28. package/dist/review-card/render-leaf.js +47 -0
  29. package/dist/run/index.d.ts +36 -139
  30. package/dist/run/index.js +125 -437
  31. package/dist/run/schemas.d.ts +168 -1929
  32. package/dist/run/schemas.js +40 -269
  33. package/dist/seams/types.d.ts +0 -2
  34. package/dist/simulate/deny-cases.d.ts +26 -0
  35. package/dist/simulate/deny-cases.js +282 -0
  36. package/dist/simulate/evaluate.d.ts +18 -0
  37. package/dist/simulate/evaluate.js +271 -0
  38. package/dist/simulate/index.d.ts +4 -0
  39. package/dist/simulate/index.js +8 -0
  40. package/dist/synth/compose-from-recording.d.ts +24 -51
  41. package/dist/synth/compose-from-recording.js +120 -282
  42. package/dist/synth/deny-cases.d.ts +5 -7
  43. package/dist/synth/deny-cases.js +23 -373
  44. package/dist/synth/evaluate.d.ts +0 -21
  45. package/dist/synth/evaluate.js +56 -336
  46. package/dist/synth/index.d.ts +0 -5
  47. package/dist/synth/index.js +0 -5
  48. package/dist/synth/permit-context.d.ts +2 -9
  49. package/dist/synth/permit-context.js +1 -82
  50. package/dist/synth/synthesize-from-recording.d.ts +1 -29
  51. package/dist/synth/synthesize-from-recording.js +41 -356
  52. package/dist/types.d.ts +8 -60
  53. package/dist/types.js +5 -2
  54. package/dist/verify/simulate.d.ts +1 -14
  55. package/dist/verify/simulate.js +2 -88
  56. package/dist/verify/verify.d.ts +1 -7
  57. package/dist/verify/verify.js +0 -8
  58. package/dist-cjs/adapters/interpreter/adapter.d.ts +19 -25
  59. package/dist-cjs/adapters/interpreter/adapter.js +56 -489
  60. package/dist-cjs/adapters/interpreter/index.d.ts +1 -1
  61. package/dist-cjs/adapters/interpreter/index.js +1 -2
  62. package/dist-cjs/adapters/oz/adapter.js +1 -15
  63. package/dist-cjs/errors.d.ts +1 -1
  64. package/dist-cjs/index.d.ts +1 -4
  65. package/dist-cjs/index.js +1 -4
  66. package/dist-cjs/install/build-add-context-rule.d.ts +3 -19
  67. package/dist-cjs/install/build-add-context-rule.js +1 -97
  68. package/dist-cjs/install/build-install-policy.d.ts +4 -53
  69. package/dist-cjs/install/build-install-policy.js +44 -122
  70. package/dist-cjs/install/index.d.ts +0 -2
  71. package/dist-cjs/install/index.js +2 -23
  72. package/dist-cjs/ir/index.d.ts +1 -1
  73. package/dist-cjs/ir/types.d.ts +7 -76
  74. package/dist-cjs/ir/types.js +0 -2
  75. package/dist-cjs/predicate/decode.d.ts +1 -1
  76. package/dist-cjs/predicate/decode.js +2 -70
  77. package/dist-cjs/predicate/encode.js +45 -229
  78. package/dist-cjs/predicate/from-json.js +1 -42
  79. package/dist-cjs/record/decode.js +3 -8
  80. package/dist-cjs/review-card/builder.d.ts +4 -3
  81. package/dist-cjs/review-card/builder.js +23 -167
  82. package/dist-cjs/review-card/cross-check.js +7 -110
  83. package/dist-cjs/review-card/render-leaf.d.ts +4 -0
  84. package/dist-cjs/review-card/render-leaf.js +52 -0
  85. package/dist-cjs/run/index.d.ts +36 -139
  86. package/dist-cjs/run/index.js +125 -444
  87. package/dist-cjs/run/schemas.d.ts +168 -1929
  88. package/dist-cjs/run/schemas.js +41 -270
  89. package/dist-cjs/seams/types.d.ts +0 -2
  90. package/dist-cjs/simulate/deny-cases.d.ts +26 -0
  91. package/dist-cjs/simulate/deny-cases.js +286 -0
  92. package/dist-cjs/simulate/evaluate.d.ts +18 -0
  93. package/dist-cjs/simulate/evaluate.js +274 -0
  94. package/dist-cjs/simulate/index.d.ts +4 -0
  95. package/dist-cjs/simulate/index.js +13 -0
  96. package/dist-cjs/synth/compose-from-recording.d.ts +24 -51
  97. package/dist-cjs/synth/compose-from-recording.js +120 -282
  98. package/dist-cjs/synth/deny-cases.d.ts +5 -7
  99. package/dist-cjs/synth/deny-cases.js +23 -374
  100. package/dist-cjs/synth/evaluate.d.ts +0 -21
  101. package/dist-cjs/synth/evaluate.js +57 -337
  102. package/dist-cjs/synth/index.d.ts +0 -5
  103. package/dist-cjs/synth/index.js +1 -11
  104. package/dist-cjs/synth/permit-context.d.ts +2 -9
  105. package/dist-cjs/synth/permit-context.js +1 -82
  106. package/dist-cjs/synth/synthesize-from-recording.d.ts +1 -29
  107. package/dist-cjs/synth/synthesize-from-recording.js +39 -354
  108. package/dist-cjs/types.d.ts +8 -60
  109. package/dist-cjs/types.js +6 -3
  110. package/dist-cjs/verify/simulate.d.ts +1 -14
  111. package/dist-cjs/verify/simulate.js +2 -88
  112. package/dist-cjs/verify/verify.d.ts +1 -7
  113. package/dist-cjs/verify/verify.js +0 -8
  114. package/package.json +4 -4
  115. package/src/adapters/interpreter/adapter.ts +76 -572
  116. package/src/errors.ts +0 -19
  117. package/src/index.ts +1 -4
  118. package/src/install/build-add-context-rule.ts +5 -139
  119. package/src/install/build-install-policy.ts +93 -214
  120. package/src/install/index.ts +0 -34
  121. package/src/predicate/decode.ts +2 -70
  122. package/src/predicate/encode.ts +49 -261
  123. package/src/predicate/from-json.ts +1 -43
  124. package/src/record/decode.ts +3 -8
  125. package/src/review-card/builder.ts +19 -168
  126. package/src/review-card/cross-check.ts +3 -105
  127. package/src/review-card/render-leaf.ts +48 -0
  128. package/src/run/index.ts +144 -572
  129. package/src/run/schemas.ts +42 -291
  130. package/src/simulate/deny-cases.ts +334 -0
  131. package/src/simulate/evaluate.ts +284 -0
  132. package/src/simulate/index.ts +11 -0
  133. package/src/synth/compose-from-recording.ts +148 -347
  134. package/src/synth/index.ts +0 -13
  135. package/src/synth/synthesize-from-recording.ts +43 -456
  136. package/src/types.ts +13 -49
  137. package/dist/install/authority-overlap.d.ts +0 -134
  138. package/dist/install/authority-overlap.js +0 -0
  139. package/dist/install/build-install-predicate.d.ts +0 -96
  140. package/dist/install/build-install-predicate.js +0 -444
  141. package/dist/install/build-merge-policy.d.ts +0 -70
  142. package/dist/install/build-merge-policy.js +0 -130
  143. package/dist/install/plan-merge-policy.d.ts +0 -49
  144. package/dist/install/plan-merge-policy.js +0 -86
  145. package/dist/install/read-account-rules.d.ts +0 -100
  146. package/dist/install/read-account-rules.js +0 -283
  147. package/dist-cjs/install/authority-overlap.d.ts +0 -134
  148. package/dist-cjs/install/authority-overlap.js +0 -0
  149. package/dist-cjs/install/build-install-predicate.d.ts +0 -96
  150. package/dist-cjs/install/build-install-predicate.js +0 -479
  151. package/dist-cjs/install/build-merge-policy.d.ts +0 -70
  152. package/dist-cjs/install/build-merge-policy.js +0 -134
  153. package/dist-cjs/install/plan-merge-policy.d.ts +0 -49
  154. package/dist-cjs/install/plan-merge-policy.js +0 -90
  155. package/dist-cjs/install/read-account-rules.d.ts +0 -100
  156. package/dist-cjs/install/read-account-rules.js +0 -296
  157. package/src/adapters/interpreter/index.ts +0 -8
  158. package/src/adapters/oz/adapter.ts +0 -376
  159. package/src/adapters/oz/index.ts +0 -9
  160. package/src/codegen/compile-gate.ts +0 -167
  161. package/src/codegen/index.ts +0 -17
  162. package/src/codegen/template.ts +0 -165
  163. package/src/install/authority-overlap.ts +0 -0
  164. package/src/install/build-merge-policy.ts +0 -219
  165. package/src/install/plan-merge-policy.ts +0 -133
  166. package/src/install/read-account-rules.ts +0 -376
  167. package/src/ir/index.ts +0 -13
  168. package/src/ir/types.ts +0 -132
  169. package/src/mandate/index.ts +0 -4
  170. package/src/mandate/to-ir.ts +0 -71
  171. package/src/mandate/types.ts +0 -21
  172. package/src/seams/index.ts +0 -11
  173. package/src/seams/types.ts +0 -81
  174. package/src/synth/deny-cases.ts +0 -663
  175. package/src/synth/evaluate.ts +0 -613
  176. package/src/synth/harness.ts +0 -68
  177. package/src/synth/minimize.ts +0 -48
  178. package/src/synth/permit-context.ts +0 -136
  179. package/src/synth/predicate-literals.ts +0 -27
  180. package/src/synth/synthesize-from-mandate.ts +0 -82
  181. package/src/verify/envelope.ts +0 -28
  182. package/src/verify/index.ts +0 -5
  183. package/src/verify/simulate.ts +0 -311
  184. package/src/verify/verify.ts +0 -243
@@ -1,30 +1,25 @@
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).
8
- import { createInterpreterAdapter, lowerRuleToPredicate, PLACEHOLDER_INTERPRETER_ADDRESS, } from "../adapters/interpreter/adapter.js";
9
- import { createOzAdapter } from "../adapters/oz/adapter.js";
6
+ // -> interpreter compile + self-verify.
7
+ // Same (tx, opts) -> byte-identical ProposedPolicy (no randomness, clock, globals).
8
+ import { compileInterpreterPolicy, lowerRuleToPredicate, PLACEHOLDER_INTERPRETER_ADDRESS, } from "../adapters/interpreter/adapter.js";
10
9
  import { encodePredicate } from "../predicate/encode.js";
11
- import { MAX_SCVAL_CLONE_DEPTH, OZ_LIMITS, SOROBAN_LIMITS, } from "../types.js";
10
+ import { GRAMMAR_VERSION, OZ_LIMITS, SOROBAN_LIMITS, } from "../types.js";
12
11
  import { composeFromRecording, } from "./compose-from-recording.js";
13
- import { generateCases, ORIGINAL_DIMENSIONS } from "./deny-cases.js";
14
- import { evaluate } from "./evaluate.js";
15
- import { runHarness } from "./harness.js";
16
12
  import { lower } from "./lower.js";
17
- import { minimize } from "./minimize.js";
18
13
  import { decideScope } from "./scope.js";
19
- const UNCOVERED_PREFIX = 'Not covered by OZ built-in primitives: ';
20
- export function synthesizeFromRecording(tx, opts, ozConfig) {
14
+ const UNCOVERED_PREFIX = 'Not expressible as a predicate constraint: ';
15
+ export function synthesizeFromRecording(tx, opts) {
21
16
  // Convert any ToolError-shaped throw (object with a string `.code`) to a
22
17
  // structured `{ok:false, error}`; anything else is rethrown so genuine bugs
23
18
  // crash instead of being silently swallowed. Wraps the entire body so
24
- // cap errors (PREDICATE_TOO_DEEP, TOO_MANY_LEAVES) and cloneScVal depth
19
+ // cap errors (PREDICATE_TOO_DEEP, TOO_MANY_LEAVES) and encoder depth
25
20
  // throws surface as structured ToolErrors rather than RangeErrors.
26
21
  try {
27
- return synthesizeFromRecordingInner(tx, opts, ozConfig);
22
+ return synthesizeFromRecordingInner(tx, opts);
28
23
  }
29
24
  catch (e) {
30
25
  if (isToolErrorShape(e)) {
@@ -60,7 +55,7 @@ function throwToolError(code, message) {
60
55
  err.retryable = false;
61
56
  throw err;
62
57
  }
63
- function synthesizeFromRecordingInner(tx, opts, ozConfig) {
58
+ function synthesizeFromRecordingInner(tx, opts) {
64
59
  // 0. validate inputs (fail closed - never synthesize from garbage).
65
60
  const invalid = validateOptions(opts);
66
61
  if (invalid)
@@ -152,7 +147,6 @@ function synthesizeFromRecordingInner(tx, opts, ozConfig) {
152
147
  const topLevel = tx.invocations[0] ?? null;
153
148
  const composeOpts = {
154
149
  network: opts.network,
155
- interpreterEnabled: opts.interpreter !== undefined,
156
150
  ...(opts.userResponses !== undefined ? { userResponses: opts.userResponses } : {}),
157
151
  };
158
152
  const composed = composeFromRecording(facts, scope.contract, topLevel, composeOpts);
@@ -160,37 +154,17 @@ function synthesizeFromRecordingInner(tx, opts, ozConfig) {
160
154
  // self-verify verdict (built from the SAME runHarness + evaluate that gated
161
155
  // the synthesis, not a parallel simulation).
162
156
  let explain = null;
163
- let explainSim = null;
164
- // 5. OZ compile (always runs).
165
- const ozAdapter = createOzAdapter(ozConfig);
166
- const compileRes = ozAdapter.compile(composed.ir);
167
- if (!compileRes.proposed) {
168
- return {
169
- ok: false,
170
- error: {
171
- code: 'SYNTHESIS_ERROR',
172
- message: `recording lowered to no installable OZ policy: ${compileRes.uncovered.join('; ')}`,
173
- severity: 'error',
174
- retryable: false,
175
- details: { uncovered: compileRes.uncovered },
176
- },
177
- };
178
- }
179
157
  // 6. Interpreter compile (opt-in; fail-closed when the user routed real
180
158
  // restrictions to the interpreter).
181
159
  const interpreterOpts = opts.interpreter;
182
160
  let interpreterPolicyDocument = null;
183
161
  let interpreterPolicyRef = null;
184
- // Cross-layer L3: declared OUTSIDE the `if (interpreterOpts)` block so the
185
- // warnings folded into `proposed.warnings[]` (which lives after that block)
186
- // can read it. The block assigns it; the default is empty.
187
- let permitCtxWarnings = [];
162
+ let interpreterContextRule = null;
188
163
  if (interpreterOpts) {
189
164
  const interpreterConfig = {
190
165
  network: opts.network,
191
166
  installNonce: interpreterOpts.installNonce ?? 1,
192
167
  smartAccountAddress: interpreterOpts.smartAccountAddress,
193
- ...(interpreterOpts.oracleParams ? { oracleParams: interpreterOpts.oracleParams } : {}),
194
168
  };
195
169
  let startingPredicate = null;
196
170
  // `__testPredicateNode` is a test-only seam. It is NOT in the public
@@ -207,23 +181,19 @@ function synthesizeFromRecordingInner(tx, opts, ozConfig) {
207
181
  startingPredicate = testSeam;
208
182
  }
209
183
  else {
210
- const interpreterAdapter = createInterpreterAdapter(interpreterConfig);
211
184
  let interpreterRes;
212
185
  try {
213
- interpreterRes = interpreterAdapter.compile(composed.interpreterIr);
186
+ interpreterRes = compileInterpreterPolicy(composed.interpreterRule, interpreterConfig);
214
187
  }
215
188
  catch (e) {
216
189
  const code = e.code;
217
190
  const allowedCodes = [
218
191
  'SCOPE_SELF_CALL',
219
- 'ORACLE_LEAF_INVALID_POSITION',
220
- 'ORACLE_PARAMS_OUT_OF_RANGE',
221
192
  'MALFORMED_PREDICATE',
222
193
  'PREDICATE_TOO_LARGE',
223
194
  'PREDICATE_TOO_DEEP',
224
195
  'TOO_MANY_LEAVES',
225
196
  'IN_OPERAND_LIMIT',
226
- 'PREDICATE_ORACLE_OVER_LIMIT',
227
197
  ];
228
198
  const surfacedCode = code && allowedCodes.includes(code) ? code : 'SYNTHESIS_ERROR';
229
199
  return {
@@ -261,6 +231,7 @@ function synthesizeFromRecordingInner(tx, opts, ozConfig) {
261
231
  }
262
232
  interpreterPolicyDocument = interpreterRes.proposed.policyDocuments[0] ?? null;
263
233
  interpreterPolicyRef = interpreterRes.proposed.policyRefs[0] ?? null;
234
+ interpreterContextRule = interpreterRes.proposed.contextRule;
264
235
  if (!interpreterPolicyDocument || !interpreterPolicyRef) {
265
236
  return {
266
237
  ok: false,
@@ -272,7 +243,7 @@ function synthesizeFromRecordingInner(tx, opts, ozConfig) {
272
243
  },
273
244
  };
274
245
  }
275
- const firstInterpreterRule = composed.interpreterIr.rules[0];
246
+ const firstInterpreterRule = composed.interpreterRule;
276
247
  if (!firstInterpreterRule) {
277
248
  return {
278
249
  ok: false,
@@ -297,7 +268,9 @@ function synthesizeFromRecordingInner(tx, opts, ozConfig) {
297
268
  },
298
269
  };
299
270
  }
300
- // 6b. Self-verify + minimise.
271
+ // 6b. The predicate is used as compiled. There is no minimisation pass:
272
+ // nothing strips a leaf and re-checks that the reduced predicate still
273
+ // denies, so the result is NOT a proven-minimal policy.
301
274
  if (!topLevel) {
302
275
  return {
303
276
  ok: false,
@@ -320,81 +293,21 @@ function synthesizeFromRecordingInner(tx, opts, ozConfig) {
320
293
  },
321
294
  };
322
295
  }
323
- // Cross-layer L3: warnings collected from `buildPermitContext` (currently
324
- // only the oracle-on-right normaliser) and folded into the proposed
325
- // policy's `warnings[]` so the caller sees them on the success envelope.
326
- const permitCtxResult = buildPermitContext(tx, scope, topLevel, opts.userResponses, startingPredicate);
327
- const permitCtx = permitCtxResult.ctx;
328
- permitCtxWarnings = permitCtxResult.warnings;
329
- const finalPredicate = startingPredicate.op === 'and'
330
- ? minimize(startingPredicate, permitCtx, ORIGINAL_DIMENSIONS)
331
- : startingPredicate;
332
- const harnessCases = generateCases(finalPredicate, permitCtx, ORIGINAL_DIMENSIONS);
333
- const harnessResult = runHarness(finalPredicate, harnessCases);
334
- if (!harnessResult.ok) {
335
- return {
336
- ok: false,
337
- error: {
338
- code: 'DENY_CASE_FAILURE',
339
- message: `self-verify harness failed for the interpreter predicate (${harnessResult.failures.length} failure(s))`,
340
- severity: 'error',
341
- retryable: false,
342
- details: { failures: harnessResult.failures },
343
- },
344
- };
345
- }
346
- const evalResult = evaluate(finalPredicate, permitCtx);
347
- if (!evalResult.permit) {
348
- return {
349
- ok: false,
350
- error: {
351
- code: 'DENY_CASE_FAILURE',
352
- message: `intended recorded call was denied by the interpreter predicate: ${evalResult.reason}`,
353
- severity: 'error',
354
- retryable: false,
355
- details: { reason: evalResult.reason },
356
- },
357
- };
358
- }
359
- // --explain capture: quote the SAME verdict that gated the synthesis.
360
- // Re-evaluate each deny case to surface its concrete reason (the harness
361
- // only records whether the got-matches-expected boundary held).
296
+ const finalPredicate = startingPredicate;
362
297
  if (opts.explain) {
363
298
  explain = finalPredicate;
364
- const evaluatedCases = [
365
- {
366
- dimension: 'permit',
367
- outcome: evalResult.permit ? 'permit' : 'deny',
368
- reason: 'matches recorded call',
369
- },
370
- ];
371
- for (const deny of harnessCases.denies) {
372
- const r = evaluate(finalPredicate, deny.ctx);
373
- evaluatedCases.push({
374
- dimension: deny.dimension,
375
- outcome: r.permit ? 'permit' : 'deny',
376
- reason: r.permit ? 'no matching deny' : r.reason,
377
- });
378
- }
379
- explainSim = {
380
- permit: { tx: 'permit' },
381
- evaluatedCases,
382
- backend: 'ts-model',
383
- simulatorVersion: 'ts-model-1.0.0',
384
- };
385
299
  }
386
- // 6c. Re-encode the (possibly minimised) PredicateNode and stamp the
387
- // canonical bytes back onto the PolicyDocument + PolicyRef. Cap breaches
300
+ // 6c. Encode the PredicateNode and stamp the canonical bytes back onto
301
+ // the PolicyDocument + PolicyRef. Cap breaches
388
302
  // (PREDICATE_TOO_DEEP, TOO_MANY_LEAVES) throw ToolError-shaped errors;
389
303
  // the outer envelope converts them to structured `{ok:false, error}`.
390
304
  const { encodedPredicate, predicateHash } = encodePredicate(finalPredicate);
391
305
  if (testSeam !== undefined) {
392
306
  interpreterPolicyDocument = {
393
- grammarVersion: 1,
307
+ grammarVersion: GRAMMAR_VERSION,
394
308
  installNonce: interpreterOpts.installNonce ?? 1,
395
309
  encodedPredicate,
396
310
  predicateHash,
397
- ...(interpreterOpts.oracleParams ? { oracleParams: interpreterOpts.oracleParams } : {}),
398
311
  };
399
312
  interpreterPolicyRef = {
400
313
  kind: 'interpreter',
@@ -427,20 +340,23 @@ function synthesizeFromRecordingInner(tx, opts, ozConfig) {
427
340
  }
428
341
  }
429
342
  }
430
- // When the interpreter succeeds, OZ-side `uncovered` warnings that the
431
- // interpreter actually lowered are misleading: OZ really did not lower them,
432
- // but the interpreter did. Drop those so the user-facing warnings reflect
433
- // what is still UN-enforced, not what OZ alone could not do.
434
- const ozUncovered = interpreterPolicyRef
435
- ? compileRes.uncovered.filter((u) => !INTERPRETER_COVERED_OZ_PATTERN.test(u))
436
- : compileRes.uncovered;
437
- // 7. Merge into the OZ-shaped ProposedPolicy.
438
- const ozRefs = compileRes.proposed.policyRefs;
439
- const mergedRefs = [];
440
- if (interpreterPolicyRef)
441
- mergedRefs.push(interpreterPolicyRef);
442
- for (const r of ozRefs)
443
- mergedRefs.push(r);
343
+ // 7. Assemble the ProposedPolicy. The interpreter is the only backend, so
344
+ // its context rule and policy ref are the whole policy. Without interpreter
345
+ // options nothing installable was produced, which is an error rather than
346
+ // an empty-but-successful policy.
347
+ if (!interpreterContextRule || !interpreterPolicyRef) {
348
+ return {
349
+ ok: false,
350
+ error: {
351
+ code: 'SYNTHESIS_ERROR',
352
+ message: 'no installable policy was synthesised: supply `interpreter` options (a smart account address) so the recording can be lowered to an interpreter predicate',
353
+ severity: 'error',
354
+ retryable: false,
355
+ details: { uncovered: composed.warnings },
356
+ },
357
+ };
358
+ }
359
+ const mergedRefs = [interpreterPolicyRef];
444
360
  if (mergedRefs.length > OZ_LIMITS.maxPoliciesPerRule) {
445
361
  return {
446
362
  ok: false,
@@ -453,7 +369,7 @@ function synthesizeFromRecordingInner(tx, opts, ozConfig) {
453
369
  };
454
370
  }
455
371
  const mergedContextRule = {
456
- ...compileRes.proposed.contextRule,
372
+ ...interpreterContextRule,
457
373
  policies: mergedRefs,
458
374
  };
459
375
  // When nothing installable was synthesised (no interpreter doc AND no OZ
@@ -470,12 +386,7 @@ function synthesizeFromRecordingInner(tx, opts, ozConfig) {
470
386
  policyDocuments: interpreterPolicyDocument ? [interpreterPolicyDocument] : [],
471
387
  policyRefs: mergedRefs,
472
388
  parseConfidence: { ...tx.parseConfidence },
473
- warnings: [
474
- ...zeroPolicyWarning,
475
- ...ozUncovered.map((u) => `${UNCOVERED_PREFIX}${u}`),
476
- ...composed.warnings.map((w) => `${UNCOVERED_PREFIX}${w}`),
477
- ...permitCtxWarnings,
478
- ],
389
+ warnings: [...zeroPolicyWarning, ...composed.warnings.map((w) => `${UNCOVERED_PREFIX}${w}`)],
479
390
  ambiguities: mergeAmbiguities(composed.ambiguities, scope.ambiguities),
480
391
  };
481
392
  // --explain success envelope. When opts.explain is set and the OZ-only
@@ -484,41 +395,11 @@ function synthesizeFromRecordingInner(tx, opts, ozConfig) {
484
395
  // permit is deny with a truthful reason and evaluatedCases is empty.
485
396
  const envelope = { ok: true, data: proposed };
486
397
  if (opts.explain) {
487
- if (explainSim) {
488
- envelope.explain = { predicateTree: explain, simulation: explainSim };
489
- }
490
- else {
491
- envelope.explain = {
492
- predicateTree: null,
493
- simulation: {
494
- permit: {
495
- tx: 'deny',
496
- reason: 'No self-verification was performed (interpreter adapter was not engaged)',
497
- },
498
- evaluatedCases: [],
499
- backend: 'ts-model',
500
- simulatorVersion: 'not-run',
501
- },
502
- };
503
- }
398
+ envelope.explain = { predicateTree: explain };
504
399
  }
505
400
  return envelope;
506
401
  }
507
402
  export { throwToolError };
508
- /** OZ-side `uncovered` warning patterns the interpreter adapter actually lowers
509
- * when wired in. When the interpreter adapter succeeds, matching entries are
510
- * dropped from the OZ uncovered list so user-facing warnings reflect what is
511
- * still UN-enforced rather than what OZ alone could not do. Matches the
512
- * exact descriptor strings the OZ adapter emits (see `src/adapters/oz/adapter.ts`
513
- * `describeCondition` / `describeSelector`). */
514
- const INTERPRETER_COVERED_OZ_PATTERN = /^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/;
515
- /** Reject non-sane inputs before any policy is synthesized. windowSeconds /
516
- * validUntilLedger / invocationLimit must be positive integers; limitAmount a
517
- * positive i128 decimal string; network mainnet|testnet. When the interpreter
518
- * adapter is opted in, `smartAccountAddress` must be a C... contract address
519
- * (the on-chain policy-bound account, NOT the G... source account),
520
- * `installNonce` must fit u32 (default 1), and `oracleParams` must
521
- * tighten-only vs the wasm defaults. */
522
403
  function validateOptions(opts) {
523
404
  if (opts.network !== 'mainnet' && opts.network !== 'testnet') {
524
405
  return synthesisError(`network must be 'mainnet' or 'testnet', got: ${String(opts.network)}`);
@@ -531,16 +412,10 @@ function validateOptions(opts) {
531
412
  }
532
413
  const ur = opts.userResponses;
533
414
  if (ur) {
534
- if (ur.windowSeconds !== undefined && !isPositiveInt(ur.windowSeconds)) {
535
- return synthesisError(`windowSeconds must be a positive integer, got: ${ur.windowSeconds}`);
536
- }
537
415
  if (ur.validUntilLedger !== undefined &&
538
416
  (!isPositiveInt(ur.validUntilLedger) || ur.validUntilLedger > SOROBAN_LIMITS.u32Max)) {
539
417
  return synthesisError(`validUntilLedger must be a positive u32 ledger sequence (<= ${SOROBAN_LIMITS.u32Max}), got: ${ur.validUntilLedger}`);
540
418
  }
541
- if (ur.invocationLimit !== undefined && !isPositiveInt(ur.invocationLimit)) {
542
- return synthesisError(`invocationLimit must be a positive integer, got: ${ur.invocationLimit}`);
543
- }
544
419
  if (ur.limitAmount !== undefined && !isPositiveI128(ur.limitAmount)) {
545
420
  return synthesisError(`limitAmount must be a positive i128 decimal string within [1, ${I128_MAX}] (2^127-1), got: ${ur.limitAmount}`);
546
421
  }
@@ -564,19 +439,6 @@ function validateOptions(opts) {
564
439
  if (nonce !== undefined && (!isPositiveInt(nonce) || nonce > SOROBAN_LIMITS.u32Max)) {
565
440
  return synthesisError(`interpreter.installNonce must be a positive u32 integer (<= ${SOROBAN_LIMITS.u32Max}), got: ${nonce}`);
566
441
  }
567
- const op = opts.interpreter.oracleParams;
568
- if (op) {
569
- const MAX_STALE = 600;
570
- const MAX_DEV = 200;
571
- if (op.maxStalenessSeconds !== undefined &&
572
- (!isPositiveInt(op.maxStalenessSeconds) || op.maxStalenessSeconds > MAX_STALE)) {
573
- return synthesisError(`interpreter.oracleParams.maxStalenessSeconds must be a positive integer <= ${MAX_STALE} (tighten-only), got: ${op.maxStalenessSeconds}`);
574
- }
575
- if (op.maxDeviationBps !== undefined &&
576
- (!isPositiveInt(op.maxDeviationBps) || op.maxDeviationBps > MAX_DEV)) {
577
- return synthesisError(`interpreter.oracleParams.maxDeviationBps must be a positive integer <= ${MAX_DEV} (tighten-only), got: ${op.maxDeviationBps}`);
578
- }
579
- }
580
442
  }
581
443
  return null;
582
444
  }
@@ -630,180 +492,3 @@ function mergeAmbiguities(...lists) {
630
492
  }
631
493
  return out;
632
494
  }
633
- /** Build the permit `EvalContext` the self-verify harness drives. Shape
634
- * mirrors the intended recorded call so:
635
- * - `evaluate(predicate, ctx).permit === true` must hold (a failure
636
- * surfaces as DENY_CASE_FAILURE).
637
- * - `generateCases(predicate, ctx)` produces a deny battery that reflects
638
- * the actual recorded move (real amount, args, window start).
639
- * Amounts are summed per-token over all movements (BigInt, never lossy).
640
- * `oraclePriceByAsset` contains a price+timestamp satisfying each
641
- * `oracle_price` leaf so the intended call permits under every bound;
642
- * the harness mutates those entries (stale / missing / deviation / paused)
643
- * to exercise the ORACLE_* deny paths. */
644
- function buildPermitContext(tx, scope, topLevel, userResponses, predicate) {
645
- const amountByToken = {};
646
- const totals = new Map();
647
- for (const m of tx.tokenMovements) {
648
- const current = totals.get(m.token) ?? 0n;
649
- totals.set(m.token, current + BigInt(m.amount));
650
- }
651
- for (const [token, total] of totals) {
652
- amountByToken[token] = total.toString();
653
- }
654
- const warnings = [];
655
- const oraclePriceByAsset = oracleSatisfyingPrices(predicate, tx.fetchedAt, warnings);
656
- const ctx = {
657
- contract: scope.contract,
658
- fn: topLevel.fn,
659
- args: topLevel.args.map(cloneScVal),
660
- atLedger: tx.ledgerSequence,
661
- nowSeconds: tx.fetchedAt,
662
- amountByToken,
663
- windowSpentByToken: {},
664
- invocationCountByWindow: {},
665
- oraclePriceByAsset,
666
- };
667
- if (userResponses?.validUntilLedger !== undefined) {
668
- ctx.validUntilLedger = userResponses.validUntilLedger;
669
- }
670
- return { ctx, warnings };
671
- }
672
- function cloneScVal(value, depth = 0) {
673
- // Clone top-level shells so the harness can mutate deny cases without
674
- // aliasing the recorded call. Recursion bounded by MAX_SCVAL_CLONE_DEPTH so
675
- // a hand-crafted nested-vec cannot RangeError the JS stack; the over-depth
676
- // branch throws a ToolError-shaped error the envelope converts to
677
- // `{ok:false, error}`.
678
- if (value.type === 'vec') {
679
- if (depth >= MAX_SCVAL_CLONE_DEPTH) {
680
- throw cloneDepthError(value);
681
- }
682
- return {
683
- type: 'vec',
684
- value: value.value.map((v) => cloneScVal(v, depth + 1)),
685
- };
686
- }
687
- return { ...value };
688
- }
689
- function cloneDepthError(value) {
690
- const err = new Error(`ScVal clone depth exceeds MAX_SCVAL_CLONE_DEPTH (${MAX_SCVAL_CLONE_DEPTH})`);
691
- err.code = 'SYNTHESIS_ERROR';
692
- err.severity = 'error';
693
- err.retryable = false;
694
- err.depthContext = value.type;
695
- throw err;
696
- }
697
- /** Walk every `oracle_price` leaf and return a price map whose entries satisfy
698
- * the bound. Timestamp pinned to `nowSeconds` (the recorded `fetchedAt`) so
699
- * the fresh-oracle deny case in `generateCases` is the only path that flips
700
- * this map. Negatives clamped at 0 - oracle prices are non-negative on Stellar. */
701
- function oracleSatisfyingPrices(predicate, nowSeconds, warnings) {
702
- const out = {};
703
- visitOracleLeaves(predicate, (asset, op, bound) => {
704
- let price;
705
- switch (op) {
706
- case 'lt':
707
- case 'gt':
708
- price = op === 'lt' ? bound - 1n : bound + 1n;
709
- break;
710
- case 'lte':
711
- case 'gte':
712
- case 'eq':
713
- price = bound;
714
- break;
715
- }
716
- if (price < 0n)
717
- price = 0n;
718
- out[asset] = { price: price.toString(), timestampSeconds: nowSeconds };
719
- }, (warning) => {
720
- // Cross-layer L3: oracle-on-right that cannot be normalised is
721
- // surfaced as a warning, not silently dropped. The Rust interpreter
722
- // would surface this as `UnsupportedNode`; the warning lets the
723
- // caller decide whether to fix the predicate shape or accept the
724
- // over-permissive hole.
725
- warnings.push(warning.message);
726
- });
727
- return out;
728
- }
729
- function visitOracleLeaves(node, visit, onWarning) {
730
- switch (node.op) {
731
- case 'and':
732
- case 'or':
733
- for (const child of node.children)
734
- visitOracleLeaves(child, visit, onWarning);
735
- return;
736
- case 'not':
737
- visitOracleLeaves(node.child, visit, onWarning);
738
- return;
739
- case 'eq':
740
- case 'lt':
741
- case 'lte':
742
- case 'gt':
743
- case 'gte': {
744
- const leftLeaf = node.left;
745
- const rightLeaf = node.right;
746
- const leftIsOracle = leftLeaf.kind === 'oracle_price';
747
- const rightIsOracle = rightLeaf.kind === 'oracle_price';
748
- let oracleAsset;
749
- let literal;
750
- if (leftIsOracle) {
751
- oracleAsset = leftLeaf.asset;
752
- literal = oracleLiteralFromLeaf(rightLeaf);
753
- }
754
- else if (rightIsOracle) {
755
- oracleAsset = rightLeaf.asset;
756
- literal = oracleLiteralFromLeaf(leftLeaf);
757
- }
758
- if (oracleAsset === undefined || literal === undefined) {
759
- // Two reasons the case cannot be normalised:
760
- // - neither side is an oracle leaf (visitor has nothing to do)
761
- // - oracle-on-right with a non-literal LHS (Rust dispatch would
762
- // hit UnsupportedNode; the TS model cannot build a permit ctx
763
- // for it)
764
- // Only the second is a meaningful warning; the first is a no-op
765
- // (the visitor was called for a non-oracle comparison). Emit the
766
- // warning when an oracle IS present on one side but the other
767
- // side is not a parseable threshold literal.
768
- if (rightIsOracle) {
769
- onWarning?.({
770
- dimension: 'oracle_normalisation_dropped',
771
- op: node.op,
772
- asset: rightLeaf.kind === 'oracle_price' ? rightLeaf.asset : '',
773
- otherKind: leftLeaf.kind,
774
- 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`,
775
- });
776
- }
777
- return;
778
- }
779
- visit(oracleAsset, node.op, literal);
780
- return;
781
- }
782
- case 'in':
783
- // `in` is pure membership; oracle leaves inside haystacks are
784
- // forbidden by the position rule, so there's nothing to set up here.
785
- return;
786
- }
787
- }
788
- /** Restate an oracle threshold on the normalised 9-dp basis prices use.
789
- * Thresholds carry their own basis, so reading the digits raw would build
790
- * a permit context off by 10^(decimals-9) and the intended call would not
791
- * satisfy its own bound. Mirrors NORMALISED_DECIMALS in oracle.rs. */
792
- function oracleLiteralFromLeaf(leaf) {
793
- if (leaf.kind !== 'oracle_threshold')
794
- return undefined;
795
- let value;
796
- try {
797
- value = BigInt(leaf.value);
798
- }
799
- catch {
800
- return undefined;
801
- }
802
- const normalised = 9n;
803
- const decimals = BigInt(leaf.decimals);
804
- if (decimals <= normalised)
805
- return value * 10n ** (normalised - decimals);
806
- // Floor: a finer-grained threshold has no exact 9-dp representation; the
807
- // caller offsets by one from here to land on the right side of the bound.
808
- return value / 10n ** (decimals - normalised);
809
- }