@fairfox/polly 0.91.0 → 0.92.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 (107) hide show
  1. package/dist/cli/polly.js +3 -3
  2. package/dist/cli/polly.js.map +3 -3
  3. package/dist/gallery/gallery.css +2 -2
  4. package/dist/gallery/gallery.js +248 -241
  5. package/dist/src/background/index.js +27 -11
  6. package/dist/src/background/index.js.map +2 -2
  7. package/dist/src/background/message-router.js +27 -11
  8. package/dist/src/background/message-router.js.map +2 -2
  9. package/dist/src/client/index.js +56 -56
  10. package/dist/src/client/index.js.map +4 -4
  11. package/dist/src/elysia/index.js +6 -6
  12. package/dist/src/elysia/index.js.map +3 -3
  13. package/dist/src/index.js +27 -11
  14. package/dist/src/index.js.map +2 -2
  15. package/dist/src/mesh.js +2 -2
  16. package/dist/src/mesh.js.map +3 -3
  17. package/dist/src/peer.js +6 -6
  18. package/dist/src/peer.js.map +3 -3
  19. package/dist/src/polly-ui/index.css +2 -2
  20. package/dist/src/polly-ui/markdown.js +1844 -1010
  21. package/dist/src/polly-ui/markdown.js.map +5 -5
  22. package/dist/src/polly-ui/styles.css +2 -2
  23. package/dist/src/shared/adapters/index.js +27 -11
  24. package/dist/src/shared/adapters/index.js.map +2 -2
  25. package/dist/src/shared/lib/context-helpers.js +27 -11
  26. package/dist/src/shared/lib/context-helpers.js.map +2 -2
  27. package/dist/src/shared/lib/message-bus.js +27 -11
  28. package/dist/src/shared/lib/message-bus.js.map +2 -2
  29. package/dist/src/shared/lib/resource.js +17 -1
  30. package/dist/src/shared/lib/resource.js.map +2 -2
  31. package/dist/src/shared/lib/state.js +17 -1
  32. package/dist/src/shared/lib/state.js.map +2 -2
  33. package/dist/src/shared/state/app-state.js +17 -1
  34. package/dist/src/shared/state/app-state.js.map +2 -2
  35. package/dist/tools/bdd/src/cli.js +20 -2
  36. package/dist/tools/bdd/src/cli.js.map +4 -4
  37. package/dist/tools/bdd/src/index.js +20 -2
  38. package/dist/tools/bdd/src/index.js.map +3 -3
  39. package/dist/tools/init/templates/pwa/package.json.template +2 -3
  40. package/dist/tools/mutate/src/args.d.ts +25 -3
  41. package/dist/tools/mutate/src/cli.js +3234 -312
  42. package/dist/tools/mutate/src/cli.js.map +32 -12
  43. package/dist/tools/mutate/src/config.d.ts +13 -2
  44. package/dist/tools/mutate/src/define.d.ts +76 -0
  45. package/dist/tools/mutate/src/engine/ast.d.ts +13 -0
  46. package/dist/tools/mutate/src/engine/baseline.d.ts +20 -0
  47. package/dist/tools/mutate/src/engine/cache.d.ts +32 -0
  48. package/dist/tools/mutate/src/engine/coverage.d.ts +60 -0
  49. package/dist/tools/mutate/src/engine/generate.d.ts +23 -0
  50. package/dist/tools/mutate/src/engine/graph.d.ts +11 -0
  51. package/dist/tools/mutate/src/engine/index.d.ts +16 -0
  52. package/dist/tools/mutate/src/engine/junit.d.ts +38 -0
  53. package/dist/tools/mutate/src/engine/operators.d.ts +14 -0
  54. package/dist/tools/mutate/src/engine/preload.d.ts +10 -0
  55. package/dist/tools/mutate/src/engine/preload.js +180 -0
  56. package/dist/tools/mutate/src/engine/preload.js.map +10 -0
  57. package/dist/tools/mutate/src/engine/probes.d.ts +12 -0
  58. package/dist/tools/mutate/src/engine/report-writer.d.ts +44 -0
  59. package/dist/tools/mutate/src/engine/runner.d.ts +31 -0
  60. package/dist/tools/mutate/src/engine/stray-writes.d.ts +25 -0
  61. package/dist/tools/mutate/src/engine/typecheck.d.ts +37 -0
  62. package/dist/tools/mutate/src/engine/types.d.ts +191 -0
  63. package/dist/tools/mutate/src/engine/verify-callsites.d.ts +9 -0
  64. package/dist/tools/mutate/src/engine/workdir.d.ts +16 -0
  65. package/dist/tools/mutate/src/engine-config.d.ts +7 -0
  66. package/dist/tools/mutate/src/index.d.ts +7 -3
  67. package/dist/tools/mutate/src/index.js +2701 -77
  68. package/dist/tools/mutate/src/index.js.map +26 -7
  69. package/dist/tools/mutate/src/ingest.d.ts +30 -1
  70. package/dist/tools/mutate/src/init.d.ts +2 -1
  71. package/dist/tools/mutate/src/migrate.d.ts +32 -0
  72. package/dist/tools/mutate/src/report.d.ts +3 -0
  73. package/dist/tools/mutate/src/run.d.ts +17 -8
  74. package/dist/tools/mutate/src/verify-matrix.d.ts +12 -15
  75. package/dist/tools/quality/src/cli.js +9 -9
  76. package/dist/tools/quality/src/cli.js.map +4 -4
  77. package/dist/tools/quality/src/index.js +9 -9
  78. package/dist/tools/quality/src/index.js.map +4 -4
  79. package/dist/tools/quality/src/no-tautology-ensures.d.ts +2 -2
  80. package/dist/tools/quality/src/plugins/import-checks.d.ts +1 -1
  81. package/dist/tools/test/src/adapters/index.js +3 -2
  82. package/dist/tools/test/src/adapters/index.js.map +3 -3
  83. package/dist/tools/test/src/coverage-policy/cli.d.ts +2 -2
  84. package/dist/tools/test/src/coverage-policy/cli.js +217 -17
  85. package/dist/tools/test/src/coverage-policy/cli.js.map +7 -5
  86. package/dist/tools/test/src/coverage-policy/index.d.ts +1 -1
  87. package/dist/tools/test/src/coverage-policy/index.js +213 -12
  88. package/dist/tools/test/src/coverage-policy/index.js.map +7 -5
  89. package/dist/tools/test/src/coverage-policy/mutate-targets.d.ts +29 -12
  90. package/dist/tools/test/src/e2e-mesh/index.js +6 -6
  91. package/dist/tools/test/src/e2e-mesh/index.js.map +4 -4
  92. package/dist/tools/test/src/e2e-relay/index.js +230 -226
  93. package/dist/tools/test/src/e2e-relay/index.js.map +7 -7
  94. package/dist/tools/test/src/index.js +3 -2
  95. package/dist/tools/test/src/index.js.map +3 -3
  96. package/dist/tools/test/src/visual/index.js +684 -511
  97. package/dist/tools/test/src/visual/index.js.map +11 -11
  98. package/dist/tools/verify/src/cli.js +2 -2
  99. package/dist/tools/verify/src/cli.js.map +3 -3
  100. package/dist/tools/verify/src/config.d.ts +1 -1
  101. package/dist/tools/verify/src/config.js +68 -3
  102. package/dist/tools/verify/src/config.js.map +4 -4
  103. package/dist/tools/verify/src/primitives/index.d.ts +34 -0
  104. package/package.json +29 -39
  105. package/dist/tools/verify/src/stryker/index.d.ts +0 -32
  106. package/dist/tools/verify/src/stryker/index.js +0 -86
  107. package/dist/tools/verify/src/stryker/index.js.map +0 -10
