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