@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
@@ -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
- }