@@ -200,4 +200,4 @@ type UnifiedVerificationConfig = LegacyVerificationConfig | AdapterVerificationC
200
200
  * ```
201
201
  */
202
202
  export declare function defineVerification<T extends UnifiedVerificationConfig>(config: T): T;
203
- export { $constraints, ensures, forAllPeers, hasLength, inRange, oneOf, requires, somePeer, stateConstraint, } from "./primitives/index.js";
203
+ export { $constraints, CONTRACT_VIOLATIONS_KEY, type ContractPrimitive, type ContractsMode, ContractViolation, contractsMode, drainContractViolations, ensures, forAllPeers, hasLength, inRange, oneOf, requires, somePeer, stateConstraint, } from "./primitives/index.js";
@@ -1,6 +1,67 @@
1
1
  // tools/verify/src/primitives/index.ts
2
- function requires(condition, message) {}
3
- function ensures(condition, message) {}
2
+ var CONTRACT_VIOLATIONS_KEY = "__pollyContractViolations";
3
+ function readContractsMode() {
4
+ const v = typeof process !== "undefined" && process.env ? process.env["POLLY_CONTRACTS"] : undefined;
5
+ return v === "all" || v === "ensures" ? v : "off";
6
+ }
7
+ var MODE = readContractsMode();
8
+ function contractsMode() {
9
+ return MODE;
10
+ }
11
+
12
+ class ContractViolation extends Error {
13
+ primitive;
14
+ site;
15
+ contractMessage;
16
+ name = "ContractViolation";
17
+ constructor(primitive, site, contractMessage) {
18
+ super(`[polly-contract ${primitive} ${site}]${contractMessage === undefined ? "" : ` ${contractMessage}`}`);
19
+ this.primitive = primitive;
20
+ this.site = site;
21
+ this.contractMessage = contractMessage;
22
+ }
23
+ }
24
+ var OWN_FILE = import.meta.url.startsWith("file://") ? decodeURIComponent(import.meta.url.slice("file://".length)) : import.meta.url;
25
+ var FRAME = /\(?([^()\s]+):(\d+):\d+\)?\s*$/;
26
+ function callSite() {
27
+ const frames = [];
28
+ for (const line of (new Error().stack ?? "").split(`
29
+ `)) {
30
+ const m = FRAME.exec(line);
31
+ if (m?.[1] && m[2])
32
+ frames.push({ file: m[1], line: m[2] });
33
+ }
34
+ const outside = frames.find((f) => f.file !== OWN_FILE) ?? frames[3];
35
+ return outside ? `${outside.file}:${outside.line}` : "unknown:0";
36
+ }
37
+ function violationList() {
38
+ const g = globalThis;
39
+ const existing = g[CONTRACT_VIOLATIONS_KEY];
40
+ if (Array.isArray(existing))
41
+ return existing;
42
+ const list = [];
43
+ g[CONTRACT_VIOLATIONS_KEY] = list;
44
+ return list;
45
+ }
46
+ function violate(primitive, condition, message) {
47
+ if (condition)
48
+ return;
49
+ if (MODE === "off" || MODE === "ensures" && primitive === "requires")
50
+ return;
51
+ const violation = new ContractViolation(primitive, callSite(), message);
52
+ violationList().push(violation);
53
+ throw violation;
54
+ }
55
+ function drainContractViolations() {
56
+ const list = violationList();
57
+ return list.splice(0, list.length);
58
+ }
59
+ function requires(condition, message) {
60
+ violate("requires", condition, message);
61
+ }
62
+ function ensures(condition, message) {
63
+ violate("ensures", condition, message);
64
+ }
4
65
  function inRange(value, min, max) {
5
66
  return value >= min && value <= max;
6
67
  }
@@ -52,7 +113,11 @@ function defineVerification(config) {
52
113
  }
53
114
  export {
54
115
  $constraints,
116
+ CONTRACT_VIOLATIONS_KEY,
117
+ ContractViolation,
118
+ contractsMode,
55
119
  defineVerification,
120
+ drainContractViolations,
56
121
  ensures,
57
122
  forAllPeers,
58
123
  hasLength,
@@ -63,4 +128,4 @@ export {
63
128
  stateConstraint
64
129
  };
65
130
 
66
- //# debugId=EB2A6F997258A60464756E2164756E21
131
+ //# debugId=BFA2A054CE121A2F64756E2164756E21
@@ -2,10 +2,10 @@
2
2
  "version": 3,
3
3
  "sources": ["../tools/verify/src/primitives/index.ts", "../tools/verify/src/config.ts"],
4
4
  "sourcesContent": [
5
- "// Verification primitives for formal verification\n// These are runtime no-ops but extracted during verification\n\n/**\n * Assert a precondition that must be true when the handler executes.\n *\n * In production: No-op (compiled away)\n * In verification: Translated to TLA+ assertion\n *\n * @example\n * messageBus.on(\"USER_LOGIN\", (payload) => {\n * requires(state.user.loggedIn === false, \"User must not be logged in\")\n * state.user.loggedIn = true\n * })\n */\nexport function requires(condition: boolean, message?: string): void {\n // Runtime no-op - only used during verification\n // Condition and message are checked during static analysis only\n void condition;\n void message;\n}\n\n/**\n * Assert a postcondition that must be true after the handler completes.\n *\n * In production: No-op (compiled away)\n * In verification: Translated to TLA+ assertion\n *\n * @example\n * messageBus.on(\"USER_LOGIN\", (payload) => {\n * state.user.loggedIn = true\n * ensures(state.user.loggedIn === true, \"User must be logged in\")\n * })\n */\nexport function ensures(condition: boolean, message?: string): void {\n // Runtime no-op - only used during verification\n // Condition and message are checked during static analysis only\n void condition;\n void message;\n}\n\n/**\n * Define a global invariant that must always hold.\n *\n * In production: No-op (compiled away)\n * In verification: Translated to TLA+ invariant\n *\n * @example\n * invariant(\"UserIdConsistent\", () =>\n * state.user.loggedIn === false || state.user.id !== null\n * )\n */\nexport function invariant(_name: string, condition: () => boolean): void {\n // Runtime no-op - only used during verification\n // Name and condition are checked during static analysis only\n void condition;\n}\n\n/**\n * Assert that a value is within a valid range.\n *\n * @example\n * requires(inRange(todoCount, 0, 100), \"Todo count must be 0-100\")\n */\nexport function inRange(value: number, min: number, max: number): boolean {\n return value >= min && value <= max;\n}\n\n/**\n * Assert that a value is one of the allowed values.\n *\n * @example\n * requires(oneOf(state.user.role, [\"admin\", \"user\"]), \"Role must be admin or user\")\n */\nexport function oneOf<T>(value: T, allowed: T[]): boolean {\n return allowed.includes(value);\n}\n\n/**\n * Assert that an array has a specific length constraint.\n *\n * @example\n * requires(hasLength(state.todos, { max: 10 }), \"Too many todos\")\n */\nexport function hasLength(array: unknown[], constraint: { min?: number; max?: number }): boolean {\n if (constraint.min !== undefined && array.length < constraint.min) return false;\n if (constraint.max !== undefined && array.length > constraint.max) return false;\n return true;\n}\n\n/**\n * Declare state-level constraints for verification and optional runtime checking.\n * Maps message types to preconditions on state fields.\n *\n * The parser automatically wires these constraints to handlers during verification.\n * Optionally, constraints can be enforced at runtime by passing `{ runtime: true }`.\n *\n * @example\n * // Verification only (TLA+ generation)\n * const state = { loggedIn: false };\n *\n * $constraints(\"loggedIn\", {\n * USER_LOGOUT: { requires: \"state.loggedIn === true\", message: \"Must be logged in\" },\n * BOOKMARK_ADD: { requires: \"state.loggedIn === true\", message: \"Must be logged in\" },\n * });\n *\n * @example\n * // Runtime enforcement (function predicates)\n * $constraints(\"loggedIn\", {\n * USER_LOGOUT: {\n * requires: (state) => state.loggedIn === true,\n * message: \"Must be logged in to logout\"\n * },\n * }, { runtime: true });\n */\nexport function $constraints(\n stateField: string,\n constraints: Record<\n string,\n {\n requires?: string | ((state: unknown) => boolean);\n ensures?: string | ((state: unknown) => boolean);\n message?: string;\n }\n >,\n options?: { runtime?: boolean }\n): void {\n // Register constraints for runtime checking if enabled\n if (options?.runtime) {\n // Import dynamically to avoid circular dependencies\n // This is safe because it only happens at runtime, not during static analysis\n // @ts-expect-error - Dynamic import path resolves correctly at runtime\n import(\"../../../src/shared/lib/constraints.js\")\n .then(({ registerConstraints }) => {\n registerConstraints(stateField, constraints);\n })\n .catch(() => {\n // Silently ignore - constraints module may not be available during static analysis\n });\n }\n\n // For verification: Still a no-op at runtime\n // Parser extracts these and wires them to TLA+ handlers\n}\n\n/**\n * Declare a global state constraint that prunes structurally impossible states.\n *\n * In production: No-op (compiled away)\n * In verification: Translated to TLC CONSTRAINT clause, discarding states\n * that violate the predicate from the exploration queue entirely.\n *\n * Unlike `invariant()` (which checks but still explores), `stateConstraint()`\n * prevents the model checker from ever reaching the pruned states.\n *\n * @example\n * stateConstraint(\"LeaderRequiresConnection\", () =>\n * !connectionState.value.isLeader || connectionState.value.status === \"connected\"\n * )\n */\nexport function stateConstraint(\n name: string,\n predicate: () => boolean,\n options?: { message?: string }\n): void {\n void name;\n void predicate;\n void options;\n}\n\n/**\n * Cross-peer universal quantifier for use inside `requires` and\n * `ensures` predicates. The runtime is a no-op that simply runs the\n * predicate against a single dummy context (so the assertion never\n * triggers at runtime); the verifier recognizes the wrapper in\n * predicate source and emits a `\\A peer \\in Contexts \\ {ctx} : (...)`\n * clause that asks TLC to check the inner predicate against every\n * other context.\n *\n * @example\n * ensures(\n * forAllPeers(peer => peer.todos.value.length === todos.value.length),\n * \"every peer agrees on todo count\"\n * );\n */\nexport function forAllPeers<TPeer>(predicate: (peer: TPeer) => boolean): boolean {\n void predicate;\n return true;\n}\n\n/**\n * Cross-peer existential quantifier for use inside `requires` and\n * `ensures` predicates. Runtime no-op; the verifier emits a\n * `\\E peer \\in Contexts \\ {ctx} : (...)` clause.\n *\n * @example\n * ensures(\n * somePeer(peer => peer.user.value.loggedIn === true),\n * \"at least one peer has a logged-in user\"\n * );\n */\nexport function somePeer<TPeer>(predicate: (peer: TPeer) => boolean): boolean {\n void predicate;\n return true;\n}\n\n// Re-export for convenience\nexport const verify = {\n requires,\n ensures,\n invariant,\n inRange,\n oneOf,\n hasLength,\n $constraints,\n stateConstraint,\n forAllPeers,\n somePeer,\n};\n",
6
- "// Configuration Helper for @fairfox/polly/verify\n//\n// Lightweight entry point for user configuration files.\n// Does NOT include heavy dependencies (ts-morph, analysis, etc.)\n// which are only needed by the CLI tool.\n\n// Configuration Types (inlined to avoid heavy dependencies)\n\n// polly#160: a directional capability whose grant requires a precondition.\n// Desugars to a TLA+ safety invariant `(enabledBy) => (requires)`. enabledBy /\n// requires are TS boolean expressions in the requires()/ensures() dialect\n// referencing state via `state.` / signal `.value` form (e.g. \"state.authReady\").\n// Inlined here (like SubsystemConfig) so this authoring entry point stays\n// dependency-light; the canonical type is CapabilityConfig in config/types.ts.\ninterface CapabilityConfig {\n name: string;\n enabledBy: string;\n requires: string;\n message?: string;\n}\n\n// Subsystem configuration for compositional verification\ninterface SubsystemConfig {\n state: string[]; // Field names from parent state config\n handlers: string[]; // Message type names\n // Per-subsystem message bounds; override messages.maxInFlight and merge\n // into messages.perMessageBounds for this subsystem only.\n bounds?: {\n maxInFlight?: number;\n perMessageBounds?: Record<string, number>;\n };\n /** polly#171: check message-resolution liveness for this subsystem only. */\n liveness?: boolean;\n}\n\n// Point a subsystem's reachability witnesses at a hand-written TLA+ spec instead\n// of the generated UserApp_<subsystem>. Inlined here (like SubsystemConfig) to\n// keep this authoring entry point dependency-light; canonical type is\n// CustomTLAPath in config/types.ts.\ninterface CustomTLAPath {\n tla: string; // path (relative to cwd) to the hand-written .tla the witness EXTENDS\n cfg: string; // path (relative to cwd) to its .cfg\n module?: string; // module to EXTEND; defaults to the MODULE name parsed from `tla`\n fields?: Record<string, string>; // BDD field name → this spec's TLA+ variable name\n}\n\n// Legacy verification configuration\ninterface LegacyVerificationConfig {\n state: Record<string, unknown>;\n /**\n * polly#185: the contexts this project actually has, emitted as the `.cfg`'s\n * `Contexts` set.\n *\n * Every generated model replicates application state across the set and\n * quantifies each send over `|Contexts| * (2^|Contexts| - 1)` source/target\n * pairs, so its size is the cheapest term in the state space to cut.\n * Measured on a two-handler model at `maxInFlight: 1`: 38,464 distinct\n * states at three contexts, 368 at one.\n *\n * Names become TLA+ model values, so each must sanitise to\n * `[A-Za-z_][A-Za-z0-9_]*`. Omit the key to keep the browser-extension\n * default `[\"background\", \"content\", \"popup\"]`.\n *\n * A single context makes every send a self-send, and disables\n * `PropagateMeshOp` (which requires two distinct contexts) — so a `mesh`\n * block wants at least two.\n *\n * @example\n * ```ts\n * defineVerification({\n * contexts: [\"server\", \"client\"],\n * state: { ready: { type: \"boolean\" } },\n * messages: { maxInFlight: 1 },\n * });\n * ```\n */\n contexts?: string[];\n\n /**\n * polly#117: optional mesh-document declarations. When present, each\n * key names a `$meshState` document and its value declares the\n * field-level state schema for that document. The verifier emits a\n * separate slot in `contextStates[ctx].mesh[<docId>]` for these\n * fields and adds a `PropagateMeshOp` action that allows the doc's\n * value on one context to flow to another — modelling Automerge\n * sync between peers. Mesh references inside `forAllPeers` quantifiers\n * route through this slot so cross-peer convergence claims are\n * actually checked.\n *\n * @example\n * ```ts\n * defineVerification({\n * state: { localCounter: { type: \"number\", min: 0, max: 3 } },\n * mesh: {\n * todos: {\n * entries: { type: \"enum\", values: [\"empty\", \"one\", \"many\"] },\n * },\n * },\n * messages: { maxInFlight: 2 },\n * });\n * ```\n */\n mesh?: Record<string, Record<string, unknown>>;\n messages: {\n // Basic bounds\n maxInFlight?: number;\n maxTabs?: number;\n maxClients?: number;\n maxRenderers?: number;\n maxWorkers?: number;\n maxContexts?: number;\n\n // Tier 1 Optimizations (no precision loss)\n include?: string[]; // Only verify these message types\n exclude?: string[]; // Exclude these message types (mutually exclusive with include)\n symmetry?: string[][]; // Groups of symmetric message types [[type1, type2], [type3, type4]]\n perMessageBounds?: Record<string, number>; // Different maxInFlight per message type\n };\n onBuild?: \"warn\" | \"error\" | \"off\";\n onRelease?: \"warn\" | \"error\" | \"off\";\n\n // Verification engine options\n verification?: {\n timeout?: number; // Timeout in seconds (0 = no timeout)\n workers?: number; // Number of TLC workers\n /**\n * polly#181: docker-style container memory ceiling for the TLC run, e.g.\n * \"8g\". The JVM heap is sized under it, so a model too large for the\n * ceiling ends with a TLC out-of-memory message instead of a silent kill.\n * Unset uses a fixed default (4g), never unbounded.\n */\n memory?: string;\n };\n\n /**\n * polly#171: check that nothing sent stays pending for ever.\n *\n * Off by default because it is expensive: it adds a temporal property and the\n * routing fairness it needs, and TLC must then explore behaviours rather than\n * only reachable states. Measured on an 11-subsystem consumer model, the full\n * verify gate went from ~110s to ~512s.\n *\n * Every property the generator emits without this is a SAFETY property, and a\n * safety property is true of a system that does nothing at all — so a spec in\n * which routing has stopped verifies identically to one in which it works.\n * Turn this on for the subsystems whose progress actually matters.\n *\n * LIMITATION, and it is Lamport's own warning (Specifying Systems 14.3.5):\n * generated specs also declare a CONSTRAINT to bound the state space, and TLC\n * prints \"Declaring state or action constraints during liveness checking is\n * dangerous\" when both are present. A constraint truncates behaviours at its\n * boundary, so a liveness violation reachable only beyond that boundary is not\n * found. Treat a green liveness run as \"no wedge within the bounds\", not as a\n * proof of progress — the same reading every bounded model-checking result\n * deserves.\n */\n liveness?: boolean;\n\n // Subsystem-scoped verification (compositional)\n subsystems?: Record<string, SubsystemConfig>;\n\n // Bind a subsystem's witnesses to a hand-written .tla/.cfg (read only by the\n // --witness pass; a custom subsystem is skipped during generated-spec verify).\n customTLAPaths?: Record<string, CustomTLAPath>;\n\n // Tier 2 Optimizations (controlled approximations)\n tier2?: {\n // Temporal constraints: ordering requirements between messages\n temporalConstraints?: Array<{\n before: string; // Message type that must occur first\n after: string; // Message type that must occur after\n description?: string; // Human-readable description\n }>;\n\n // Bounded exploration: limit depth for specific scenarios\n boundedExploration?: {\n maxDepth?: number; // Maximum state depth to explore\n criticalPaths?: string[][]; // Sequences of message types that must be fully explored\n };\n };\n\n // polly#160: directional capability invariants + write-coupling lint groups\n capabilities?: CapabilityConfig[];\n coupledFields?: string[][];\n}\n\n// Adapter-based configuration (for future use)\ninterface AdapterVerificationConfig {\n adapter: unknown; // Adapter interface not exported to avoid heavy deps\n state: Record<string, unknown>;\n /**\n * polly#185: the contexts this project actually has, emitted as the `.cfg`'s\n * `Contexts` set.\n *\n * Every generated model replicates application state across the set and\n * quantifies each send over `|Contexts| * (2^|Contexts| - 1)` source/target\n * pairs, so its size is the cheapest term in the state space to cut.\n * Measured on a two-handler model at `maxInFlight: 1`: 38,464 distinct\n * states at three contexts, 368 at one.\n *\n * Names become TLA+ model values, so each must sanitise to\n * `[A-Za-z_][A-Za-z0-9_]*`. Omit the key to keep the browser-extension\n * default `[\"background\", \"content\", \"popup\"]`.\n *\n * A single context makes every send a self-send, and disables\n * `PropagateMeshOp` (which requires two distinct contexts) — so a `mesh`\n * block wants at least two.\n *\n * @example\n * ```ts\n * defineVerification({\n * contexts: [\"server\", \"client\"],\n * state: { ready: { type: \"boolean\" } },\n * messages: { maxInFlight: 1 },\n * });\n * ```\n */\n contexts?: string[];\n\n bounds?: {\n maxInFlight?: number;\n [key: string]: unknown;\n };\n onBuild?: \"warn\" | \"error\" | \"off\";\n\n // polly#160: directional capability invariants + write-coupling lint groups\n capabilities?: CapabilityConfig[];\n coupledFields?: string[][];\n}\n\n// Union type for both config formats\ntype UnifiedVerificationConfig = LegacyVerificationConfig | AdapterVerificationConfig;\n\n/**\n * Define verification configuration with type checking\n *\n * Used in generated verification.config.ts files.\n *\n * @example\n * ```typescript\n * import { defineVerification } from '@fairfox/polly/verify'\n *\n * export default defineVerification({\n * state: {\n * \"user.role\": { type: \"enum\", values: [\"admin\", \"user\", \"guest\"] },\n * },\n * messages: {\n * maxInFlight: 6,\n * maxTabs: 2,\n * },\n * })\n * ```\n */\nexport function defineVerification<T extends UnifiedVerificationConfig>(config: T): T {\n // Validate configuration structure\n if (\"adapter\" in config) {\n // New adapter-based format\n if (!config.adapter) {\n throw new Error(\"Configuration must include an adapter\");\n }\n if (!config.state) {\n throw new Error(\"Configuration must include state bounds\");\n }\n } else if (\"messages\" in config) {\n // Legacy format\n if (!config.state) {\n throw new Error(\"Configuration must include state bounds\");\n }\n if (!config.messages) {\n throw new Error(\"Legacy configuration must include messages bounds\");\n }\n } else {\n throw new Error(\n \"Invalid configuration format. Must include either 'adapter' (new format) or 'messages' (legacy format)\"\n );\n }\n\n return config;\n}\n\n// Re-export verification primitives for user code\nexport {\n $constraints,\n ensures,\n forAllPeers,\n hasLength,\n inRange,\n oneOf,\n requires,\n somePeer,\n stateConstraint,\n} from \"./primitives/index.js\";\n"
5
+ "// Verification primitives for formal verification.\n//\n// `requires` / `ensures` are runtime no-ops in production: the caller\n// evaluates the condition, the primitive discards it, and only the verifier\n// reads the call. Under POLLY_CONTRACTS (set by `polly mutate`, or by hand)\n// a false condition throws a ContractViolation instead, so every contract\n// doubles as an assertion in whatever test drives its handler.\n\n/**\n * POLLY_CONTRACTS: off (default) keeps requires/ensures as no-ops; \"ensures\"\n * makes a false postcondition throw; \"all\" makes a false precondition throw too.\n */\nexport type ContractsMode = \"off\" | \"ensures\" | \"all\";\n\nexport type ContractPrimitive = \"requires\" | \"ensures\";\n\n/**\n * Where violations are recorded before they are thrown, so a harness can fail\n * a test even when the caller (the message bus) swallowed the throw. The\n * mutate preload and the BDD runner read this key; they repeat the string\n * because they import no module of this package.\n */\nexport const CONTRACT_VIOLATIONS_KEY = \"__pollyContractViolations\";\n\n/** Read once at load; browser bundles have no `process`, so they stay off. */\nfunction readContractsMode(): ContractsMode {\n const v =\n typeof process !== \"undefined\" && process.env ? process.env[\"POLLY_CONTRACTS\"] : undefined;\n return v === \"all\" || v === \"ensures\" ? v : \"off\";\n}\n\nconst MODE: ContractsMode = readContractsMode();\n\n/** The mode this process runs under (read from POLLY_CONTRACTS at load). */\nexport function contractsMode(): ContractsMode {\n return MODE;\n}\n\n/**\n * A false `requires` / `ensures` under POLLY_CONTRACTS. The message starts\n * with a marker the mutate engine reads out of junit failure text:\n * `[polly-contract ensures /abs/handlers.ts:50] the author's message`.\n */\nexport class ContractViolation extends Error {\n override readonly name = \"ContractViolation\";\n constructor(\n readonly primitive: ContractPrimitive,\n /** `file:line` of the call, absolute. */\n readonly site: string,\n readonly contractMessage?: string\n ) {\n super(\n `[polly-contract ${primitive} ${site}]${contractMessage === undefined ? \"\" : ` ${contractMessage}`}`\n );\n }\n}\n\nconst OWN_FILE = import.meta.url.startsWith(\"file://\")\n ? decodeURIComponent(import.meta.url.slice(\"file://\".length))\n : import.meta.url;\nconst FRAME = /\\(?([^()\\s]+):(\\d+):\\d+\\)?\\s*$/;\n\n/**\n * `file:line` of the `requires` / `ensures` call: the first stack frame\n * outside this module. When this module is bundled with its caller every\n * frame is \"own\", so the fourth frame (callSite, violate, the primitive,\n * caller) is taken instead.\n */\nfunction callSite(): string {\n const frames: { file: string; line: string }[] = [];\n for (const line of (new Error().stack ?? \"\").split(\"\\n\")) {\n const m = FRAME.exec(line);\n if (m?.[1] && m[2]) frames.push({ file: m[1], line: m[2] });\n }\n const outside = frames.find((f) => f.file !== OWN_FILE) ?? frames[3];\n return outside ? `${outside.file}:${outside.line}` : \"unknown:0\";\n}\n\nfunction violationList(): ContractViolation[] {\n const g = globalThis as Record<string, unknown>;\n const existing = g[CONTRACT_VIOLATIONS_KEY];\n if (Array.isArray(existing)) return existing as ContractViolation[];\n const list: ContractViolation[] = [];\n g[CONTRACT_VIOLATIONS_KEY] = list;\n return list;\n}\n\nfunction violate(primitive: ContractPrimitive, condition: boolean, message?: string): void {\n if (condition) return;\n if (MODE === \"off\" || (MODE === \"ensures\" && primitive === \"requires\")) return;\n const violation = new ContractViolation(primitive, callSite(), message);\n violationList().push(violation);\n throw violation;\n}\n\n/** Return every violation recorded since the last drain, and clear the list. */\nexport function drainContractViolations(): ContractViolation[] {\n const list = violationList();\n return list.splice(0, list.length);\n}\n\n/**\n * Assert a precondition that must be true when the handler executes.\n *\n * In production: No-op (compiled away)\n * In verification: Translated to TLA+ assertion\n * Under POLLY_CONTRACTS=all: throws ContractViolation when false\n *\n * @example\n * messageBus.on(\"USER_LOGIN\", (payload) => {\n * requires(state.user.loggedIn === false, \"User must not be logged in\")\n * state.user.loggedIn = true\n * })\n */\nexport function requires(condition: boolean, message?: string): void {\n // No-op unless POLLY_CONTRACTS is \"all\"; the verifier reads the call statically.\n violate(\"requires\", condition, message);\n}\n\n/**\n * Assert a postcondition that must be true after the handler completes.\n *\n * In production: No-op (compiled away)\n * In verification: Translated to TLA+ assertion\n * Under POLLY_CONTRACTS=ensures|all: throws ContractViolation when false\n *\n * @example\n * messageBus.on(\"USER_LOGIN\", (payload) => {\n * state.user.loggedIn = true\n * ensures(state.user.loggedIn === true, \"User must be logged in\")\n * })\n */\nexport function ensures(condition: boolean, message?: string): void {\n // No-op unless POLLY_CONTRACTS is set; the verifier reads the call statically.\n violate(\"ensures\", condition, message);\n}\n\n/**\n * Define a global invariant that must always hold.\n *\n * In production: No-op (compiled away)\n * In verification: Translated to TLA+ invariant\n *\n * @example\n * invariant(\"UserIdConsistent\", () =>\n * state.user.loggedIn === false || state.user.id !== null\n * )\n */\nexport function invariant(_name: string, condition: () => boolean): void {\n // Runtime no-op - only used during verification\n // Name and condition are checked during static analysis only\n void condition;\n}\n\n/**\n * Assert that a value is within a valid range.\n *\n * @example\n * requires(inRange(todoCount, 0, 100), \"Todo count must be 0-100\")\n */\nexport function inRange(value: number, min: number, max: number): boolean {\n return value >= min && value <= max;\n}\n\n/**\n * Assert that a value is one of the allowed values.\n *\n * @example\n * requires(oneOf(state.user.role, [\"admin\", \"user\"]), \"Role must be admin or user\")\n */\nexport function oneOf<T>(value: T, allowed: T[]): boolean {\n return allowed.includes(value);\n}\n\n/**\n * Assert that an array has a specific length constraint.\n *\n * @example\n * requires(hasLength(state.todos, { max: 10 }), \"Too many todos\")\n */\nexport function hasLength(array: unknown[], constraint: { min?: number; max?: number }): boolean {\n if (constraint.min !== undefined && array.length < constraint.min) return false;\n if (constraint.max !== undefined && array.length > constraint.max) return false;\n return true;\n}\n\n/**\n * Declare state-level constraints for verification and optional runtime checking.\n * Maps message types to preconditions on state fields.\n *\n * The parser automatically wires these constraints to handlers during verification.\n * Optionally, constraints can be enforced at runtime by passing `{ runtime: true }`.\n *\n * @example\n * // Verification only (TLA+ generation)\n * const state = { loggedIn: false };\n *\n * $constraints(\"loggedIn\", {\n * USER_LOGOUT: { requires: \"state.loggedIn === true\", message: \"Must be logged in\" },\n * BOOKMARK_ADD: { requires: \"state.loggedIn === true\", message: \"Must be logged in\" },\n * });\n *\n * @example\n * // Runtime enforcement (function predicates)\n * $constraints(\"loggedIn\", {\n * USER_LOGOUT: {\n * requires: (state) => state.loggedIn === true,\n * message: \"Must be logged in to logout\"\n * },\n * }, { runtime: true });\n */\nexport function $constraints(\n stateField: string,\n constraints: Record<\n string,\n {\n requires?: string | ((state: unknown) => boolean);\n ensures?: string | ((state: unknown) => boolean);\n message?: string;\n }\n >,\n options?: { runtime?: boolean }\n): void {\n // Register constraints for runtime checking if enabled\n if (options?.runtime) {\n // Import dynamically to avoid circular dependencies\n // This is safe because it only happens at runtime, not during static analysis\n // @ts-expect-error - Dynamic import path resolves correctly at runtime\n import(\"../../../src/shared/lib/constraints.js\")\n .then(({ registerConstraints }) => {\n registerConstraints(stateField, constraints);\n })\n .catch(() => {\n // Silently ignore - constraints module may not be available during static analysis\n });\n }\n\n // For verification: Still a no-op at runtime\n // Parser extracts these and wires them to TLA+ handlers\n}\n\n/**\n * Declare a global state constraint that prunes structurally impossible states.\n *\n * In production: No-op (compiled away)\n * In verification: Translated to TLC CONSTRAINT clause, discarding states\n * that violate the predicate from the exploration queue entirely.\n *\n * Unlike `invariant()` (which checks but still explores), `stateConstraint()`\n * prevents the model checker from ever reaching the pruned states.\n *\n * @example\n * stateConstraint(\"LeaderRequiresConnection\", () =>\n * !connectionState.value.isLeader || connectionState.value.status === \"connected\"\n * )\n */\nexport function stateConstraint(\n name: string,\n predicate: () => boolean,\n options?: { message?: string }\n): void {\n void name;\n void predicate;\n void options;\n}\n\n/**\n * Cross-peer universal quantifier for use inside `requires` and\n * `ensures` predicates. The runtime is a no-op that simply runs the\n * predicate against a single dummy context (so the assertion never\n * triggers at runtime); the verifier recognizes the wrapper in\n * predicate source and emits a `\\A peer \\in Contexts \\ {ctx} : (...)`\n * clause that asks TLC to check the inner predicate against every\n * other context.\n *\n * @example\n * ensures(\n * forAllPeers(peer => peer.todos.value.length === todos.value.length),\n * \"every peer agrees on todo count\"\n * );\n */\nexport function forAllPeers<TPeer>(predicate: (peer: TPeer) => boolean): boolean {\n void predicate;\n return true;\n}\n\n/**\n * Cross-peer existential quantifier for use inside `requires` and\n * `ensures` predicates. Runtime no-op; the verifier emits a\n * `\\E peer \\in Contexts \\ {ctx} : (...)` clause.\n *\n * @example\n * ensures(\n * somePeer(peer => peer.user.value.loggedIn === true),\n * \"at least one peer has a logged-in user\"\n * );\n */\nexport function somePeer<TPeer>(predicate: (peer: TPeer) => boolean): boolean {\n void predicate;\n return true;\n}\n\n// Re-export for convenience\nexport const verify = {\n requires,\n ensures,\n invariant,\n inRange,\n oneOf,\n hasLength,\n $constraints,\n stateConstraint,\n forAllPeers,\n somePeer,\n};\n",
6
+ "// Configuration Helper for @fairfox/polly/verify\n//\n// Lightweight entry point for user configuration files.\n// Does NOT include heavy dependencies (ts-morph, analysis, etc.)\n// which are only needed by the CLI tool.\n\n// Configuration Types (inlined to avoid heavy dependencies)\n\n// polly#160: a directional capability whose grant requires a precondition.\n// Desugars to a TLA+ safety invariant `(enabledBy) => (requires)`. enabledBy /\n// requires are TS boolean expressions in the requires()/ensures() dialect\n// referencing state via `state.` / signal `.value` form (e.g. \"state.authReady\").\n// Inlined here (like SubsystemConfig) so this authoring entry point stays\n// dependency-light; the canonical type is CapabilityConfig in config/types.ts.\ninterface CapabilityConfig {\n name: string;\n enabledBy: string;\n requires: string;\n message?: string;\n}\n\n// Subsystem configuration for compositional verification\ninterface SubsystemConfig {\n state: string[]; // Field names from parent state config\n handlers: string[]; // Message type names\n // Per-subsystem message bounds; override messages.maxInFlight and merge\n // into messages.perMessageBounds for this subsystem only.\n bounds?: {\n maxInFlight?: number;\n perMessageBounds?: Record<string, number>;\n };\n /** polly#171: check message-resolution liveness for this subsystem only. */\n liveness?: boolean;\n}\n\n// Point a subsystem's reachability witnesses at a hand-written TLA+ spec instead\n// of the generated UserApp_<subsystem>. Inlined here (like SubsystemConfig) to\n// keep this authoring entry point dependency-light; canonical type is\n// CustomTLAPath in config/types.ts.\ninterface CustomTLAPath {\n tla: string; // path (relative to cwd) to the hand-written .tla the witness EXTENDS\n cfg: string; // path (relative to cwd) to its .cfg\n module?: string; // module to EXTEND; defaults to the MODULE name parsed from `tla`\n fields?: Record<string, string>; // BDD field name → this spec's TLA+ variable name\n}\n\n// Legacy verification configuration\ninterface LegacyVerificationConfig {\n state: Record<string, unknown>;\n /**\n * polly#185: the contexts this project actually has, emitted as the `.cfg`'s\n * `Contexts` set.\n *\n * Every generated model replicates application state across the set and\n * quantifies each send over `|Contexts| * (2^|Contexts| - 1)` source/target\n * pairs, so its size is the cheapest term in the state space to cut.\n * Measured on a two-handler model at `maxInFlight: 1`: 38,464 distinct\n * states at three contexts, 368 at one.\n *\n * Names become TLA+ model values, so each must sanitise to\n * `[A-Za-z_][A-Za-z0-9_]*`. Omit the key to keep the browser-extension\n * default `[\"background\", \"content\", \"popup\"]`.\n *\n * A single context makes every send a self-send, and disables\n * `PropagateMeshOp` (which requires two distinct contexts) — so a `mesh`\n * block wants at least two.\n *\n * @example\n * ```ts\n * defineVerification({\n * contexts: [\"server\", \"client\"],\n * state: { ready: { type: \"boolean\" } },\n * messages: { maxInFlight: 1 },\n * });\n * ```\n */\n contexts?: string[];\n\n /**\n * polly#117: optional mesh-document declarations. When present, each\n * key names a `$meshState` document and its value declares the\n * field-level state schema for that document. The verifier emits a\n * separate slot in `contextStates[ctx].mesh[<docId>]` for these\n * fields and adds a `PropagateMeshOp` action that allows the doc's\n * value on one context to flow to another — modelling Automerge\n * sync between peers. Mesh references inside `forAllPeers` quantifiers\n * route through this slot so cross-peer convergence claims are\n * actually checked.\n *\n * @example\n * ```ts\n * defineVerification({\n * state: { localCounter: { type: \"number\", min: 0, max: 3 } },\n * mesh: {\n * todos: {\n * entries: { type: \"enum\", values: [\"empty\", \"one\", \"many\"] },\n * },\n * },\n * messages: { maxInFlight: 2 },\n * });\n * ```\n */\n mesh?: Record<string, Record<string, unknown>>;\n messages: {\n // Basic bounds\n maxInFlight?: number;\n maxTabs?: number;\n maxClients?: number;\n maxRenderers?: number;\n maxWorkers?: number;\n maxContexts?: number;\n\n // Tier 1 Optimizations (no precision loss)\n include?: string[]; // Only verify these message types\n exclude?: string[]; // Exclude these message types (mutually exclusive with include)\n symmetry?: string[][]; // Groups of symmetric message types [[type1, type2], [type3, type4]]\n perMessageBounds?: Record<string, number>; // Different maxInFlight per message type\n };\n onBuild?: \"warn\" | \"error\" | \"off\";\n onRelease?: \"warn\" | \"error\" | \"off\";\n\n // Verification engine options\n verification?: {\n timeout?: number; // Timeout in seconds (0 = no timeout)\n workers?: number; // Number of TLC workers\n /**\n * polly#181: docker-style container memory ceiling for the TLC run, e.g.\n * \"8g\". The JVM heap is sized under it, so a model too large for the\n * ceiling ends with a TLC out-of-memory message instead of a silent kill.\n * Unset uses a fixed default (4g), never unbounded.\n */\n memory?: string;\n };\n\n /**\n * polly#171: check that nothing sent stays pending for ever.\n *\n * Off by default because it is expensive: it adds a temporal property and the\n * routing fairness it needs, and TLC must then explore behaviours rather than\n * only reachable states. Measured on an 11-subsystem consumer model, the full\n * verify gate went from ~110s to ~512s.\n *\n * Every property the generator emits without this is a SAFETY property, and a\n * safety property is true of a system that does nothing at all — so a spec in\n * which routing has stopped verifies identically to one in which it works.\n * Turn this on for the subsystems whose progress actually matters.\n *\n * LIMITATION, and it is Lamport's own warning (Specifying Systems 14.3.5):\n * generated specs also declare a CONSTRAINT to bound the state space, and TLC\n * prints \"Declaring state or action constraints during liveness checking is\n * dangerous\" when both are present. A constraint truncates behaviours at its\n * boundary, so a liveness violation reachable only beyond that boundary is not\n * found. Treat a green liveness run as \"no wedge within the bounds\", not as a\n * proof of progress — the same reading every bounded model-checking result\n * deserves.\n */\n liveness?: boolean;\n\n // Subsystem-scoped verification (compositional)\n subsystems?: Record<string, SubsystemConfig>;\n\n // Bind a subsystem's witnesses to a hand-written .tla/.cfg (read only by the\n // --witness pass; a custom subsystem is skipped during generated-spec verify).\n customTLAPaths?: Record<string, CustomTLAPath>;\n\n // Tier 2 Optimizations (controlled approximations)\n tier2?: {\n // Temporal constraints: ordering requirements between messages\n temporalConstraints?: Array<{\n before: string; // Message type that must occur first\n after: string; // Message type that must occur after\n description?: string; // Human-readable description\n }>;\n\n // Bounded exploration: limit depth for specific scenarios\n boundedExploration?: {\n maxDepth?: number; // Maximum state depth to explore\n criticalPaths?: string[][]; // Sequences of message types that must be fully explored\n };\n };\n\n // polly#160: directional capability invariants + write-coupling lint groups\n capabilities?: CapabilityConfig[];\n coupledFields?: string[][];\n}\n\n// Adapter-based configuration (for future use)\ninterface AdapterVerificationConfig {\n adapter: unknown; // Adapter interface not exported to avoid heavy deps\n state: Record<string, unknown>;\n /**\n * polly#185: the contexts this project actually has, emitted as the `.cfg`'s\n * `Contexts` set.\n *\n * Every generated model replicates application state across the set and\n * quantifies each send over `|Contexts| * (2^|Contexts| - 1)` source/target\n * pairs, so its size is the cheapest term in the state space to cut.\n * Measured on a two-handler model at `maxInFlight: 1`: 38,464 distinct\n * states at three contexts, 368 at one.\n *\n * Names become TLA+ model values, so each must sanitise to\n * `[A-Za-z_][A-Za-z0-9_]*`. Omit the key to keep the browser-extension\n * default `[\"background\", \"content\", \"popup\"]`.\n *\n * A single context makes every send a self-send, and disables\n * `PropagateMeshOp` (which requires two distinct contexts) — so a `mesh`\n * block wants at least two.\n *\n * @example\n * ```ts\n * defineVerification({\n * contexts: [\"server\", \"client\"],\n * state: { ready: { type: \"boolean\" } },\n * messages: { maxInFlight: 1 },\n * });\n * ```\n */\n contexts?: string[];\n\n bounds?: {\n maxInFlight?: number;\n [key: string]: unknown;\n };\n onBuild?: \"warn\" | \"error\" | \"off\";\n\n // polly#160: directional capability invariants + write-coupling lint groups\n capabilities?: CapabilityConfig[];\n coupledFields?: string[][];\n}\n\n// Union type for both config formats\ntype UnifiedVerificationConfig = LegacyVerificationConfig | AdapterVerificationConfig;\n\n/**\n * Define verification configuration with type checking\n *\n * Used in generated verification.config.ts files.\n *\n * @example\n * ```typescript\n * import { defineVerification } from '@fairfox/polly/verify'\n *\n * export default defineVerification({\n * state: {\n * \"user.role\": { type: \"enum\", values: [\"admin\", \"user\", \"guest\"] },\n * },\n * messages: {\n * maxInFlight: 6,\n * maxTabs: 2,\n * },\n * })\n * ```\n */\nexport function defineVerification<T extends UnifiedVerificationConfig>(config: T): T {\n // Validate configuration structure\n if (\"adapter\" in config) {\n // New adapter-based format\n if (!config.adapter) {\n throw new Error(\"Configuration must include an adapter\");\n }\n if (!config.state) {\n throw new Error(\"Configuration must include state bounds\");\n }\n } else if (\"messages\" in config) {\n // Legacy format\n if (!config.state) {\n throw new Error(\"Configuration must include state bounds\");\n }\n if (!config.messages) {\n throw new Error(\"Legacy configuration must include messages bounds\");\n }\n } else {\n throw new Error(\n \"Invalid configuration format. Must include either 'adapter' (new format) or 'messages' (legacy format)\"\n );\n }\n\n return config;\n}\n\n// Re-export verification primitives for user code\nexport {\n $constraints,\n CONTRACT_VIOLATIONS_KEY,\n type ContractPrimitive,\n type ContractsMode,\n ContractViolation,\n contractsMode,\n drainContractViolations,\n ensures,\n forAllPeers,\n hasLength,\n inRange,\n oneOf,\n requires,\n somePeer,\n stateConstraint,\n} from \"./primitives/index.js\";\n"
7
7
  ],
