@fairfox/polly 0.1.3 → 0.1.5

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 (69) hide show
  1. package/README.md +560 -184
  2. package/{cli/polly.ts → dist/cli/polly.js} +100 -206
  3. package/dist/cli/polly.js.map +10 -0
  4. package/dist/scripts/build-extension.js +137 -0
  5. package/dist/scripts/build-extension.js.map +10 -0
  6. package/dist/vendor/verify/src/cli.js +2089 -0
  7. package/dist/vendor/verify/src/cli.js.map +16 -0
  8. package/dist/vendor/visualize/src/cli.js +2204 -0
  9. package/dist/vendor/visualize/src/cli.js.map +19 -0
  10. package/package.json +12 -12
  11. package/vendor/analysis/src/extract/adr.ts +0 -212
  12. package/vendor/analysis/src/extract/architecture.ts +0 -160
  13. package/vendor/analysis/src/extract/contexts.ts +0 -298
  14. package/vendor/analysis/src/extract/flows.ts +0 -309
  15. package/vendor/analysis/src/extract/handlers.ts +0 -321
  16. package/vendor/analysis/src/extract/index.ts +0 -9
  17. package/vendor/analysis/src/extract/integrations.ts +0 -329
  18. package/vendor/analysis/src/extract/manifest.ts +0 -298
  19. package/vendor/analysis/src/extract/types.ts +0 -389
  20. package/vendor/analysis/src/index.ts +0 -7
  21. package/vendor/analysis/src/types/adr.ts +0 -53
  22. package/vendor/analysis/src/types/architecture.ts +0 -245
  23. package/vendor/analysis/src/types/core.ts +0 -210
  24. package/vendor/analysis/src/types/index.ts +0 -18
  25. package/vendor/verify/src/adapters/base.ts +0 -164
  26. package/vendor/verify/src/adapters/detection.ts +0 -281
  27. package/vendor/verify/src/adapters/event-bus/index.ts +0 -480
  28. package/vendor/verify/src/adapters/web-extension/index.ts +0 -508
  29. package/vendor/verify/src/adapters/websocket/index.ts +0 -486
  30. package/vendor/verify/src/cli.ts +0 -430
  31. package/vendor/verify/src/codegen/config.ts +0 -354
  32. package/vendor/verify/src/codegen/tla.ts +0 -719
  33. package/vendor/verify/src/config/parser.ts +0 -303
  34. package/vendor/verify/src/config/types.ts +0 -113
  35. package/vendor/verify/src/core/model.ts +0 -267
  36. package/vendor/verify/src/core/primitives.ts +0 -106
  37. package/vendor/verify/src/extract/handlers.ts +0 -2
  38. package/vendor/verify/src/extract/types.ts +0 -2
  39. package/vendor/verify/src/index.ts +0 -150
  40. package/vendor/verify/src/primitives/index.ts +0 -102
  41. package/vendor/verify/src/runner/docker.ts +0 -283
  42. package/vendor/verify/src/types.ts +0 -51
  43. package/vendor/visualize/src/cli.ts +0 -365
  44. package/vendor/visualize/src/codegen/structurizr.ts +0 -770
  45. package/vendor/visualize/src/index.ts +0 -13
  46. package/vendor/visualize/src/runner/export.ts +0 -235
  47. package/vendor/visualize/src/viewer/server.ts +0 -485
  48. /package/dist/{background → src/background}/index.js +0 -0
  49. /package/dist/{background → src/background}/index.js.map +0 -0
  50. /package/dist/{background → src/background}/message-router.js +0 -0
  51. /package/dist/{background → src/background}/message-router.js.map +0 -0
  52. /package/dist/{index.js → src/index.js} +0 -0
  53. /package/dist/{index.js.map → src/index.js.map} +0 -0
  54. /package/dist/{shared → src/shared}/adapters/index.js +0 -0
  55. /package/dist/{shared → src/shared}/adapters/index.js.map +0 -0
  56. /package/dist/{shared → src/shared}/lib/context-helpers.js +0 -0
  57. /package/dist/{shared → src/shared}/lib/context-helpers.js.map +0 -0
  58. /package/dist/{shared → src/shared}/lib/errors.js +0 -0
  59. /package/dist/{shared → src/shared}/lib/errors.js.map +0 -0
  60. /package/dist/{shared → src/shared}/lib/message-bus.js +0 -0
  61. /package/dist/{shared → src/shared}/lib/message-bus.js.map +0 -0
  62. /package/dist/{shared → src/shared}/lib/state.js +0 -0
  63. /package/dist/{shared → src/shared}/lib/state.js.map +0 -0
  64. /package/dist/{shared → src/shared}/lib/test-helpers.js +0 -0
  65. /package/dist/{shared → src/shared}/lib/test-helpers.js.map +0 -0
  66. /package/dist/{shared → src/shared}/state/app-state.js +0 -0
  67. /package/dist/{shared → src/shared}/state/app-state.js.map +0 -0
  68. /package/dist/{shared → src/shared}/types/messages.js +0 -0
  69. /package/dist/{shared → src/shared}/types/messages.js.map +0 -0
