@fairfox/polly 0.67.0 → 0.70.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.
- package/dist/src/client/index.js +17 -20
- package/dist/src/client/index.js.map +4 -4
- package/dist/src/mesh.js +85 -11
- package/dist/src/mesh.js.map +7 -6
- package/dist/src/peer.js +80 -6
- package/dist/src/peer.js.map +7 -6
- package/dist/src/polly-ui/markdown.js +3 -3
- package/dist/src/polly-ui/markdown.js.map +2 -2
- package/dist/src/shared/lib/crdt-specialised.d.ts +6 -0
- package/dist/src/shared/lib/crdt-state.d.ts +8 -1
- package/dist/src/shared/lib/mesh-diagnostics.d.ts +98 -0
- package/dist/src/shared/lib/mesh-network-adapter.d.ts +0 -4
- package/dist/tools/test/src/e2e-mesh/console-allowlist.d.ts +31 -0
- package/dist/tools/test/src/e2e-mesh/index.d.ts +27 -0
- package/dist/tools/test/src/e2e-mesh/index.js +1089 -0
- package/dist/tools/test/src/e2e-mesh/index.js.map +22 -0
- package/dist/tools/test/src/e2e-mesh/keys.d.ts +55 -0
- package/dist/tools/test/src/e2e-mesh/launch-peer.d.ts +70 -0
- package/dist/tools/test/src/e2e-mesh/mesh-assertions.d.ts +53 -0
- package/dist/tools/test/src/e2e-mesh/serve-consumer.d.ts +32 -0
- package/dist/tools/test/src/e2e-mesh/wait-for-convergence.d.ts +38 -0
- package/dist/tools/test/src/e2e-mesh/with-relay.d.ts +53 -0
- package/dist/tools/test/src/visual/index.js +24 -24
- package/dist/tools/test/src/visual/index.js.map +2 -2
- package/dist/tools/verify/src/cli.js +361 -22
- package/dist/tools/verify/src/cli.js.map +6 -6
- package/dist/tools/verify/src/config.d.ts +26 -1
- package/dist/tools/verify/src/config.js +9 -1
- package/dist/tools/verify/src/config.js.map +4 -4
- package/dist/tools/verify/src/primitives/index.d.ts +30 -0
- package/dist/tools/visualize/src/cli.js +43 -1
- package/dist/tools/visualize/src/cli.js.map +3 -3
- package/package.json +11 -8
- package/LICENSE +0 -21
- package/README.md +0 -362
|
@@ -8,6 +8,31 @@ interface SubsystemConfig {
|
|
|
8
8
|
}
|
|
9
9
|
interface LegacyVerificationConfig {
|
|
10
10
|
state: Record<string, unknown>;
|
|
11
|
+
/**
|
|
12
|
+
* polly#117: optional mesh-document declarations. When present, each
|
|
13
|
+
* key names a `$meshState` document and its value declares the
|
|
14
|
+
* field-level state schema for that document. The verifier emits a
|
|
15
|
+
* separate slot in `contextStates[ctx].mesh[<docId>]` for these
|
|
16
|
+
* fields and adds a `PropagateMeshOp` action that allows the doc's
|
|
17
|
+
* value on one context to flow to another — modelling Automerge
|
|
18
|
+
* sync between peers. Mesh references inside `forAllPeers` quantifiers
|
|
19
|
+
* route through this slot so cross-peer convergence claims are
|
|
20
|
+
* actually checked.
|
|
21
|
+
*
|
|
22
|
+
* @example
|
|
23
|
+
* ```ts
|
|
24
|
+
* defineVerification({
|
|
25
|
+
* state: { localCounter: { type: "number", min: 0, max: 3 } },
|
|
26
|
+
* mesh: {
|
|
27
|
+
* todos: {
|
|
28
|
+
* entries: { type: "enum", values: ["empty", "one", "many"] },
|
|
29
|
+
* },
|
|
30
|
+
* },
|
|
31
|
+
* messages: { maxInFlight: 2 },
|
|
32
|
+
* });
|
|
33
|
+
* ```
|
|
34
|
+
*/
|
|
35
|
+
mesh?: Record<string, Record<string, unknown>>;
|
|
11
36
|
messages: {
|
|
12
37
|
maxInFlight?: number;
|
|
13
38
|
maxTabs?: number;
|
|
@@ -70,4 +95,4 @@ type UnifiedVerificationConfig = LegacyVerificationConfig | AdapterVerificationC
|
|
|
70
95
|
* ```
|
|
71
96
|
*/
|
|
72
97
|
export declare function defineVerification<T extends UnifiedVerificationConfig>(config: T): T;
|
|
73
|
-
export { $constraints, ensures, hasLength, inRange, oneOf, requires, stateConstraint, } from "./primitives/index.js";
|
|
98
|
+
export { $constraints, ensures, forAllPeers, hasLength, inRange, oneOf, requires, somePeer, stateConstraint, } from "./primitives/index.js";
|
|
@@ -93,6 +93,12 @@ function $constraints(stateField, constraints, options) {
|
|
|
93
93
|
}
|
|
94
94
|
}
|
|
95
95
|
function stateConstraint(name, predicate, options) {}
|
|
96
|
+
function forAllPeers(predicate) {
|
|
97
|
+
return true;
|
|
98
|
+
}
|
|
99
|
+
function somePeer(predicate) {
|
|
100
|
+
return true;
|
|
101
|
+
}
|
|
96
102
|
|
|
97
103
|
// tools/verify/src/config.ts
|
|
98
104
|
function defineVerification(config) {
|
|
@@ -117,13 +123,15 @@ function defineVerification(config) {
|
|
|
117
123
|
}
|
|
118
124
|
export {
|
|
119
125
|
stateConstraint,
|
|
126
|
+
somePeer,
|
|
120
127
|
requires,
|
|
121
128
|
oneOf,
|
|
122
129
|
inRange,
|
|
123
130
|
hasLength,
|
|
131
|
+
forAllPeers,
|
|
124
132
|
ensures,
|
|
125
133
|
defineVerification,
|
|
126
134
|
$constraints
|
|
127
135
|
};
|
|
128
136
|
|
|
129
|
-
//# debugId=
|
|
137
|
+
//# debugId=D9257AB1F243F69864756E2164756E21
|
|
@@ -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// Re-export for convenience\nexport const verify = {\n requires,\n ensures,\n invariant,\n inRange,\n oneOf,\n hasLength,\n $constraints,\n stateConstraint,\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// 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}\n\n// Legacy verification configuration\ninterface LegacyVerificationConfig {\n state: 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\n // Subsystem-scoped verification (compositional)\n subsystems?: Record<string, SubsystemConfig>;\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\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 bounds?: {\n maxInFlight?: number;\n [key: string]: unknown;\n };\n onBuild?: \"warn\" | \"error\" | \"off\";\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 hasLength,\n inRange,\n oneOf,\n requires,\n stateConstraint,\n} from \"./primitives/index.js\";\n"
|
|
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// 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}\n\n// Legacy verification configuration\ninterface LegacyVerificationConfig {\n state: Record<string, unknown>;\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\n // Subsystem-scoped verification (compositional)\n subsystems?: Record<string, SubsystemConfig>;\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\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 bounds?: {\n maxInFlight?: number;\n [key: string]: unknown;\n };\n onBuild?: \"warn\" | \"error\" | \"off\";\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"
|
|
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
|
|
9
|
-
"debugId": "
|
|
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;;;AC7EF,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": "D9257AB1F243F69864756E2164756E21",
|
|
10
10
|
"names": []
|
|
11
11
|
}
|
|
@@ -110,6 +110,34 @@ export declare function $constraints(stateField: string, constraints: Record<str
|
|
|
110
110
|
export declare function stateConstraint(name: string, predicate: () => boolean, options?: {
|
|
111
111
|
message?: string;
|
|
112
112
|
}): void;
|
|
113
|
+
/**
|
|
114
|
+
* Cross-peer universal quantifier for use inside `requires` and
|
|
115
|
+
* `ensures` predicates. The runtime is a no-op that simply runs the
|
|
116
|
+
* predicate against a single dummy context (so the assertion never
|
|
117
|
+
* triggers at runtime); the verifier recognizes the wrapper in
|
|
118
|
+
* predicate source and emits a `\A peer \in Contexts \ {ctx} : (...)`
|
|
119
|
+
* clause that asks TLC to check the inner predicate against every
|
|
120
|
+
* other context.
|
|
121
|
+
*
|
|
122
|
+
* @example
|
|
123
|
+
* ensures(
|
|
124
|
+
* forAllPeers(peer => peer.todos.value.length === todos.value.length),
|
|
125
|
+
* "every peer agrees on todo count"
|
|
126
|
+
* );
|
|
127
|
+
*/
|
|
128
|
+
export declare function forAllPeers<TPeer>(predicate: (peer: TPeer) => boolean): boolean;
|
|
129
|
+
/**
|
|
130
|
+
* Cross-peer existential quantifier for use inside `requires` and
|
|
131
|
+
* `ensures` predicates. Runtime no-op; the verifier emits a
|
|
132
|
+
* `\E peer \in Contexts \ {ctx} : (...)` clause.
|
|
133
|
+
*
|
|
134
|
+
* @example
|
|
135
|
+
* ensures(
|
|
136
|
+
* somePeer(peer => peer.user.value.loggedIn === true),
|
|
137
|
+
* "at least one peer has a logged-in user"
|
|
138
|
+
* );
|
|
139
|
+
*/
|
|
140
|
+
export declare function somePeer<TPeer>(predicate: (peer: TPeer) => boolean): boolean;
|
|
113
141
|
export declare const verify: {
|
|
114
142
|
requires: typeof requires;
|
|
115
143
|
ensures: typeof ensures;
|
|
@@ -119,4 +147,6 @@ export declare const verify: {
|
|
|
119
147
|
hasLength: typeof hasLength;
|
|
120
148
|
$constraints: typeof $constraints;
|
|
121
149
|
stateConstraint: typeof stateConstraint;
|
|
150
|
+
forAllPeers: typeof forAllPeers;
|
|
151
|
+
somePeer: typeof somePeer;
|
|
122
152
|
};
|
|
@@ -1788,6 +1788,7 @@ class HandlerExtractor {
|
|
|
1788
1788
|
const stateConstraints = [];
|
|
1789
1789
|
const globalStateConstraints = [];
|
|
1790
1790
|
const verifiedStates = [];
|
|
1791
|
+
const meshOrPeerSignals = [];
|
|
1791
1792
|
const resources = [];
|
|
1792
1793
|
this.warnings = [];
|
|
1793
1794
|
const allSourceFiles = this.project.getSourceFiles();
|
|
@@ -1818,6 +1819,12 @@ class HandlerExtractor {
|
|
|
1818
1819
|
}
|
|
1819
1820
|
}
|
|
1820
1821
|
}
|
|
1822
|
+
for (const filePath of this.analyzedFiles) {
|
|
1823
|
+
const sourceFile = this.project.getSourceFile(filePath);
|
|
1824
|
+
if (!sourceFile)
|
|
1825
|
+
continue;
|
|
1826
|
+
meshOrPeerSignals.push(...this.extractMeshOrPeerSignalsFromFile(sourceFile));
|
|
1827
|
+
}
|
|
1821
1828
|
this.debugLogExtractionResults(handlers.length, invalidMessageTypes.size);
|
|
1822
1829
|
this.debugLogAnalysisStats(allSourceFiles.length, entryPoints.length);
|
|
1823
1830
|
return {
|
|
@@ -1826,10 +1833,45 @@ class HandlerExtractor {
|
|
|
1826
1833
|
stateConstraints,
|
|
1827
1834
|
globalStateConstraints,
|
|
1828
1835
|
verifiedStates,
|
|
1836
|
+
meshOrPeerSignals,
|
|
1829
1837
|
resources,
|
|
1830
1838
|
warnings: this.warnings
|
|
1831
1839
|
};
|
|
1832
1840
|
}
|
|
1841
|
+
extractMeshOrPeerSignalsFromFile(sourceFile) {
|
|
1842
|
+
const out = [];
|
|
1843
|
+
const filePath = sourceFile.getFilePath();
|
|
1844
|
+
sourceFile.forEachDescendant((node) => {
|
|
1845
|
+
const info = this.recognizeMeshOrPeerStateCall(node, filePath);
|
|
1846
|
+
if (info)
|
|
1847
|
+
out.push(info);
|
|
1848
|
+
});
|
|
1849
|
+
return out;
|
|
1850
|
+
}
|
|
1851
|
+
recognizeMeshOrPeerStateCall(node, filePath) {
|
|
1852
|
+
if (!Node4.isCallExpression(node))
|
|
1853
|
+
return null;
|
|
1854
|
+
const expression = node.getExpression();
|
|
1855
|
+
if (!Node4.isIdentifier(expression))
|
|
1856
|
+
return null;
|
|
1857
|
+
const funcName = expression.getText();
|
|
1858
|
+
const kind = funcName === "$meshState" ? "mesh" : funcName === "$peerState" ? "peer" : null;
|
|
1859
|
+
if (!kind)
|
|
1860
|
+
return null;
|
|
1861
|
+
const args = node.getArguments();
|
|
1862
|
+
const keyArg = args[0];
|
|
1863
|
+
if (!keyArg || !Node4.isStringLiteral(keyArg))
|
|
1864
|
+
return null;
|
|
1865
|
+
const key = keyArg.getLiteralValue();
|
|
1866
|
+
const variableName = this.getVariableNameFromParent(node) || key;
|
|
1867
|
+
return {
|
|
1868
|
+
kind,
|
|
1869
|
+
key,
|
|
1870
|
+
variableName,
|
|
1871
|
+
filePath,
|
|
1872
|
+
line: node.getStartLineNumber()
|
|
1873
|
+
};
|
|
1874
|
+
}
|
|
1833
1875
|
analyzeFileAndImports(sourceFile, handlers, messageTypes, invalidMessageTypes, stateConstraints, globalStateConstraints, verifiedStates, resources) {
|
|
1834
1876
|
const filePath = sourceFile.getFilePath();
|
|
1835
1877
|
if (this.analyzedFiles.has(filePath)) {
|
|
@@ -6248,4 +6290,4 @@ main().catch((_error) => {
|
|
|
6248
6290
|
process.exit(1);
|
|
6249
6291
|
});
|
|
6250
6292
|
|
|
6251
|
-
//# debugId=
|
|
6293
|
+
//# debugId=76D47496DA7C4C5D64756E2164756E21
|