8
- "mappings": ";AAeO,SAAS,QAAQ,CAAC,WAAoB,SAAwB;AAmB9D,SAAS,OAAO,CAAC,WAAoB,SAAwB;AA8B7D,SAAS,OAAO,CAAC,OAAe,KAAa,KAAsB;AAAA,EACxE,OAAO,SAAS,OAAO,SAAS;AAAA;AAS3B,SAAS,KAAQ,CAAC,OAAU,SAAuB;AAAA,EACxD,OAAO,QAAQ,SAAS,KAAK;AAAA;AASxB,SAAS,SAAS,CAAC,OAAkB,YAAqD;AAAA,EAC/F,IAAI,WAAW,QAAQ,aAAa,MAAM,SAAS,WAAW;AAAA,IAAK,OAAO;AAAA,EAC1E,IAAI,WAAW,QAAQ,aAAa,MAAM,SAAS,WAAW;AAAA,IAAK,OAAO;AAAA,EAC1E,OAAO;AAAA;AA4BF,SAAS,YAAY,CAC1B,YACA,aAQA,SACM;AAAA,EAEN,IAAI,SAAS,SAAS;AAAA,IAIb,iDACJ,KAAK,GAAG,0BAA0B;AAAA,MACjC,oBAAoB,YAAY,WAAW;AAAA,KAC5C,EACA,MAAM,MAAM,EAEZ;AAAA,EACL;AAAA;AAqBK,SAAS,eAAe,CAC7B,MACA,WACA,SACM;AAqBD,SAAS,WAAkB,CAAC,WAA8C;AAAA,EAE/E,OAAO;AAAA;AAcF,SAAS,QAAe,CAAC,WAA8C;AAAA,EAE5E,OAAO;AAAA;;;ACkDF,SAAS,kBAAuD,CAAC,QAAc;AAAA,EAEpF,IAAI,aAAa,QAAQ;AAAA,IAEvB,IAAI,CAAC,OAAO,SAAS;AAAA,MACnB,MAAM,IAAI,MAAM,uCAAuC;AAAA,IACzD;AAAA,IACA,IAAI,CAAC,OAAO,OAAO;AAAA,MACjB,MAAM,IAAI,MAAM,yCAAyC;AAAA,IAC3D;AAAA,EACF,EAAO,SAAI,cAAc,QAAQ;AAAA,IAE/B,IAAI,CAAC,OAAO,OAAO;AAAA,MACjB,MAAM,IAAI,MAAM,yCAAyC;AAAA,IAC3D;AAAA,IACA,IAAI,CAAC,OAAO,UAAU;AAAA,MACpB,MAAM,IAAI,MAAM,mDAAmD;AAAA,IACrE;AAAA,EACF,EAAO;AAAA,IACL,MAAM,IAAI,MACR,wGACF;AAAA;AAAA,EAGF,OAAO;AAAA;",
9
- "debugId": "EB2A6F997258A60464756E2164756E21",
8
+ "mappings": ";AAsBO,IAAM,0BAA0B;AAGvC,SAAS,iBAAiB,GAAkB;AAAA,EAC1C,MAAM,IACJ,OAAO,YAAY,eAAe,QAAQ,MAAM,QAAQ,IAAI,qBAAqB;AAAA,EACnF,OAAO,MAAM,SAAS,MAAM,YAAY,IAAI;AAAA;AAG9C,IAAM,OAAsB,kBAAkB;AAGvC,SAAS,aAAa,GAAkB;AAAA,EAC7C,OAAO;AAAA;AAAA;AAQF,MAAM,0BAA0B,MAAM;AAAA,EAGhC;AAAA,EAEA;AAAA,EACA;AAAA,EALO,OAAO;AAAA,EACzB,WAAW,CACA,WAEA,MACA,iBACT;AAAA,IACA,MACE,mBAAmB,aAAa,QAAQ,oBAAoB,YAAY,KAAK,IAAI,mBACnF;AAAA,IAPS;AAAA,IAEA;AAAA,IACA;AAAA;AAMb;AAEA,IAAM,WAAW,YAAY,IAAI,WAAW,SAAS,IACjD,mBAAmB,YAAY,IAAI,MAAM,UAAU,MAAM,CAAC,IAC1D,YAAY;AAChB,IAAM,QAAQ;AAQd,SAAS,QAAQ,GAAW;AAAA,EAC1B,MAAM,SAA2C,CAAC;AAAA,EAClD,WAAW,SAAS,IAAI,MAAM,EAAE,SAAS,IAAI,MAAM;AAAA,CAAI,GAAG;AAAA,IACxD,MAAM,IAAI,MAAM,KAAK,IAAI;AAAA,IACzB,IAAI,IAAI,MAAM,EAAE;AAAA,MAAI,OAAO,KAAK,EAAE,MAAM,EAAE,IAAI,MAAM,EAAE,GAAG,CAAC;AAAA,EAC5D;AAAA,EACA,MAAM,UAAU,OAAO,KAAK,CAAC,MAAM,EAAE,SAAS,QAAQ,KAAK,OAAO;AAAA,EAClE,OAAO,UAAU,GAAG,QAAQ,QAAQ,QAAQ,SAAS;AAAA;AAGvD,SAAS,aAAa,GAAwB;AAAA,EAC5C,MAAM,IAAI;AAAA,EACV,MAAM,WAAW,EAAE;AAAA,EACnB,IAAI,MAAM,QAAQ,QAAQ;AAAA,IAAG,OAAO;AAAA,EACpC,MAAM,OAA4B,CAAC;AAAA,EACnC,EAAE,2BAA2B;AAAA,EAC7B,OAAO;AAAA;AAGT,SAAS,OAAO,CAAC,WAA8B,WAAoB,SAAwB;AAAA,EACzF,IAAI;AAAA,IAAW;AAAA,EACf,IAAI,SAAS,SAAU,SAAS,aAAa,cAAc;AAAA,IAAa;AAAA,EACxE,MAAM,YAAY,IAAI,kBAAkB,WAAW,SAAS,GAAG,OAAO;AAAA,EACtE,cAAc,EAAE,KAAK,SAAS;AAAA,EAC9B,MAAM;AAAA;AAID,SAAS,uBAAuB,GAAwB;AAAA,EAC7D,MAAM,OAAO,cAAc;AAAA,EAC3B,OAAO,KAAK,OAAO,GAAG,KAAK,MAAM;AAAA;AAgB5B,SAAS,QAAQ,CAAC,WAAoB,SAAwB;AAAA,EAEnE,QAAQ,YAAY,WAAW,OAAO;AAAA;AAgBjC,SAAS,OAAO,CAAC,WAAoB,SAAwB;AAAA,EAElE,QAAQ,WAAW,WAAW,OAAO;AAAA;AA0BhC,SAAS,OAAO,CAAC,OAAe,KAAa,KAAsB;AAAA,EACxE,OAAO,SAAS,OAAO,SAAS;AAAA;AAS3B,SAAS,KAAQ,CAAC,OAAU,SAAuB;AAAA,EACxD,OAAO,QAAQ,SAAS,KAAK;AAAA;AASxB,SAAS,SAAS,CAAC,OAAkB,YAAqD;AAAA,EAC/F,IAAI,WAAW,QAAQ,aAAa,MAAM,SAAS,WAAW;AAAA,IAAK,OAAO;AAAA,EAC1E,IAAI,WAAW,QAAQ,aAAa,MAAM,SAAS,WAAW;AAAA,IAAK,OAAO;AAAA,EAC1E,OAAO;AAAA;AA4BF,SAAS,YAAY,CAC1B,YACA,aAQA,SACM;AAAA,EAEN,IAAI,SAAS,SAAS;AAAA,IAIb,iDACJ,KAAK,GAAG,0BAA0B;AAAA,MACjC,oBAAoB,YAAY,WAAW;AAAA,KAC5C,EACA,MAAM,MAAM,EAEZ;AAAA,EACL;AAAA;AAqBK,SAAS,eAAe,CAC7B,MACA,WACA,SACM;AAqBD,SAAS,WAAkB,CAAC,WAA8C;AAAA,EAE/E,OAAO;AAAA;AAcF,SAAS,QAAe,CAAC,WAA8C;AAAA,EAE5E,OAAO;AAAA;;;AC9CF,SAAS,kBAAuD,CAAC,QAAc;AAAA,EAEpF,IAAI,aAAa,QAAQ;AAAA,IAEvB,IAAI,CAAC,OAAO,SAAS;AAAA,MACnB,MAAM,IAAI,MAAM,uCAAuC;AAAA,IACzD;AAAA,IACA,IAAI,CAAC,OAAO,OAAO;AAAA,MACjB,MAAM,IAAI,MAAM,yCAAyC;AAAA,IAC3D;AAAA,EACF,EAAO,SAAI,cAAc,QAAQ;AAAA,IAE/B,IAAI,CAAC,OAAO,OAAO;AAAA,MACjB,MAAM,IAAI,MAAM,yCAAyC;AAAA,IAC3D;AAAA,IACA,IAAI,CAAC,OAAO,UAAU;AAAA,MACpB,MAAM,IAAI,MAAM,mDAAmD;AAAA,IACrE;AAAA,EACF,EAAO;AAAA,IACL,MAAM,IAAI,MACR,wGACF;AAAA;AAAA,EAGF,OAAO;AAAA;",
9
+ "debugId": "BFA2A054CE121A2F64756E2164756E21",
10
10
  "names": []