@@ -1,486 +0,0 @@
1
- // ═══════════════════════════════════════════════════════════════
2
- // WebSocket Adapter
3
- // ═══════════════════════════════════════════════════════════════
4
- //
5
- // Extracts verification model from Bun WebSocket applications.
6
- // Recognizes:
7
- // - WebSocket server/client communication
8
- // - Eden type definitions for messages
9
- // - Handler functions (handle* pattern)
10
- // - State mutations via global state object
11
- // - Verification primitives (requires, ensures)
12
-
13
- import { Project, type SourceFile, SyntaxKind, Node } from "ts-morph";
14
- import type {
15
- CoreVerificationModel,
16
- MessageHandler,
17
- StateAssignment,
18
- VerificationCondition,
19
- NodeDefinition,
20
- MessageType,
21
- RoutingRule,
22
- } from "../../core/model";
23
- import type { AdapterConfig, RoutingAdapter } from "../base";
24
-
25
- // ─────────────────────────────────────────────────────────────────
26
- // Configuration
27
- // ─────────────────────────────────────────────────────────────────
28
-
29
- export interface WebSocketAdapterConfig extends AdapterConfig {
30
- /** Whether to use Eden types for message extraction */
31
- useEdenTypes?: boolean;
32
-
33
- /** Pattern to match handler functions (default: /^handle[A-Z]/) */
34
- handlerPattern?: RegExp;
35
-
36
- /** Maximum concurrent connections to model (default: 10) */
37
- maxConnections?: number;
38
-
39
- /** Maximum messages in flight (default: 5) */
40
- maxInFlight?: number;
41
- }
42
-
43
- // ─────────────────────────────────────────────────────────────────
44
- // WebSocket Adapter Implementation
45
- // ─────────────────────────────────────────────────────────────────
46
-
47
- export class WebSocketAdapter implements RoutingAdapter<WebSocketAdapterConfig> {
48
- readonly name = "websocket";
49
- readonly config: WebSocketAdapterConfig;
50
- private project: Project;
51
-
52
- constructor(config: WebSocketAdapterConfig) {
53
- this.config = {
54
- useEdenTypes: true,
55
- handlerPattern: /^handle[A-Z]/,
56
- maxConnections: 10,
57
- maxInFlight: 5,
58
- ...config,
59
- };
60
-
61
- this.project = new Project({
62
- tsConfigFilePath: config.tsConfigPath,
63
- });
64
- }
65
-
66
- /**
67
- * Extract the complete verification model from the WebSocket application
68
- */
69
- extractModel(): CoreVerificationModel {
70
- const sourceFiles = this.project.getSourceFiles();
71
-
72
- // Extract all handlers
73
- const handlers: MessageHandler[] = [];
74
- const messageTypeNames = new Set<string>();
75
-
76
- for (const sourceFile of sourceFiles) {
77
- const fileHandlers = this.extractHandlersFromFile(sourceFile);
78
- handlers.push(...fileHandlers);
79
-
80
- for (const handler of fileHandlers) {
81
- messageTypeNames.add(handler.messageType);
82
- }
83
- }
84
-
85
- // Define nodes (server + clients)
86
- const nodes: NodeDefinition[] = [
87
- {
88
- id: "server",
89
- type: "websocket-server",
90
- canSendTo: ["*"], // Server can send to all clients
91
- canReceiveFrom: ["*"], // Server receives from all clients
92
- metadata: {
93
- isHub: true,
94
- },
95
- },
96
- ];
97
-
98
- // Add client nodes (bounded for model checking)
99
- for (let i = 1; i <= this.config.maxConnections!; i++) {
100
- nodes.push({
101
- id: `client-${i}`,
102
- type: "websocket-client",
103
- canSendTo: ["server"], // Clients send to server
104
- canReceiveFrom: ["server"], // Clients receive from server
105
- metadata: {
106
- clientId: i,
107
- },
108
- });
109
- }
110
-
111
- // Define message types
112
- const messageTypes: MessageType[] = Array.from(messageTypeNames).map((name) => ({
113
- name,
114
- payload: {
115
- name: "unknown",
116
- kind: "unknown",
117
- nullable: false,
118
- },
119
- routing: {
120
- from:
121
- name.startsWith("USER_") ||
122
- name.startsWith("CHAT_") ||
123
- name.startsWith("TODO_") ||
124
- name.startsWith("SYNC_")
125
- ? ["client-*"] // Client messages
126
- : ["server"], // Server messages
127
- to:
128
- name.startsWith("USER_") ||
129
- name.startsWith("CHAT_") ||
130
- name.startsWith("TODO_") ||
131
- name.startsWith("SYNC_")
132
- ? ["server"] // To server
133
- : ["client-*"], // To clients
134
- },
135
- }));
136
-
137
- // Define routing rules (hub-and-spoke pattern)
138
- const routingRules: RoutingRule[] = [
139
- {
140
- pattern: "request-reply",
141
- messageTypes: Array.from(messageTypeNames),
142
- description:
143
- "WebSocket hub-and-spoke: clients send to server, server broadcasts to clients",
144
- },
145
- ];
146
-
147
- return {
148
- nodes,
149
- messageTypes,
150
- routingRules,
151
- state: {}, // Populated by user configuration
152
- handlers,
153
- bounds: {
154
- maxConcurrentMessages: this.config.maxInFlight!,
155
- maxNodes: nodes.length,
156
- custom: {
157
- maxConnections: this.config.maxConnections!,
158
- },
159
- },
160
- };
161
- }
162
-
163
- /**
164
- * Recognize message handler: handle* functions
165
- */
166
- recognizeMessageHandler(node: Node): MessageHandler | null {
167
- if (!Node.isFunctionDeclaration(node)) {
168
- return null;
169
- }
170
-
171
- const name = node.getName();
172
- if (!name || !this.config.handlerPattern!.test(name)) {
173
- return null;
174
- }
175
-
176
- return this.extractHandlerFromFunction(node);
177
- }
178
-
179
- /**
180
- * Recognize state mutation: state.field = value
181
- */
182
- recognizeStateUpdate(node: Node): StateAssignment | null {
183
- if (!Node.isBinaryExpression(node)) {
184
- return null;
185
- }
186
-
187
- const operator = node.getOperatorToken().getText();
188
- if (operator !== "=" && operator !== "+=" && operator !== "-=") {
189
- return null;
190
- }
191
-
192
- const left = node.getLeft();
193
- const right = node.getRight();
194
-
195
- // Check if left side is a state property access
196
- if (!Node.isPropertyAccessExpression(left)) {
197
- return null;
198
- }
199
-
200
- const fieldPath = this.getPropertyPath(left);
201
-
202
- // Check if this is a state access
203
- if (!fieldPath.startsWith("state.")) {
204
- return null;
205
- }
206
-
207
- const field = fieldPath.substring(6); // Remove "state." prefix
208
-
209
- // For += and -= operators, we can't extract the exact value
210
- if (operator !== "=") {
211
- return null;
212
- }
213
-
214
- const value = this.extractValue(right);
215
-
216
- if (value === undefined) {
217
- return null;
218
- }
219
-
220
- return {
221
- field,
222
- value,
223
- };
224
- }
225
-
226
- /**
227
- * Recognize verification condition: requires() or ensures()
228
- */
229
- recognizeVerificationCondition(
230
- node: Node,
231
- type: "precondition" | "postcondition"
232
- ): VerificationCondition | null {
233
- if (!Node.isCallExpression(node)) {
234
- return null;
235
- }
236
-
237
- const callee = node.getExpression();
238
- if (!Node.isIdentifier(callee)) {
239
- return null;
240
- }
241
-
242
- const functionName = callee.getText();
243
- const expectedName = type === "precondition" ? "requires" : "ensures";
244
-
245
- if (functionName !== expectedName) {
246
- return null;
247
- }
248
-
249
- return this.extractCondition(node);
250
- }
251
-
252
- // ─────────────────────────────────────────────────────────────────
253
- // Private Helper Methods
254
- // ─────────────────────────────────────────────────────────────────
255
-
256
- /**
257
- * Extract all handlers from a source file
258
- */
259
- private extractHandlersFromFile(sourceFile: SourceFile): MessageHandler[] {
260
- const handlers: MessageHandler[] = [];
261
-
262
- sourceFile.forEachDescendant((node) => {
263
- const handler = this.recognizeMessageHandler(node);
264
- if (handler) {
265
- handlers.push(handler);
266
- }
267
- });
268
-
269
- return handlers;
270
- }
271
-
272
- /**
273
- * Extract handler details from a handle* function
274
- */
275
- private extractHandlerFromFunction(funcNode: Node): MessageHandler | null {
276
- if (!Node.isFunctionDeclaration(funcNode)) {
277
- return null;
278
- }
279
-
280
- const name = funcNode.getName();
281
- if (!name) {
282
- return null;
283
- }
284
-
285
- // Extract message type from function name
286
- // handleUserJoin -> USER_JOIN
287
- const messageType = this.functionNameToMessageType(name);
288
-
289
- const assignments: StateAssignment[] = [];
290
- const preconditions: VerificationCondition[] = [];
291
- const postconditions: VerificationCondition[] = [];
292
-
293
- // Parse the function body
294
- this.extractAssignmentsFromFunction(funcNode, assignments);
295
- this.extractVerificationConditionsFromFunction(funcNode, preconditions, postconditions);
296
-
297
- const sourceFile = funcNode.getSourceFile();
298
- const line = funcNode.getStartLineNumber();
299
-
300
- return {
301
- messageType,
302
- node: "server", // All handlers run on server
303
- assignments,
304
- preconditions,
305
- postconditions,
306
- location: {
307
- file: sourceFile.getFilePath(),
308
- line,
309
- },
310
- };
311
- }
312
-
313
- /**
314
- * Convert handler function name to message type
315
- * handleUserJoin -> USER_JOIN
316
- */
317
- private functionNameToMessageType(name: string): string {
318
- // Remove "handle" prefix
319
- const withoutHandle = name.replace(/^handle/, "");
320
-
321
- // Convert PascalCase to SCREAMING_SNAKE_CASE
322
- return withoutHandle
323
- .replace(/([A-Z])/g, "_$1")
324
- .replace(/^_/, "")
325
- .toUpperCase();
326
- }
327
-
328
- /**
329
- * Extract state assignments from a function
330
- */
331
- private extractAssignmentsFromFunction(funcNode: Node, assignments: StateAssignment[]): void {
332
- funcNode.forEachDescendant((node) => {
333
- const assignment = this.recognizeStateUpdate(node);
334
- if (assignment) {
335
- assignments.push(assignment);
336
- }
337
- });
338
- }
339
-
340
- /**
341
- * Extract verification conditions from a function
342
- */
343
- private extractVerificationConditionsFromFunction(
344
- funcNode: Node,
345
- preconditions: VerificationCondition[],
346
- postconditions: VerificationCondition[]
347
- ): void {
348
- if (!Node.isFunctionDeclaration(funcNode)) {
349
- return;
350
- }
351
-
352
- const body = funcNode.getBody();
353
-
354
- if (!body || !Node.isBlock(body)) {
355
- return;
356
- }
357
-
358
- // Get all statements in the function body
359
- const statements = body.getStatements();
360
-
361
- for (const statement of statements) {
362
- // Look for expression statements that are function calls
363
- if (Node.isExpressionStatement(statement)) {
364
- const expr = statement.getExpression();
365
-
366
- const precond = this.recognizeVerificationCondition(expr, "precondition");
367
- if (precond) {
368
- preconditions.push(precond);
369
- }
370
-
371
- const postcond = this.recognizeVerificationCondition(expr, "postcondition");
372
- if (postcond) {
373
- postconditions.push(postcond);
374
- }
375
- }
376
- }
377
- }
378
-
379
- /**
380
- * Extract condition from a requires() or ensures() call
381
- */
382
- private extractCondition(callExpr: Node): VerificationCondition | null {
383
- if (!Node.isCallExpression(callExpr)) {
384
- return null;
385
- }
386
-
387
- const args = callExpr.getArguments();
388
-
389
- if (args.length === 0) {
390
- return null;
391
- }
392
-
393
- // First argument is the condition expression
394
- const conditionArg = args[0];
395
- const expression = conditionArg.getText();
396
-
397
- // Second argument (optional) is the message
398
- let message: string | undefined;
399
- if (args.length >= 2 && Node.isStringLiteral(args[1])) {
400
- message = args[1].getLiteralValue();
401
- }
402
-
403
- const line = callExpr.getStartLineNumber();
404
- const column = callExpr.getStart();
405
-
406
- return {
407
- expression,
408
- message,
409
- location: {
410
- line,
411
- column,
412
- },
413
- };
414
- }
415
-
416
- /**
417
- * Get the full property access path (e.g., "state.user.loggedIn")
418
- */
419
- private getPropertyPath(node: Node): string {
420
- const parts: string[] = [];
421
-
422
- let current: Node = node;
423
- while (Node.isPropertyAccessExpression(current)) {
424
- parts.unshift(current.getName());
425
- current = current.getExpression();
426
- }
427
-
428
- // Add the base identifier
429
- if (Node.isIdentifier(current)) {
430
- parts.unshift(current.getText());
431
- }
432
-
433
- return parts.join(".");
434
- }
435
-
436
- /**
437
- * Extract a literal value from an expression
438
- */
439
- private extractValue(node: Node): string | boolean | number | null | undefined {
440
- if (Node.isStringLiteral(node)) {
441
- return node.getLiteralValue();
442
- }
443
-
444
- if (Node.isNumericLiteral(node)) {
445
- return node.getLiteralValue();
446
- }
447
-
448
- if (node.getKind() === SyntaxKind.TrueKeyword) {
449
- return true;
450
- }
451
-
452
- if (node.getKind() === SyntaxKind.FalseKeyword) {
453
- return false;
454
- }
455
-
456
- if (node.getKind() === SyntaxKind.NullKeyword) {
457
- return null;
458
- }
459
-
460
- // For complex expressions, return undefined (can't extract)
461
- return undefined;
462
- }
463
-
464
- /**
465
- * Custom invariants specific to WebSocket systems
466
- */
467
- customInvariants(): Array<[name: string, tlaExpression: string]> {
468
- return [
469
- [
470
- "ServerAlwaysAvailable",
471
- 'ports["server"] = "connected" \\* Server must always be available',
472
- ],
473
- [
474
- "ClientsConnectToServer",
475
- "\\A msg \\in Range(messages) : " +
476
- '(msg.source # "server") => ("server" \\in msg.targets) \\* Clients must route through server',
477
- ],
478
- [
479
- "BroadcastConsistency",
480
- "\\A c1, c2 \\in Contexts : " +
481
- '(c1 # "server" /\\ c2 # "server" /\\ ports[c1] = "connected" /\\ ports[c2] = "connected") => ' +
482
- "\\* All connected clients eventually receive broadcasts",
483
- ],
484
- ];
485
- }
486
- }