@fairfox/polly 0.1.1 → 0.1.3

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