@directive-run/core 1.21.0 → 1.23.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 (86) hide show
  1. package/README.md +2 -2
  2. package/dist/adapter-utils.cjs +1 -1
  3. package/dist/adapter-utils.d.cts +1 -1
  4. package/dist/adapter-utils.d.ts +1 -1
  5. package/dist/adapter-utils.js +1 -1
  6. package/dist/chunk-AFPLATPV.js +16 -0
  7. package/dist/chunk-AFPLATPV.js.map +1 -0
  8. package/dist/{chunk-4OXCKDKS.cjs → chunk-FCCJUQB2.cjs} +3 -3
  9. package/dist/chunk-FCCJUQB2.cjs.map +1 -0
  10. package/dist/chunk-IZNOPHPL.js +3 -0
  11. package/dist/chunk-IZNOPHPL.js.map +1 -0
  12. package/dist/chunk-LN5H74CU.js +2 -0
  13. package/dist/{chunk-RBF653NR.js.map → chunk-LN5H74CU.js.map} +1 -1
  14. package/dist/chunk-PJIIBQTK.js +2 -0
  15. package/dist/chunk-PJIIBQTK.js.map +1 -0
  16. package/dist/chunk-PTT4MWNL.cjs +3 -0
  17. package/dist/chunk-PTT4MWNL.cjs.map +1 -0
  18. package/dist/{chunk-GKMJ7NMP.cjs → chunk-RD2HO5NZ.cjs} +2 -2
  19. package/dist/{chunk-GKMJ7NMP.cjs.map → chunk-RD2HO5NZ.cjs.map} +1 -1
  20. package/dist/{chunk-GACC2DMS.cjs → chunk-S6W6Y44G.cjs} +2 -2
  21. package/dist/{chunk-GACC2DMS.cjs.map → chunk-S6W6Y44G.cjs.map} +1 -1
  22. package/dist/{chunk-6PF2FRBG.js → chunk-WD47DB52.js} +2 -2
  23. package/dist/{chunk-6PF2FRBG.js.map → chunk-WD47DB52.js.map} +1 -1
  24. package/dist/chunk-WKGNK4QK.cjs +2 -0
  25. package/dist/chunk-WKGNK4QK.cjs.map +1 -0
  26. package/dist/chunk-WVP3GOY2.js +3 -0
  27. package/dist/chunk-WVP3GOY2.js.map +1 -0
  28. package/dist/chunk-XKXTRVJF.cjs +3 -0
  29. package/dist/chunk-XKXTRVJF.cjs.map +1 -0
  30. package/dist/{index-CvQuu0tu.d.cts → index-DpvgHVHR.d.cts} +29 -2
  31. package/dist/{index-DUbpbGw8.d.ts → index-n8oQmdOZ.d.ts} +29 -2
  32. package/dist/index.cjs +2 -2
  33. package/dist/index.cjs.map +1 -1
  34. package/dist/index.d.cts +46 -164
  35. package/dist/index.d.ts +46 -164
  36. package/dist/index.js +2 -2
  37. package/dist/index.js.map +1 -1
  38. package/dist/internals.cjs +1 -1
  39. package/dist/internals.d.cts +45 -25
  40. package/dist/internals.d.ts +45 -25
  41. package/dist/internals.js +1 -1
  42. package/dist/plugins/index.cjs +2 -2
  43. package/dist/plugins/index.cjs.map +1 -1
  44. package/dist/plugins/index.d.cts +358 -3
  45. package/dist/plugins/index.d.ts +358 -3
  46. package/dist/plugins/index.js +2 -2
  47. package/dist/plugins/index.js.map +1 -1
  48. package/dist/{plugins-4IfhJV32.d.cts → plugins-CaPMTICa.d.cts} +400 -31
  49. package/dist/{plugins-4IfhJV32.d.ts → plugins-CaPMTICa.d.ts} +400 -31
  50. package/dist/{predicate-DBTPnlg6.d.cts → predicate-CU0YC1i6.d.cts} +1 -1
  51. package/dist/{predicate-BW05x5el.d.ts → predicate-CgpwJqf4.d.ts} +1 -1
  52. package/dist/system-HOP37Z4V.js +2 -0
  53. package/dist/{system-SJBP4TO5.js.map → system-HOP37Z4V.js.map} +1 -1
  54. package/dist/system-VZUHL56W.cjs +2 -0
  55. package/dist/{system-SB7JYMRB.cjs.map → system-VZUHL56W.cjs.map} +1 -1
  56. package/dist/testing.cjs +1 -1
  57. package/dist/testing.cjs.map +1 -1
  58. package/dist/testing.d.cts +1 -1
  59. package/dist/testing.d.ts +1 -1
  60. package/dist/testing.js +1 -1
  61. package/dist/testing.js.map +1 -1
  62. package/dist/worker.cjs +1 -1
  63. package/dist/worker.cjs.map +1 -1
  64. package/dist/worker.d.cts +1 -1
  65. package/dist/worker.d.ts +1 -1
  66. package/dist/worker.js +1 -1
  67. package/dist/worker.js.map +1 -1
  68. package/package.json +1 -1
  69. package/dist/chunk-4OXCKDKS.cjs.map +0 -1
  70. package/dist/chunk-6QAUJFQH.js +0 -3
  71. package/dist/chunk-6QAUJFQH.js.map +0 -1
  72. package/dist/chunk-E53A4NHM.js +0 -16
  73. package/dist/chunk-E53A4NHM.js.map +0 -1
  74. package/dist/chunk-GBCWXTXW.cjs +0 -3
  75. package/dist/chunk-GBCWXTXW.cjs.map +0 -1
  76. package/dist/chunk-KGTBTGOY.js +0 -2
  77. package/dist/chunk-KGTBTGOY.js.map +0 -1
  78. package/dist/chunk-M5PEB553.cjs +0 -3
  79. package/dist/chunk-M5PEB553.cjs.map +0 -1
  80. package/dist/chunk-MARZI3EY.cjs +0 -2
  81. package/dist/chunk-MARZI3EY.cjs.map +0 -1
  82. package/dist/chunk-RBF653NR.js +0 -2
  83. package/dist/chunk-SFUVPP4L.js +0 -3
  84. package/dist/chunk-SFUVPP4L.js.map +0 -1
  85. package/dist/system-SB7JYMRB.cjs +0 -2
  86. package/dist/system-SJBP4TO5.js +0 -2