11
11
  }
@@ -1,8 +1,41 @@
1
+ /**
2
+ * POLLY_CONTRACTS: off (default) keeps requires/ensures as no-ops; "ensures"
3
+ * makes a false postcondition throw; "all" makes a false precondition throw too.
4
+ */
5
+ export type ContractsMode = "off" | "ensures" | "all";
6
+ export type ContractPrimitive = "requires" | "ensures";
7
+ /**
8
+ * Where violations are recorded before they are thrown, so a harness can fail
9
+ * a test even when the caller (the message bus) swallowed the throw. The
10
+ * mutate preload and the BDD runner read this key; they repeat the string
11
+ * because they import no module of this package.
12
+ */
13
+ export declare const CONTRACT_VIOLATIONS_KEY = "__pollyContractViolations";
14
+ /** The mode this process runs under (read from POLLY_CONTRACTS at load). */
15
+ export declare function contractsMode(): ContractsMode;
16
+ /**
17
+ * A false `requires` / `ensures` under POLLY_CONTRACTS. The message starts
18
+ * with a marker the mutate engine reads out of junit failure text:
19
+ * `[polly-contract ensures /abs/handlers.ts:50] the author's message`.
20
+ */
21
+ export declare class ContractViolation extends Error {
22
+ readonly primitive: ContractPrimitive;
23
+ /** `file:line` of the call, absolute. */
24
+ readonly site: string;
25
+ readonly contractMessage?: string | undefined;
26
+ readonly name = "ContractViolation";
27
+ constructor(primitive: ContractPrimitive,
28
+ /** `file:line` of the call, absolute. */
29
+ site: string, contractMessage?: string | undefined);
30
+ }
31
+ /** Return every violation recorded since the last drain, and clear the list. */
32
+ export declare function drainContractViolations(): ContractViolation[];
1
33
  /**
2
34
  * Assert a precondition that must be true when the handler executes.
3
35
  *
4
36
  * In production: No-op (compiled away)
5
37
  * In verification: Translated to TLA+ assertion
38
+ * Under POLLY_CONTRACTS=all: throws ContractViolation when false
6
39
  *
7
40
  * @example
8
41
  * messageBus.on("USER_LOGIN", (payload) => {
@@ -16,6 +49,7 @@ export declare function requires(condition: boolean, message?: string): void;
16
49
  *
17
50
  * In production: No-op (compiled away)
18
51
  * In verification: Translated to TLA+ assertion
52
+ * Under POLLY_CONTRACTS=ensures|all: throws ContractViolation when false
19
53
  *
20
54
  * @example
21
55
  * messageBus.on("USER_LOGIN", (payload) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fairfox/polly",
3
- "version": "0.91.0",
3
+ "version": "0.92.0",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "Multi-execution-context framework with reactive state and cross-context messaging for Chrome extensions, PWAs, and worker-based applications",
@@ -58,10 +58,6 @@
58
58
  "import": "./dist/tools/verify/src/config.js",
59
59
  "types": "./dist/tools/verify/src/config.d.ts"
60
60
  },
61
- "./stryker": {
62
- "import": "./dist/tools/verify/src/stryker/index.js",
63
- "types": "./dist/tools/verify/src/stryker/index.d.ts"
64
- },
65
61
  "./mutate": {
66
62
  "import": "./dist/tools/mutate/src/index.js",
67
63
  "types": "./dist/tools/mutate/src/index.d.ts"
@@ -155,7 +151,7 @@
155
151
  "build:registry": "bun scripts/build-polly-ui-registry.ts",
156
152
  "build:full-featured": "bun run scripts/build-user-extension.ts ../../examples/full-featured",
157
153
  "build:full-featured:prod": "bun run scripts/build-user-extension.ts ../../examples/full-featured --prod",
158
- "typecheck": "bunx tsc --noEmit && bun run --cwd tests typecheck",
154
+ "typecheck": "bun --check && bun run --cwd tests typecheck",
159
155
  "typecheck:tests": "bun run --cwd tests typecheck",
160
156
  "lint": "biome check .",
161
157
  "lint:fix": "biome check --write .",
@@ -172,43 +168,41 @@
172
168
  "publish:public": "bun run scripts/publish.ts"
173
169
  },
174
170
  "devDependencies": {
175
- "@automerge/automerge-repo": "2.5.5",
176
- "@automerge/automerge-repo-network-websocket": "2.5.5",
177
- "@automerge/automerge-repo-storage-indexeddb": "2.5.5",
178
- "@automerge/automerge-repo-storage-nodefs": "2.5.5",
179
- "@babel/parser": "7.29.3",
180
- "@babel/traverse": "7.29.0",
181
- "@biomejs/biome": "2.4.14",
182
- "@stryker-mutator/api": "9.6.1",
171
+ "@automerge/automerge-repo": "2.5.6",
172
+ "@automerge/automerge-repo-network-websocket": "2.5.6",
173
+ "@automerge/automerge-repo-storage-indexeddb": "2.5.6",
174
+ "@automerge/automerge-repo-storage-nodefs": "2.5.6",
175
+ "@biomejs/biome": "2.5.15",
183
176
  "@elysiajs/eden": "1.4.9",
184
- "@preact/signals": "2.9.0",
185
- "@preact/signals-core": "1.14.2",
186
- "@types/bun": "1.3.13",
187
- "@types/chrome": "0.1.40",
188
- "@types/pixelmatch": "5.2.6",
177
+ "@preact/signals": "2.11.3",
178
+ "@preact/signals-core": "1.14.4",
179
+ "@types/bun": "1.4.3",
180
+ "@types/chrome": "0.3.5",
181
+ "@types/pixelmatch": "7.1.0",
189
182
  "@types/pngjs": "6.0.5",
190
- "@types/ws": "8.18.1",
191
- "dompurify": "3.4.14",
192
- "elysia": "1.4.28",
193
- "fast-check": "4.7.0",
194
- "marked": "18.0.3",
195
- "pixelmatch": "7.2.0",
183
+ "@types/ws": "8.18.2",
184
+ "dompurify": "3.4.17",
185
+ "elysia": "1.4.30",
186
+ "fast-check": "4.10.2",
187
+ "marked": "18.1.1",
188
+ "pixelmatch": "8.0.0",
196
189
  "pngjs": "7.0.0",
197
- "preact": "10.29.1",
198
- "puppeteer": "24.42.0",
199
- "tweetnacl": "1.0.3",
200
- "typescript": "6.0.3"
190
+ "preact": "10.29.8",
191
+ "puppeteer": "25.14.0",
192
+ "tweetnacl": "1.0.3"
201
193
  },
202
194
  "engines": {
203
- "bun": ">=1.2.0"
195
+ "bun": ">=1.4.3"
204
196
  },
205
197
  "dependencies": {
206
- "@cucumber/gherkin": "40.0.0",
207
- "@cucumber/messages": "33.0.2",
208
- "ts-morph": "28.0.0"
198
+ "@cucumber/gherkin": "42.0.1",
199
+ "@cucumber/messages": "34.2.1",
200
+ "oxc-parser": "0.137.0",
201
+ "typescript-7": "npm:typescript@7.0.2",
202
+ "ts-morph": "28.0.0",
203
+ "weapon-regex": "1.3.6"
209
204
  },
210
205
  "peerDependencies": {
211
- "typescript": ">= 5.9.3",
212
206
  "preact": "^10.28.1",
213
207
  "@preact/signals": "^2.5.1",
214
208
  "@preact/signals-core": "^1.14.0",
@@ -224,13 +218,9 @@
224
218
  "werift": ">= 0.19.0",
225
219
  "@roamhq/wrtc": ">= 0.8.0",
226
220
  "marked": ">= 12.0.0",
227
- "dompurify": ">= 3.0.0",
228
- "@stryker-mutator/api": ">= 9.0.0"
221
+ "dompurify": ">= 3.0.0"
229
222
  },
230
223
  "peerDependenciesMeta": {
231
- "@stryker-mutator/api": {
232
- "optional": true
233
- },
234
224
  "elysia": {
235
225
  "optional": true
236
226
  },
@@ -1,32 +0,0 @@
1
- import type { Ignorer, NodePath } from "@stryker-mutator/api/ignore";
2
- import { PluginKind } from "@stryker-mutator/api/plugin";
3
- /**
4
- * The polly verify primitives whose argument expressions are runtime no-ops.
5
- * A mutation anywhere inside a call to one of these cannot be killed by a test,
6
- * so its mutants are excluded from scoring.
7
- */
8
- export declare const VERIFY_PRIMITIVES: ReadonlySet<string>;
9
- /** The Stryker plugin name consumers reference in `ignorers`. */
10
- export declare const POLLY_VERIFY_IGNORER_NAME = "polly-verify";
11
- /**
12
- * A Stryker `Ignore` plugin. Stryker calls `shouldIgnore` on entering each AST
13
- * node; returning a message marks that node — and every descendant, until the
14
- * node is left — as ignored. So matching the verify `CallExpression` itself
15
- * covers its condition and message arguments in one shot.
16
- */
17
- export declare class PollyVerifyIgnorer implements Ignorer {
18
- private readonly primitives;
19
- constructor(primitives?: ReadonlySet<string>);
20
- shouldIgnore(path: NodePath): string | undefined;
21
- }
22
- /** The plugin array Stryker reads when this module is listed in `plugins`. */
23
- export declare const strykerPlugins: import("@stryker-mutator/api/plugin").FactoryPlugin<PluginKind.Ignore, ["options"]>[];
24
- /**
25
- * A partial Stryker config that wires up the ignorer. Spread it into a
26
- * `stryker.conf.mjs` default export, or replicate its two keys in JSON.
27
- */
28
- export declare const pollyStrykerPreset: {
29
- readonly plugins: readonly ["@fairfox/polly/stryker"];
30
- readonly ignorers: readonly ["polly-verify"];
31
- };
32
- export default pollyStrykerPreset;
@@ -1,86 +0,0 @@
1
- // tools/verify/src/stryker/index.ts
2
- import {
3
- commonTokens,
4
- declareFactoryPlugin,
5
- PluginKind,
6
- tokens
7
- } from "@stryker-mutator/api/plugin";
8
- var VERIFY_PRIMITIVES = new Set([
9
- "requires",
10
- "ensures",
11
- "invariant",
12
- "stateConstraint",
13
- "forAllPeers",
14
- "somePeer"
15
- ]);
16
- var POLLY_VERIFY_IGNORER_NAME = "polly-verify";
17
- function isCallExpressionPath(path) {
18
- return "isCallExpression" in path && typeof path.isCallExpression === "function";
19
- }
20
- function isBabelIdentifier(callee) {
21
- return callee.type === "Identifier";
22
- }
23
- function isBabelMemberExpression(callee) {
24
- return callee.type === "MemberExpression";
25
- }
26
- function calleeName(callee) {
27
- if (!callee)
28
- return;
29
- if (isBabelIdentifier(callee)) {
30
- return callee.name;
31
- }
32
- if (isBabelMemberExpression(callee)) {
33
- if (!callee.computed && callee.property.type === "Identifier") {
34
- return callee.property.name;
35
- }
36
- }
37
- return;
38
- }
39
-
40
- class PollyVerifyIgnorer {
41
- primitives;
42
- constructor(primitives = VERIFY_PRIMITIVES) {
43
- this.primitives = primitives;
44
- }
45
- shouldIgnore(path) {
46
- if (!isCallExpressionPath(path) || !path.isCallExpression()) {
47
- return;
48
- }
49
- const name = calleeName(path.node.callee);
50
- if (name && this.primitives.has(name)) {
51
- return `Inside polly's ${name}(...) — a runtime no-op (compiled away in production, ` + `translated to a TLA+ assertion in verification). No test can observe or kill ` + `mutations here, so they are excluded from the score (polly#143).`;
52
- }
53
- return;
54
- }
55
- }
56
- function isEnabled(options) {
57
- const polly = options["polly"];
58
- if (typeof polly !== "object" || polly === null)
59
- return true;
60
- return !("excludeVerifyCallsites" in polly) || polly.excludeVerifyCallsites !== false;
61
- }
62
- var NOOP_IGNORER = { shouldIgnore: () => {
63
- return;
64
- } };
65
- function pollyVerifyIgnorerFactory(options) {
66
- return isEnabled(options) ? new PollyVerifyIgnorer : NOOP_IGNORER;
67
- }
68
- pollyVerifyIgnorerFactory.inject = tokens(commonTokens.options);
69
- var strykerPlugins = [
70
- declareFactoryPlugin(PluginKind.Ignore, POLLY_VERIFY_IGNORER_NAME, pollyVerifyIgnorerFactory)
71
- ];
72
- var pollyStrykerPreset = {
73
- plugins: ["@fairfox/polly/stryker"],
74
- ignorers: [POLLY_VERIFY_IGNORER_NAME]
75
- };
76
- var stryker_default = pollyStrykerPreset;
77
- export {
78
- POLLY_VERIFY_IGNORER_NAME,
79
- PollyVerifyIgnorer,
80
- VERIFY_PRIMITIVES,
81
- stryker_default as default,
82
- pollyStrykerPreset,
83
- strykerPlugins
84
- };
85
-
86
- //# debugId=E1FCF7B89389BBE564756E2164756E21
@@ -1,10 +0,0 @@
1
- {
2
- "version": 3,
3
- "sources": ["../tools/verify/src/stryker/index.ts"],
4
- "sourcesContent": [
5
- "// Stryker mutation-testing ignorer for polly's verify primitives (polly#143).\n//\n// `requires`, `ensures`, `invariant`, `stateConstraint`, `forAllPeers`, and\n// `somePeer` are runtime no-ops: in production they compile away, in\n// verification they translate to TLA+ assertions. Nothing observes their\n// condition or message argument at test runtime, so EVERY mutation inside one\n// of these callsites is guaranteed to survive — a string-literal flip in an\n// `ensures(...)` message, an `===` → equality mutation in a `requires(...)`\n// condition. On a downstream project mutating six state-machine specs this\n// dragged the mutation score down to 21%, all of it noise rather than real\n// test-coverage gaps.\n//\n// Polly is the right place to ship this knowledge: it knows which of its\n// primitives are no-ops. This module is a Stryker `Ignore` plugin that marks\n// every mutant inside a verify callsite as ignored, plus a small config preset\n// consumers can spread into their `stryker.conf.*`.\n//\n// Usage (stryker.conf.json):\n//\n// {\n// \"plugins\": [\"@fairfox/polly/stryker\"],\n// \"ignorers\": [\"polly-verify\"]\n// }\n//\n// Or, in stryker.conf.mjs:\n//\n// import pollyStrykerPreset from \"@fairfox/polly/stryker\";\n// export default { ...pollyStrykerPreset, mutate: [\"src/**/*.ts\"] };\n//\n// Set `\"polly\": { \"excludeVerifyCallsites\": false }` to keep the plugin listed\n// but disable the ignoring (e.g. in a shared base config).\n\nimport type { StrykerOptions } from \"@stryker-mutator/api/core\";\nimport type { Ignorer, NodePath } from \"@stryker-mutator/api/ignore\";\nimport {\n commonTokens,\n declareFactoryPlugin,\n PluginKind,\n tokens,\n} from \"@stryker-mutator/api/plugin\";\n\n/**\n * The polly verify primitives whose argument expressions are runtime no-ops.\n * A mutation anywhere inside a call to one of these cannot be killed by a test,\n * so its mutants are excluded from scoring.\n */\nexport const VERIFY_PRIMITIVES: ReadonlySet<string> = new Set([\n \"requires\",\n \"ensures\",\n \"invariant\",\n \"stateConstraint\",\n \"forAllPeers\",\n \"somePeer\",\n]);\n\n/** The Stryker plugin name consumers reference in `ignorers`. */\nexport const POLLY_VERIFY_IGNORER_NAME = \"polly-verify\";\n\n// The Stryker API types `NodePath` as an empty interface; at runtime it is a\n// Babel NodePath. We narrow only the surface we touch — `isCallExpression()`\n// and `node.callee` — without pulling in @babel/types as a dependency.\ninterface BabelIdentifier {\n type: \"Identifier\";\n name: string;\n}\ninterface BabelMemberExpression {\n type: \"MemberExpression\";\n computed: boolean;\n property: { type: string; name?: string };\n}\ntype BabelCallee = BabelIdentifier | BabelMemberExpression | { type: string };\ninterface CallExpressionPath extends NodePath {\n isCallExpression(): boolean;\n node: { callee?: BabelCallee };\n}\n\n/** Narrow the opaque `NodePath` to the Babel surface we touch. */\nfunction isCallExpressionPath(path: NodePath): path is CallExpressionPath {\n return \"isCallExpression\" in path && typeof path.isCallExpression === \"function\";\n}\n\n/**\n * Resolve the simple name of a call's callee, covering both a bare call\n * (`ensures(...)`) and a member call (`verify.ensures(...)` / `polly.ensures(...)`).\n * Computed member access (`obj[\"ensures\"](...)`) is intentionally not matched —\n * it cannot be resolved statically and is not a pattern polly emits.\n */\nfunction isBabelIdentifier(callee: BabelCallee): callee is BabelIdentifier {\n return callee.type === \"Identifier\";\n}\n\nfunction isBabelMemberExpression(callee: BabelCallee): callee is BabelMemberExpression {\n return callee.type === \"MemberExpression\";\n}\n\nfunction calleeName(callee: BabelCallee | undefined): string | undefined {\n if (!callee) return undefined;\n if (isBabelIdentifier(callee)) {\n return callee.name;\n }\n if (isBabelMemberExpression(callee)) {\n if (!callee.computed && callee.property.type === \"Identifier\") {\n return callee.property.name;\n }\n }\n return undefined;\n}\n\n/**\n * A Stryker `Ignore` plugin. Stryker calls `shouldIgnore` on entering each AST\n * node; returning a message marks that node — and every descendant, until the\n * node is left — as ignored. So matching the verify `CallExpression` itself\n * covers its condition and message arguments in one shot.\n */\nexport class PollyVerifyIgnorer implements Ignorer {\n constructor(private readonly primitives: ReadonlySet<string> = VERIFY_PRIMITIVES) {}\n\n shouldIgnore(path: NodePath): string | undefined {\n if (!isCallExpressionPath(path) || !path.isCallExpression()) {\n return undefined;\n }\n const name = calleeName(path.node.callee);\n if (name && this.primitives.has(name)) {\n return (\n `Inside polly's ${name}(...) — a runtime no-op (compiled away in production, ` +\n `translated to a TLA+ assertion in verification). No test can observe or kill ` +\n `mutations here, so they are excluded from the score (polly#143).`\n );\n }\n return undefined;\n }\n}\n\n/** Reads `polly.excludeVerifyCallsites` (default: enabled) off Stryker options. */\nfunction isEnabled(options: StrykerOptions): boolean {\n const polly = options[\"polly\"];\n if (typeof polly !== \"object\" || polly === null) return true;\n return !(\"excludeVerifyCallsites\" in polly) || polly.excludeVerifyCallsites !== false;\n}\n\n// When disabled the plugin still loads but ignores nothing, so a shared config\n// can list it unconditionally and individual projects opt out via options.\nconst NOOP_IGNORER: Ignorer = { shouldIgnore: () => undefined };\n\nfunction pollyVerifyIgnorerFactory(options: StrykerOptions): Ignorer {\n return isEnabled(options) ? new PollyVerifyIgnorer() : NOOP_IGNORER;\n}\npollyVerifyIgnorerFactory.inject = tokens(commonTokens.options);\n\n/** The plugin array Stryker reads when this module is listed in `plugins`. */\nexport const strykerPlugins = [\n declareFactoryPlugin(PluginKind.Ignore, POLLY_VERIFY_IGNORER_NAME, pollyVerifyIgnorerFactory),\n];\n\n/**\n * A partial Stryker config that wires up the ignorer. Spread it into a\n * `stryker.conf.mjs` default export, or replicate its two keys in JSON.\n */\nexport const pollyStrykerPreset = {\n plugins: [\"@fairfox/polly/stryker\"],\n ignorers: [POLLY_VERIFY_IGNORER_NAME],\n} as const;\n\n// biome-ignore lint/style/noDefaultExport: ergonomic `import preset from \"@fairfox/polly/stryker\"`\nexport default pollyStrykerPreset;\n"
6
- ],
7
- "mappings": ";AAkCA;AAAA;AAAA;AAAA;AAAA;AAAA;AAYO,IAAM,oBAAyC,IAAI,IAAI;AAAA,EAC5D;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,CAAC;AAGM,IAAM,4BAA4B;AAqBzC,SAAS,oBAAoB,CAAC,MAA4C;AAAA,EACxE,OAAO,sBAAsB,QAAQ,OAAO,KAAK,qBAAqB;AAAA;AASxE,SAAS,iBAAiB,CAAC,QAAgD;AAAA,EACzE,OAAO,OAAO,SAAS;AAAA;AAGzB,SAAS,uBAAuB,CAAC,QAAsD;AAAA,EACrF,OAAO,OAAO,SAAS;AAAA;AAGzB,SAAS,UAAU,CAAC,QAAqD;AAAA,EACvE,IAAI,CAAC;AAAA,IAAQ;AAAA,EACb,IAAI,kBAAkB,MAAM,GAAG;AAAA,IAC7B,OAAO,OAAO;AAAA,EAChB;AAAA,EACA,IAAI,wBAAwB,MAAM,GAAG;AAAA,IACnC,IAAI,CAAC,OAAO,YAAY,OAAO,SAAS,SAAS,cAAc;AAAA,MAC7D,OAAO,OAAO,SAAS;AAAA,IACzB;AAAA,EACF;AAAA,EACA;AAAA;AAAA;AASK,MAAM,mBAAsC;AAAA,EACpB;AAAA,EAA7B,WAAW,CAAkB,aAAkC,mBAAmB;AAAA,IAArD;AAAA;AAAA,EAE7B,YAAY,CAAC,MAAoC;AAAA,IAC/C,IAAI,CAAC,qBAAqB,IAAI,KAAK,CAAC,KAAK,iBAAiB,GAAG;AAAA,MAC3D;AAAA,IACF;AAAA,IACA,MAAM,OAAO,WAAW,KAAK,KAAK,MAAM;AAAA,IACxC,IAAI,QAAQ,KAAK,WAAW,IAAI,IAAI,GAAG;AAAA,MACrC,OACE,kBAAkB,+DAClB,kFACA;AAAA,IAEJ;AAAA,IACA;AAAA;AAEJ;AAGA,SAAS,SAAS,CAAC,SAAkC;AAAA,EACnD,MAAM,QAAQ,QAAQ;AAAA,EACtB,IAAI,OAAO,UAAU,YAAY,UAAU;AAAA,IAAM,OAAO;AAAA,EACxD,OAAO,EAAE,4BAA4B,UAAU,MAAM,2BAA2B;AAAA;AAKlF,IAAM,eAAwB,EAAE,cAAc,MAAG;AAAA,EAAG;AAAA,EAAU;AAE9D,SAAS,yBAAyB,CAAC,SAAkC;AAAA,EACnE,OAAO,UAAU,OAAO,IAAI,IAAI,qBAAuB;AAAA;AAEzD,0BAA0B,SAAS,OAAO,aAAa,OAAO;AAGvD,IAAM,iBAAiB;AAAA,EAC5B,qBAAqB,WAAW,QAAQ,2BAA2B,yBAAyB;AAC9F;AAMO,IAAM,qBAAqB;AAAA,EAChC,SAAS,CAAC,wBAAwB;AAAA,EAClC,UAAU,CAAC,yBAAyB;AACtC;AAGA,IAAe;",
8
- "debugId": "E1FCF7B89389BBE564756E2164756E21",
9
- "names": []
10
- }