@@ -568,26 +568,29 @@ interface ConstraintDef<S extends Schema, R extends Requirement = Requirement> {
568
568
  /** Timeout for async constraints (ms) */
569
569
  timeout?: number;
570
570
  /**
571
- * Fact keys whose **value the resolver compare-and-swaps**. Writes to
572
- * these facts land only if they still hold the snapshot value taken at
573
- * resolver dispatch; otherwise the write is dropped and the resolver
574
- * aborted.
571
+ * Fact keys this resolver **aborts on** when they change mid-flight.
572
+ * The engine snapshots their values at resolver dispatch; if any of
573
+ * them changes before the resolver writes, the resolver's writes are
574
+ * dropped and the resolver aborted.
575
575
  *
576
- * This is value-based per-fact compare-and-swap with one-shot
577
- * fact-level poisoning not a lock, not document versioning. Writes
578
- * to facts not listed always land; `when()` is not consulted. Omit for
579
- * no binding (default). Ignored on async constraints.
576
+ * Reads aloud as "abort on changes to these facts." This is the
577
+ * opposite shape of a lock the resolver yields to whoever wrote
578
+ * first; it does NOT prevent other writers. Value-based per-fact
579
+ * compare-and-swap with one-shot fact-level poisoning, not document
580
+ * versioning. Writes to facts not listed always land; `when()` is not
581
+ * consulted. Omit for no binding (default). Ignored on async
582
+ * constraints.
580
583
  *
581
584
  * @example
582
585
  * ```ts
583
586
  * executeAction: {
584
587
  * when: (f) => f.status === 'mutating',
585
588
  * require: { type: 'EXECUTE_ACTION' },
586
- * owns: ['status'], // the resolver owns `status`
589
+ * abortOn: ['status'], // abort if `status` changes mid-flight
587
590
  * }
588
591
  * ```
589
592
  */
590
- owns?: readonly string[];
593
+ abortOn?: readonly string[];
591
594
  /**
592
595
  * Constraint IDs whose resolvers must complete before this constraint is evaluated.
593
596
  * If a dependency's `when()` returns false (no requirements), this constraint proceeds.
@@ -1072,6 +1075,60 @@ type EventsDef<S extends Schema> = Record<string, FlexibleEventHandler<S>>;
1072
1075
  type JitterStrategy = "none" | "full" | "equal" | {
1073
1076
  maxMs: number;
1074
1077
  };
1078
+ /**
1079
+ * Why a resolver attempt was aborted, surfaced to {@link RetryPolicy.shouldRetry}
1080
+ * so the policy can distinguish "retry on race-loss" from "fail loud on bug."
1081
+ *
1082
+ * - `"clobbered"` — an `abortOn`-listed fact was written by something
1083
+ * outside the resolver between the resolver's baseline read and its
1084
+ * next write (RFC 0003 optimistic-concurrency check). The retry is
1085
+ * typically safe — the resolver re-reads the current value and tries
1086
+ * again with fresh context.
1087
+ * - `"timeout"` — the resolver's `timeout` ms elapsed. Retrying may
1088
+ * succeed if the underlying I/O was just slow; may fail again if the
1089
+ * timeout is set too tight for the workload.
1090
+ * - `"cancelled"` — `system.stop()` was called, or external code aborted
1091
+ * the resolver's `ctx.signal`. Retrying is almost always wrong — the
1092
+ * cancellation was the caller's explicit intent.
1093
+ * - `"error"` — the resolver threw. Default for back-compat: any abort
1094
+ * without a more specific reason maps here.
1095
+ *
1096
+ * Core has no supersession concept today; if a future package wants to
1097
+ * surface supersession, it can widen the union additively without
1098
+ * breaking back-compat.
1099
+ *
1100
+ * @public
1101
+ */
1102
+ type ResolverAbortReason = "clobbered" | "timeout" | "cancelled" | "error";
1103
+ /**
1104
+ * Context object passed as the optional third argument to
1105
+ * {@link RetryPolicy.shouldRetry}. Lets the policy decide based on WHY
1106
+ * the attempt failed — not just the thrown error.
1107
+ *
1108
+ * Two-argument `shouldRetry(error, attempt)` continues to work; the
1109
+ * context argument is additive. Existing callers see no behavior change.
1110
+ *
1111
+ * @public
1112
+ */
1113
+ interface ShouldRetryContext {
1114
+ /**
1115
+ * Why the attempt was aborted. Undefined when the engine couldn't
1116
+ * classify the cause (very rare — defaults to `"error"` in practice).
1117
+ */
1118
+ reason?: ResolverAbortReason;
1119
+ /**
1120
+ * Populated only when `reason === "clobbered"`. Names the `abortOn`
1121
+ * fact whose mid-flight change tripped the binding, plus the expected
1122
+ * baseline value and the actual current value. Useful for
1123
+ * `shouldRetry: (_, _, ctx) => ctx?.clobber?.fact === "cart.discount"`
1124
+ * patterns that only retry contention on specific facts.
1125
+ */
1126
+ clobber?: {
1127
+ fact: string;
1128
+ expected: unknown;
1129
+ actual: unknown;
1130
+ };
1131
+ }
1075
1132
  /** Retry policy configuration */
1076
1133
  interface RetryPolicy {
1077
1134
  /** Maximum number of attempts */
@@ -1098,10 +1155,24 @@ interface RetryPolicy {
1098
1155
  * Return `true` to retry, `false` to stop immediately.
1099
1156
  * If omitted, all errors are retried (up to `attempts`).
1100
1157
  *
1158
+ * The optional third `context` argument carries the abort reason and,
1159
+ * for clobbers, the fact + expected/actual values. Existing two-arg
1160
+ * callers continue to work — the third arg is undefined when omitted.
1161
+ *
1162
+ * @example Retry on contention, fail loud on bug
1163
+ * ```ts
1164
+ * shouldRetry: (err, attempt, ctx) => {
1165
+ * if (ctx?.reason === "clobbered") return attempt < 5;
1166
+ * if (ctx?.reason === "timeout") return attempt < 2;
1167
+ * return false; // "error" / "cancelled" → don't retry
1168
+ * }
1169
+ * ```
1170
+ *
1101
1171
  * @param error - The error that occurred
1102
1172
  * @param attempt - The attempt number that just failed (1-based)
1173
+ * @param context - Optional abort context (added v1.23.0); undefined for callers that don't pass it
1103
1174
  */
1104
- shouldRetry?: (error: Error, attempt: number) => boolean;
1175
+ shouldRetry?: (error: Error, attempt: number, context?: ShouldRetryContext) => boolean;
1105
1176
  }
1106
1177
  /** Batch configuration */
1107
1178
  interface BatchConfig {
@@ -1312,6 +1383,128 @@ type ResolverStatus = {
1312
1383
  canceledAt: number;
1313
1384
  };
1314
1385
 
1386
+ /**
1387
+ * Structural diff between two snapshots of a system's constraint `whenSpec`
1388
+ * map. The "git diff for business rules" — operates on the predicate AST
1389
+ * instead of source-text lines.
1390
+ *
1391
+ * A predicate is a tree of leaf clauses (`{ fact: operator(value) }`) and
1392
+ * combinators (`$all` / `$any` / `$not`). `diffRules` flattens both trees
1393
+ * into path-keyed leaf lists and compares: missing path → added/removed,
1394
+ * same path with different operand → changed (with relaxed/tightened
1395
+ * classification for numeric thresholds).
1396
+ *
1397
+ * Pure module — imports only utils. No engine / store / predicate-runtime
1398
+ * dependency: predicates are walked as plain JSON.
1399
+ */
1400
+ /** A leaf clause extracted from a predicate tree, keyed by its dotted path. */
1401
+ interface LeafClause {
1402
+ /** Dotted path through the predicate. E.g. `phase`, `$all[0].elapsed`, `$not.paused`. */
1403
+ path: string;
1404
+ /** Operator name. `$eq` is implied for bare-value equality. */
1405
+ op: string;
1406
+ /** Operand. */
1407
+ value: unknown;
1408
+ }
1409
+ /** Kind of change observed for a single clause. */
1410
+ type ChangeKind = "added" | "removed" | "changed" | "relaxed" | "tightened";
1411
+ /** A single change between two predicates at a specific path. */
1412
+ interface Change {
1413
+ path: string;
1414
+ kind: ChangeKind;
1415
+ before?: {
1416
+ op: string;
1417
+ value: unknown;
1418
+ };
1419
+ after?: {
1420
+ op: string;
1421
+ value: unknown;
1422
+ };
1423
+ }
1424
+ /** Status of a single constraint across the two snapshots. */
1425
+ type ConstraintStatus = "added" | "removed" | "changed" | "unchanged";
1426
+ interface ConstraintDiff {
1427
+ id: string;
1428
+ status: ConstraintStatus;
1429
+ /** Empty for `unchanged`. For `added` / `removed`, every leaf is listed once. */
1430
+ changes: Change[];
1431
+ }
1432
+ interface RulesDiffReport {
1433
+ constraints: ConstraintDiff[];
1434
+ summary: {
1435
+ added: number;
1436
+ removed: number;
1437
+ changed: number;
1438
+ unchanged: number;
1439
+ totalClauseChanges: number;
1440
+ };
1441
+ }
1442
+ interface DiffRulesOptions {
1443
+ before: RulesMapInput;
1444
+ after: RulesMapInput;
1445
+ }
1446
+ /**
1447
+ * Accepted shapes for the `before` / `after` predicate maps:
1448
+ * - A plain map `{ [constraintId]: whenSpec }`
1449
+ * - The `system.inspect().constraints` array form `Array<{ id, whenSpec? }>`
1450
+ * - Either wrapped as `{ constraints: <one-of-the-above> }`
1451
+ *
1452
+ * Anything else throws at `toRulesMap` so a bad input fails loud.
1453
+ */
1454
+ type RulesMapInput = unknown;
1455
+ /**
1456
+ * Coerce supported input shapes into a flat `Record<constraintId, whenSpec>`.
1457
+ * Constraints without a `whenSpec` (function-form `when`) are dropped with
1458
+ * an undefined entry so callers can distinguish them from absent constraints
1459
+ * if they care; the diff treats `undefined` as "no data" and skips clause
1460
+ * walking.
1461
+ */
1462
+ declare function toRulesMap(raw: RulesMapInput): Record<string, unknown>;
1463
+ /**
1464
+ * Walk a predicate tree and emit every leaf clause with its dotted path.
1465
+ * Combinators (`$all` / `$any` / `$not`) become indexed path segments.
1466
+ * Bare-value equality (`{ phase: "red" }`) emits as `op: "$eq"`.
1467
+ *
1468
+ * Array-form predicates (`[{ fact, op, value }, ...]`) are also accepted —
1469
+ * each clause's `fact` becomes the path, the operator is `op`, and the
1470
+ * value is `value`.
1471
+ *
1472
+ * **What gets dropped:** non-object combinator children (e.g. `$not: "red"`
1473
+ * with a string operand instead of a predicate object), function-form
1474
+ * predicates (the engine accepts a function for `when`; this walker only
1475
+ * sees data), and bare primitive predicates (a top-level `true` or `"x"`).
1476
+ * The walker is a tree visitor — anything not shaped like a predicate node
1477
+ * is silently skipped, never thrown on.
1478
+ */
1479
+ declare function flattenPredicate(spec: unknown, pathPrefix?: string, out?: LeafClause[]): LeafClause[];
1480
+ /**
1481
+ * Diff two snapshots of a system's constraint whenSpec map.
1482
+ *
1483
+ * @example
1484
+ * ```ts
1485
+ * const report = diffRules({
1486
+ * before: { blockCheckout: { cartTotal: { $gte: 100 } } },
1487
+ * after: { blockCheckout: { cartTotal: { $gte: 50 } } },
1488
+ * });
1489
+ *
1490
+ * report.constraints[0].status; // "changed"
1491
+ * report.constraints[0].changes[0].kind; // "relaxed"
1492
+ * report.summary.totalClauseChanges; // 1
1493
+ * ```
1494
+ *
1495
+ * The input shape is forgiving — either a flat `{ id: whenSpec }` map, the
1496
+ * `system.inspect().constraints` array form, or either wrapped as
1497
+ * `{ constraints: ... }`. Constraints whose `when` is a function (no
1498
+ * `whenSpec`) are tracked as added/removed but cannot be clause-diffed
1499
+ * (the function form is opaque).
1500
+ */
1501
+ declare function diffRules(options: DiffRulesOptions): RulesDiffReport;
1502
+ /**
1503
+ * Diff two predicate trees and return the list of leaf-level changes.
1504
+ * Exported for reuse beyond the full per-constraint report.
1505
+ */
1506
+ declare function diffClauses(before: unknown, after: unknown): Change[];
1507
+
1315
1508
  /**
1316
1509
  * Error Types - Type definitions for error handling
1317
1510
  */
@@ -2172,12 +2365,13 @@ interface TypedConstraintDef<M extends ModuleSchema> {
2172
2365
  /** Timeout for async constraints (ms) */
2173
2366
  timeout?: number;
2174
2367
  /**
2175
- * Fact keys whose **value the resolver compare-and-swaps**. Writes to
2176
- * these facts land only if they still hold the snapshot value taken at
2177
- * resolver dispatch; otherwise the write is dropped and the resolver
2178
- * aborted. Omit for no binding (default). Ignored on async constraints.
2368
+ * Fact keys this resolver aborts on when they change mid-flight. The
2369
+ * engine snapshots their values at resolver dispatch; if any of them
2370
+ * changes before the resolver writes, the resolver's writes are
2371
+ * dropped and the resolver aborted. Omit for no binding (default).
2372
+ * Ignored on async constraints.
2179
2373
  */
2180
- owns?: readonly string[];
2374
+ abortOn?: readonly string[];
2181
2375
  /**
2182
2376
  * Constraint IDs whose resolvers must complete before this constraint is evaluated.
2183
2377
  * If a dependency's `when()` returns false (no requirements), this constraint proceeds.
@@ -2223,12 +2417,13 @@ interface CrossModuleConstraintDef<M extends ModuleSchema, Deps extends CrossMod
2223
2417
  /** Timeout for async constraints (ms) */
2224
2418
  timeout?: number;
2225
2419
  /**
2226
- * Fact keys whose **value the resolver compare-and-swaps**. Writes to
2227
- * these facts land only if they still hold the snapshot value taken at
2228
- * resolver dispatch; otherwise the write is dropped and the resolver
2229
- * aborted. Omit for no binding (default). Ignored on async constraints.
2420
+ * Fact keys this resolver aborts on when they change mid-flight. The
2421
+ * engine snapshots their values at resolver dispatch; if any of them
2422
+ * changes before the resolver writes, the resolver's writes are
2423
+ * dropped and the resolver aborted. Omit for no binding (default).
2424
+ * Ignored on async constraints.
2230
2425
  */
2231
- owns?: readonly string[];
2426
+ abortOn?: readonly string[];
2232
2427
  /**
2233
2428
  * Constraint IDs whose resolvers must complete before this constraint is evaluated.
2234
2429
  * If a dependency's `when()` returns false (no requirements), this constraint proceeds.
@@ -2644,16 +2839,17 @@ interface SystemInspection {
2644
2839
  */
2645
2840
  whenSpec?: FactPredicate<Record<string, unknown>>;
2646
2841
  /**
2647
- * Owned-fact list for RFC-0003 binding. Populated from the
2648
- * constraint definition's `owns:` field. Exposed for `doctor.checkOwns()`
2649
- * so it can flag candidates that would race or shadow these writes.
2650
- * Absent when the constraint declares no `owns`.
2842
+ * Abort-binding fact list for RFC-0003 constraint binding. Populated
2843
+ * from the constraint definition's `abortOn:` field. Exposed for
2844
+ * `doctor.checkAbortOn()` so it can flag candidates that would race
2845
+ * or shadow these writes. Absent when the constraint declares no
2846
+ * `abortOn`.
2651
2847
  */
2652
- readonly owns?: readonly string[];
2848
+ readonly abortOn?: readonly string[];
2653
2849
  /**
2654
2850
  * Fact paths the constraint `bind:`s to. v2 promise — the runtime
2655
2851
  * does not yet emit a `bind` field on inspect snapshots, but the
2656
- * type slot is reserved so `doctor.checkOwns()` is stable across
2852
+ * type slot is reserved so `doctor.checkAbortOn()` is stable across
2657
2853
  * the rollout. (F1)
2658
2854
  */
2659
2855
  readonly bind?: readonly string[];
@@ -3071,6 +3267,46 @@ interface MetaAccessor {
3071
3267
  /** Find all definitions matching a tag across all types. */
3072
3268
  byTag(tag: string): MetaMatch[];
3073
3269
  }
3270
+ /**
3271
+ * Discriminated proof of why two resolvers' `when:` predicates fire on
3272
+ * the same state — attached to `resolver.clobber.loop.detected` events
3273
+ * so the warning can point at the specific clauses that co-fire instead
3274
+ * of stopping at "these resolvers fight."
3275
+ *
3276
+ * - `matched` — both predicates have identical structural clauses.
3277
+ * The strongest verdict; the rules are syntactic duplicates.
3278
+ * - `overlap` — clauses share at least one path and at least one
3279
+ * pairwise comparison says they co-fire (with no direct
3280
+ * contradictions). Strong verdict, slightly weaker than `matched`.
3281
+ * - `indeterminate` — a non-COMPARABLE operator (`$regex`, `$elemMatch`,
3282
+ * `$matches`, custom) appeared. The proof builder declines to assert
3283
+ * overlap; the message says so explicitly rather than falsely
3284
+ * reporting a contradiction.
3285
+ * - `function-form-opaque` — at least one constraint uses a function
3286
+ * `when:` (`(facts) => ...`), so structural comparison is impossible.
3287
+ * The warning text disclaims; if audit-ledger is mounted and its
3288
+ * `whenSourceCache` is available, identifying hashes of the function
3289
+ * source are included for cross-version diffing.
3290
+ *
3291
+ * @public
3292
+ */
3293
+ type PredicateOverlapProof = {
3294
+ verdict: "matched";
3295
+ coFireClauses: readonly LeafClause[];
3296
+ conflictingClauses: readonly never[];
3297
+ } | {
3298
+ verdict: "overlap";
3299
+ coFireClauses: readonly LeafClause[];
3300
+ conflictingClauses: readonly LeafClause[];
3301
+ } | {
3302
+ verdict: "indeterminate";
3303
+ reason: "non-comparable-operator";
3304
+ coFireClauses: readonly LeafClause[];
3305
+ } | {
3306
+ verdict: "function-form-opaque";
3307
+ reason: "one-or-both-when-is-a-function";
3308
+ whenSourceHashes?: readonly string[];
3309
+ };
3074
3310
  /** Typed events emitted by system.observe(). */
3075
3311
  type ObservationEvent = {
3076
3312
  type: "fact.change";
@@ -3090,6 +3326,26 @@ type ObservationEvent = {
3090
3326
  type: "constraint.error";
3091
3327
  id: string;
3092
3328
  error: unknown;
3329
+ }
3330
+ /**
3331
+ * Fired when the engine silently disables a constraint's `abortOn:`
3332
+ * binding because the constraint is async (declared `async: true` OR
3333
+ * runtime-promoted because its `when()` returned a Promise). The
3334
+ * dev-mode `console.warn` is the human-facing signal; this event is
3335
+ * the SIEM-facing one. Without it, a production constraint loses its
3336
+ * clobber-protection with no plugin / observer trail.
3337
+ *
3338
+ * - `reason: "async-declared"` — the constraint def has `async: true`.
3339
+ * The author opted in; the warning + event are advisory.
3340
+ * - `reason: "async-promoted"` — the constraint's `when()` returned
3341
+ * a Promise at runtime. The author probably did not realize. This
3342
+ * is the more dangerous case — the binding silently disables and
3343
+ * the clobber check no-ops.
3344
+ */
3345
+ | {
3346
+ type: "constraint.binding.disabled";
3347
+ id: string;
3348
+ reason: "async-declared" | "async-promoted";
3093
3349
  } | {
3094
3350
  type: "requirement.created";
3095
3351
  id: string;
@@ -3117,7 +3373,7 @@ type ObservationEvent = {
3117
3373
  error: unknown;
3118
3374
  } | {
3119
3375
  /**
3120
- * A real per-write rejection. A bound resolver's owned-fact write was
3376
+ * A real per-write rejection. A bound resolver's abort-bound write was
3121
3377
  * dropped because the fact was changed by something outside the
3122
3378
  * resolver between the resolver's baseline and its next write (RFC-0003
3123
3379
  * optimistic-concurrency check). `reason` keeps the observation protocol
@@ -3162,6 +3418,52 @@ type ObservationEvent = {
3162
3418
  requirementId: string;
3163
3419
  reason: "clobbered";
3164
3420
  dropped: number;
3421
+ }
3422
+ /**
3423
+ * v1.23.0 — fires when `clobberLoopPlugin` detects a sustained pattern
3424
+ * of clobbers on a fact involving the same set of resolvers above
3425
+ * threshold within a time window. A single clobber is fine; the
3426
+ * binding catches the race and the audit ledger records it. A *loop*
3427
+ * is two or more resolvers whose `when:` predicates both satisfy a
3428
+ * shared state and rewrite the fact every reconcile tick.
3429
+ *
3430
+ * `participants` is the unordered, distinct set of resolver IDs
3431
+ * contributing to the loop. `predicateOverlap` (when both sides use a
3432
+ * data-form `when:`) explains WHY the loop occurs — which clauses
3433
+ * co-fire — so the suggested fix (add `priority:`, narrow `when:`,
3434
+ * merge) is grounded in the actual rules, not a guess.
3435
+ */
3436
+ | {
3437
+ type: "resolver.clobber.loop.detected";
3438
+ systemId: string;
3439
+ fact: string;
3440
+ participants: readonly string[];
3441
+ participantModules: readonly string[];
3442
+ count: number;
3443
+ windowMs: number;
3444
+ firstAt: number;
3445
+ lastAt: number;
3446
+ predicateOverlap?: PredicateOverlapProof;
3447
+ severity: "warn" | "error";
3448
+ factTags: readonly string[];
3449
+ suppressedSinceLastEmit: number;
3450
+ rejectionSeqs: readonly number[];
3451
+ }
3452
+ /**
3453
+ * v1.23.0 — fires when a previously-detected clobber loop closes
3454
+ * cleanly, so dashboards can show "5 active loops" not "47
3455
+ * historical loops". A loop is considered resolved when the
3456
+ * `(fact, participantSet)` goes a quiet window without further
3457
+ * `resolver.write.rejected` events, OR a participant is unregistered,
3458
+ * OR a constraint re-registration changes a participant's `whenSpec`.
3459
+ */
3460
+ | {
3461
+ type: "resolver.clobber.loop.resolved";
3462
+ systemId: string;
3463
+ fact: string;
3464
+ participants: readonly string[];
3465
+ durationMs: number;
3466
+ resolution: "no-recurrence-in-window" | "participant-disabled" | "predicate-narrowed";
3165
3467
  } | {
3166
3468
  type: "effect.run";
3167
3469
  id: string;
@@ -3349,6 +3651,31 @@ interface System<M extends ModuleSchema = ModuleSchema> {
3349
3651
  */
3350
3652
  readonly notify: {
3351
3653
  guardrailBlocked(plugin: string, key: string, kind: "redact" | "alert" | "detect", count: number, category?: string): void;
3654
+ /**
3655
+ * v1.23.0 — plugin authoring surface for emitting the
3656
+ * `"resolver.clobber.loop.detected"` ObservationEvent. Used by
3657
+ * `clobberLoopPlugin` (and any compatible third-party detector). The
3658
+ * call fans out to all registered plugins' `onClobberLoopDetected`
3659
+ * hooks (including the synthetic plugins that back `system.observe()`)
3660
+ * so audit-ledger, devtools, and OTel exporters capture the event
3661
+ * without taking a dependency on the originating detector.
3662
+ *
3663
+ * Application code should never call this directly — use
3664
+ * `system.observe()` to subscribe.
3665
+ */
3666
+ clobberLoopDetected(event: ObservationEvent & {
3667
+ type: "resolver.clobber.loop.detected";
3668
+ }): void;
3669
+ /**
3670
+ * v1.23.0 — companion to {@link clobberLoopDetected}. Fires when a
3671
+ * previously-detected loop is considered resolved (quiet window
3672
+ * elapsed, participant unregistered, or predicate narrowed). Lets
3673
+ * monitoring dashboards show "active loops" rather than
3674
+ * "historical loops."
3675
+ */
3676
+ clobberLoopResolved(event: ObservationEvent & {
3677
+ type: "resolver.clobber.loop.resolved";
3678
+ }): void;
3352
3679
  };
3353
3680
  /** Per-run trace entries (null if trace is not enabled) */
3354
3681
  readonly trace: TraceEntry[] | null;
@@ -3639,6 +3966,27 @@ interface Plugin<M extends ModuleSchema = ModuleSchema> {
3639
3966
  * @param error - The error that was thrown
3640
3967
  */
3641
3968
  onConstraintError?: (id: string, error: unknown) => void;
3969
+ /**
3970
+ * Called when the engine silently disables a constraint's `abortOn:`
3971
+ * binding because the constraint is async. Pairs with the dev-mode
3972
+ * `console.warn` for SIEM-side visibility — without this signal, a
3973
+ * production constraint loses its clobber-protection with no
3974
+ * plugin / observer trail.
3975
+ *
3976
+ * - `"async-declared"` — the constraint def has `async: true`. The
3977
+ * author opted in; treat as advisory.
3978
+ * - `"async-promoted"` — the constraint's `when()` returned a Promise
3979
+ * at runtime, even though `async: true` was not set. The author
3980
+ * probably did not realize. This is the dangerous case — escalate.
3981
+ *
3982
+ * Fired at most once per `getConstraintBinding` lookup; deduping
3983
+ * across reconcile ticks is the consumer's responsibility (the
3984
+ * audit-ledger plugin does this with a per-id sticky bit).
3985
+ *
3986
+ * @param id - The constraint ID whose binding was disabled
3987
+ * @param reason - Whether the async state was author-declared or runtime-promoted
3988
+ */
3989
+ onConstraintBindingDisabled?: (id: string, reason: "async-declared" | "async-promoted") => void;
3642
3990
  /**
3643
3991
  * Called when a new requirement is created by a constraint.
3644
3992
  * @param req - The requirement with its computed ID
@@ -3691,8 +4039,9 @@ interface Plugin<M extends ModuleSchema = ModuleSchema> {
3691
4039
  /**
3692
4040
  * Called when a resolver's write is rejected by the runtime. The only
3693
4041
  * `reason` today is `"clobbered"`: a bound resolver (RFC-0003) dropped a
3694
- * write to an owned fact because the fact was changed by something outside
3695
- * the resolver between the resolver's baseline and its next write the
4042
+ * write to an abort-bound fact (one listed in the constraint's
4043
+ * `abortOn:`) because the fact was changed by something outside the
4044
+ * resolver between the resolver's baseline and its next write — the
3696
4045
  * resolver's `AbortController` is aborted in the same step. See
3697
4046
  * {@link createBoundFacts} for the per-fact optimistic-concurrency model.
3698
4047
  * `reason` keeps the hook backend-neutral.
@@ -3826,6 +4175,26 @@ interface Plugin<M extends ModuleSchema = ModuleSchema> {
3826
4175
  * payloads.
3827
4176
  */
3828
4177
  onGuardrailBlocked?: (plugin: string, key: string, kind: "redact" | "alert" | "detect", count: number, category?: string) => void;
4178
+ /**
4179
+ * Called when `clobberLoopPlugin` (or any equivalent detector) publishes
4180
+ * a structured loop event through `system.notify.clobberLoopDetected(...)`.
4181
+ * Surfaces as the `"resolver.clobber.loop.detected"` `ObservationEvent`
4182
+ * for `system.observe()` subscribers + audit-ledger + devtools, so
4183
+ * downstream backends can capture loops without taking a direct
4184
+ * dependency on `clobberLoopPlugin`.
4185
+ */
4186
+ onClobberLoopDetected?: (event: ObservationEvent & {
4187
+ type: "resolver.clobber.loop.detected";
4188
+ }) => void;
4189
+ /**
4190
+ * Called when a previously-detected loop is considered resolved (quiet
4191
+ * window elapsed, participant unregistered, or predicate narrowed).
4192
+ * Pairs with `onClobberLoopDetected` so dashboards can show "active
4193
+ * loops" rather than "historical loops."
4194
+ */
4195
+ onClobberLoopResolved?: (event: ObservationEvent & {
4196
+ type: "resolver.clobber.loop.resolved";
4197
+ }) => void;
3829
4198
  /**
3830
4199
  * Called when a history snapshot is taken.
3831
4200
  * @param snapshot - The snapshot that was captured
@@ -3884,4 +4253,4 @@ interface Plugin<M extends ModuleSchema = ModuleSchema> {
3884
4253
  onTraceComplete?: (entry: TraceEntry) => void;
3885
4254
  }
3886
4255
 
3887
- export { type InferSchemaType as $, type AnySystem as A, type BatchConfig as B, type ClauseResult as C, type DefinitionMeta as D, type EffectsDef as E, type FactPredicate as F, type DynamicConstraintDef as G, type DynamicEffectDef as H, type DynamicResolverDef as I, type EffectDef as J, type EventsDef as K, type FactTemplate as L, type ModuleSchema as M, type NamespacedSystem as N, type FactsSnapshot as O, type Plugin as P, type HistoryAPI as Q, type RequirementWithId as R, type SchemaType as S, type TypedDerivationsDef as T, type HistoryOption as U, type HistoryState as V, type InferDerivations as W, type InferEvents as X, type InferFacts as Y, type InferRequirementTypes as Z, type InferRequirements as _, type Facts as a, type FactReturnType as a$, type InferSelectorState as a0, type KeySelector as a1, type MetaAccessor as a2, type MetaMatch as a3, type ObservationEvent as a4, type OperatorObject as a5, type PatchSpec as a6, type PatchValue as a7, type PayloadRef as a8, type PredicateClause as a9, isSingleModuleSystem as aA, type FactsStore as aB, type ConstraintState as aC, type ResolverStatus as aD, type FactChange as aE, type ReconcileResult as aF, type RecoveryStrategy as aG, type ErrorSource as aH, type RetryLaterConfig as aI, type BatchItemResult as aJ, type BatchResolveResults as aK, type ConstraintsControl as aL, type CrossModuleDerivationFn as aM, type CrossModuleFactsWithSelf as aN, type DerivationKeys as aO, type DerivationReturnType as aP, type DerivationsControl as aQ, type DerivationsSchema as aR, type DeriveAccessor as aS, type DispatchEventsFromSchema as aT, type EffectCleanup as aU, type EffectsControl as aV, type EventPayloadSchema as aW, type EventsAccessor as aX, type EventsAccessorFromSchema as aY, type EventsSchema as aZ, type FactKeys as a_, type PredicateCombinator as aa, type PredicateCombinatorKey as ab, type PredicateObject as ac, type PredicateOp as ad, type ResolverDef as ae, type ResolversDef as af, type RetryPolicy as ag, type Schema as ah, type SchemaTypedResolversDef as ai, type Snapshot as aj, type SourceDef as ak, type SourceDropReason as al, type SourcePublish as am, type SourceReportError as an, type SourceUnsubscribe as ao, type System as ap, type SystemConfig as aq, type SystemDerived as ar, type SystemFacts as as, type SystemInspection as at, type SystemMode as au, type SystemSnapshot as av, type TraceEntry as aw, type TypedConstraintDef as ax, type TypedResolverDef as ay, isNamespacedSystem as az, type TypedEventsDef as b, type FlexibleEventHandler as b0, type HistoryConfig as b1, type InferEventPayloadFromSchema as b2, type InferRequirementPayloadFromSchema as b3, type InferSchema as b4, type MutableNamespacedFacts as b5, type NamespacedDerivations as b6, type NamespacedEventsAccessor as b7, type NamespacedFacts as b8, type ObservableKeys as b9, type RequirementExplanation as ba, type RequirementOutput as bb, type RequirementPayloadSchema$1 as bc, type RequirementsSchema as bd, type ResolverContext as be, type ResolversControl as bf, type SnapshotMeta as bg, type SystemEvent as bh, type TraceConfig as bi, type TypedResolverContext as bj, type UnionEvents as bk, type RequirementOutput$1 as bl, type SourcesDef as c, type TypedConstraintsDef as d, type TypedResolversDef as e, type ModuleHooks as f, type CrossModuleDeps as g, type CrossModuleDerivationsDef as h, type CrossModuleEffectsDef as i, type CrossModuleConstraintsDef as j, type ModuleDef as k, type CreateSystemOptionsSingle as l, type SingleModuleSystem as m, type ModulesMap as n, type CreateSystemOptionsNamed as o, type TraceOption as p, type ErrorBoundaryConfig as q, type Requirement as r, type RequirementKeyFn as s, type ConstraintDef as t, type ConstraintsDef as u, type CrossModuleConstraintDef as v, type CrossModuleEffectDef as w, DirectiveError as x, type DistributableSnapshot as y, type DistributableSnapshotOptions as z };
4256
+ export { type InferDerivations as $, type AnySystem as A, type BatchConfig as B, type ClauseResult as C, type DefinitionMeta as D, type EffectsDef as E, type FactPredicate as F, type CrossModuleEffectDef as G, type DiffRulesOptions as H, DirectiveError as I, type DistributableSnapshot as J, type DistributableSnapshotOptions as K, type DynamicConstraintDef as L, type ModuleSchema as M, type NamespacedSystem as N, type DynamicEffectDef as O, type Plugin as P, type DynamicResolverDef as Q, type RequirementWithId as R, type SchemaType as S, type TypedDerivationsDef as T, type EffectDef as U, type EventsDef as V, type FactTemplate as W, type FactsSnapshot as X, type HistoryAPI as Y, type HistoryOption as Z, type HistoryState as _, type Facts as a, type CrossModuleDerivationFn as a$, type InferEvents as a0, type InferFacts as a1, type InferRequirementTypes as a2, type InferRequirements as a3, type InferSchemaType as a4, type InferSelectorState as a5, type KeySelector as a6, type LeafClause as a7, type MetaAccessor as a8, type MetaMatch as a9, type System as aA, type SystemConfig as aB, type SystemDerived as aC, type SystemFacts as aD, type SystemInspection as aE, type SystemMode as aF, type SystemSnapshot as aG, type TraceEntry as aH, type TypedConstraintDef as aI, type TypedResolverDef as aJ, diffClauses as aK, diffRules as aL, flattenPredicate as aM, isNamespacedSystem as aN, isSingleModuleSystem as aO, toRulesMap as aP, type FactsStore as aQ, type ConstraintState as aR, type ResolverStatus as aS, type FactChange as aT, type ReconcileResult as aU, type RecoveryStrategy as aV, type ErrorSource as aW, type RetryLaterConfig as aX, type BatchItemResult as aY, type BatchResolveResults as aZ, type ConstraintsControl as a_, type ObservationEvent as aa, type OperatorObject as ab, type PatchSpec as ac, type PatchValue as ad, type PayloadRef as ae, type PredicateClause as af, type PredicateCombinator as ag, type PredicateCombinatorKey as ah, type PredicateObject as ai, type PredicateOp as aj, type PredicateOverlapProof as ak, type ResolverAbortReason as al, type ResolverDef as am, type ResolversDef as an, type RetryPolicy as ao, type RulesDiffReport as ap, type RulesMapInput as aq, type Schema as ar, type SchemaTypedResolversDef as as, type ShouldRetryContext as at, type Snapshot as au, type SourceDef as av, type SourceDropReason as aw, type SourcePublish as ax, type SourceReportError as ay, type SourceUnsubscribe as az, type TypedEventsDef as b, type CrossModuleFactsWithSelf as b0, type DerivationKeys as b1, type DerivationReturnType as b2, type DerivationsControl as b3, type DerivationsSchema as b4, type DeriveAccessor as b5, type DispatchEventsFromSchema as b6, type EffectCleanup as b7, type EffectsControl as b8, type EventPayloadSchema as b9, type RequirementOutput$1 as bA, type EventsAccessor as ba, type EventsAccessorFromSchema as bb, type EventsSchema as bc, type FactKeys as bd, type FactReturnType as be, type FlexibleEventHandler as bf, type HistoryConfig as bg, type InferEventPayloadFromSchema as bh, type InferRequirementPayloadFromSchema as bi, type InferSchema as bj, type MutableNamespacedFacts as bk, type NamespacedDerivations as bl, type NamespacedEventsAccessor as bm, type NamespacedFacts as bn, type ObservableKeys as bo, type RequirementExplanation as bp, type RequirementOutput as bq, type RequirementPayloadSchema$1 as br, type RequirementsSchema as bs, type ResolverContext as bt, type ResolversControl as bu, type SnapshotMeta as bv, type SystemEvent as bw, type TraceConfig as bx, type TypedResolverContext as by, type UnionEvents as bz, type SourcesDef as c, type TypedConstraintsDef as d, type TypedResolversDef as e, type ModuleHooks as f, type CrossModuleDeps as g, type CrossModuleDerivationsDef as h, type CrossModuleEffectsDef as i, type CrossModuleConstraintsDef as j, type ModuleDef as k, type CreateSystemOptionsSingle as l, type SingleModuleSystem as m, type ModulesMap as n, type CreateSystemOptionsNamed as o, type TraceOption as p, type ErrorBoundaryConfig as q, type Requirement as r, type RequirementKeyFn as s, type Change as t, type ChangeKind as u, type ConstraintDef as v, type ConstraintDiff as w, type ConstraintStatus as x, type ConstraintsDef as y, type CrossModuleConstraintDef as z };