@fairfox/polly 0.1.1 → 0.1.2

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,321 @@
1
+ // Handler extraction from TypeScript code
2
+ // Extracts message handlers and their state mutations
3
+
4
+ import { Project, type SourceFile, SyntaxKind, Node } from "ts-morph";
5
+ import type { MessageHandler, StateAssignment, VerificationCondition } from "../types";
6
+
7
+ export interface HandlerAnalysis {
8
+ handlers: MessageHandler[];
9
+ messageTypes: Set<string>;
10
+ }
11
+
12
+ export class HandlerExtractor {
13
+ private project: Project;
14
+
15
+ constructor(tsConfigPath: string) {
16
+ this.project = new Project({
17
+ tsConfigFilePath: tsConfigPath,
18
+ });
19
+ }
20
+
21
+ /**
22
+ * Extract all message handlers from the codebase
23
+ */
24
+ extractHandlers(): HandlerAnalysis {
25
+ const handlers: MessageHandler[] = [];
26
+ const messageTypes = new Set<string>();
27
+
28
+ // Find all source files
29
+ const sourceFiles = this.project.getSourceFiles();
30
+
31
+ for (const sourceFile of sourceFiles) {
32
+ const fileHandlers = this.extractFromFile(sourceFile);
33
+ handlers.push(...fileHandlers);
34
+
35
+ for (const handler of fileHandlers) {
36
+ messageTypes.add(handler.messageType);
37
+ }
38
+ }
39
+
40
+ return {
41
+ handlers,
42
+ messageTypes,
43
+ };
44
+ }
45
+
46
+ /**
47
+ * Extract handlers from a single source file
48
+ */
49
+ private extractFromFile(sourceFile: SourceFile): MessageHandler[] {
50
+ const handlers: MessageHandler[] = [];
51
+ const filePath = sourceFile.getFilePath();
52
+
53
+ // Determine context from file path
54
+ const context = this.inferContext(filePath);
55
+
56
+ // Find all .on() call expressions
57
+ sourceFile.forEachDescendant((node) => {
58
+ if (Node.isCallExpression(node)) {
59
+ const expression = node.getExpression();
60
+
61
+ // Check if this is a .on() call
62
+ if (Node.isPropertyAccessExpression(expression)) {
63
+ const methodName = expression.getName();
64
+
65
+ if (methodName === "on") {
66
+ const handler = this.extractHandler(node, context, filePath);
67
+ if (handler) {
68
+ handlers.push(handler);
69
+ }
70
+ }
71
+ }
72
+ }
73
+ });
74
+
75
+ return handlers;
76
+ }
77
+
78
+ /**
79
+ * Extract handler details from a .on() call expression
80
+ */
81
+ private extractHandler(callExpr: any, context: string, filePath: string): MessageHandler | null {
82
+ const args = callExpr.getArguments();
83
+
84
+ if (args.length < 2) {
85
+ return null;
86
+ }
87
+
88
+ // First argument should be the message type (string literal)
89
+ const messageTypeArg = args[0];
90
+ let messageType: string | null = null;
91
+
92
+ if (Node.isStringLiteral(messageTypeArg)) {
93
+ messageType = messageTypeArg.getLiteralValue();
94
+ } else if (Node.isTemplateExpression(messageTypeArg)) {
95
+ // Handle template literals if needed
96
+ messageType = messageTypeArg.getText().replace(/[`'"]/g, "");
97
+ }
98
+
99
+ if (!messageType) {
100
+ return null;
101
+ }
102
+
103
+ // Second argument is the handler function
104
+ const handlerArg = args[1];
105
+ const assignments: StateAssignment[] = [];
106
+ const preconditions: VerificationCondition[] = [];
107
+ const postconditions: VerificationCondition[] = [];
108
+
109
+ // Parse the handler function for state assignments and verification conditions
110
+ if (Node.isArrowFunction(handlerArg) || Node.isFunctionExpression(handlerArg)) {
111
+ this.extractAssignments(handlerArg, assignments);
112
+ this.extractVerificationConditions(handlerArg, preconditions, postconditions);
113
+ }
114
+
115
+ const line = callExpr.getStartLineNumber();
116
+
117
+ return {
118
+ messageType,
119
+ node: context, // Renamed from 'context' to 'node' for generalization
120
+ assignments,
121
+ preconditions,
122
+ postconditions,
123
+ location: {
124
+ file: filePath,
125
+ line,
126
+ },
127
+ };
128
+ }
129
+
130
+ /**
131
+ * Extract state assignments from a handler function
132
+ */
133
+ private extractAssignments(funcNode: any, assignments: StateAssignment[]): void {
134
+ funcNode.forEachDescendant((node: any) => {
135
+ // Look for assignment expressions: state.field = value
136
+ if (Node.isBinaryExpression(node)) {
137
+ const operator = node.getOperatorToken().getText();
138
+
139
+ if (operator === "=") {
140
+ const left = node.getLeft();
141
+ const right = node.getRight();
142
+
143
+ // Check if left side is a state property access
144
+ if (Node.isPropertyAccessExpression(left)) {
145
+ const fieldPath = this.getPropertyPath(left);
146
+
147
+ // Check if this is a state access
148
+ if (fieldPath.startsWith("state.")) {
149
+ const field = fieldPath.substring(6); // Remove "state." prefix
150
+ const value = this.extractValue(right);
151
+
152
+ if (value !== undefined) {
153
+ assignments.push({
154
+ field,
155
+ value,
156
+ });
157
+ }
158
+ }
159
+ }
160
+ }
161
+ }
162
+ });
163
+ }
164
+
165
+ /**
166
+ * Extract verification conditions (requires/ensures) from a handler function
167
+ */
168
+ private extractVerificationConditions(
169
+ funcNode: any,
170
+ preconditions: VerificationCondition[],
171
+ postconditions: VerificationCondition[]
172
+ ): void {
173
+ const body = funcNode.getBody();
174
+
175
+ // Get all statements in the function body
176
+ const statements = Node.isBlock(body) ? body.getStatements() : [body];
177
+
178
+ statements.forEach((statement: any, index: number) => {
179
+ // Look for expression statements that are function calls
180
+ if (Node.isExpressionStatement(statement)) {
181
+ const expr = statement.getExpression();
182
+
183
+ if (Node.isCallExpression(expr)) {
184
+ const callee = expr.getExpression();
185
+
186
+ if (Node.isIdentifier(callee)) {
187
+ const functionName = callee.getText();
188
+
189
+ if (functionName === "requires") {
190
+ // Extract precondition
191
+ const condition = this.extractCondition(expr);
192
+ if (condition) {
193
+ preconditions.push(condition);
194
+ }
195
+ } else if (functionName === "ensures") {
196
+ // Extract postcondition
197
+ const condition = this.extractCondition(expr);
198
+ if (condition) {
199
+ postconditions.push(condition);
200
+ }
201
+ }
202
+ }
203
+ }
204
+ }
205
+ });
206
+ }
207
+
208
+ /**
209
+ * Extract condition from a requires() or ensures() call
210
+ */
211
+ private extractCondition(callExpr: any): VerificationCondition | null {
212
+ const args = callExpr.getArguments();
213
+
214
+ if (args.length === 0) {
215
+ return null;
216
+ }
217
+
218
+ // First argument is the condition expression
219
+ const conditionArg = args[0];
220
+ const expression = conditionArg.getText();
221
+
222
+ // Second argument (optional) is the message
223
+ let message: string | undefined;
224
+ if (args.length >= 2 && Node.isStringLiteral(args[1])) {
225
+ message = args[1].getLiteralValue();
226
+ }
227
+
228
+ const line = callExpr.getStartLineNumber();
229
+ const column = callExpr.getStartLinePos();
230
+
231
+ return {
232
+ expression,
233
+ message,
234
+ location: {
235
+ line,
236
+ column,
237
+ },
238
+ };
239
+ }
240
+
241
+ /**
242
+ * Get the full property access path (e.g., "state.user.loggedIn")
243
+ */
244
+ private getPropertyPath(node: any): string {
245
+ const parts: string[] = [];
246
+
247
+ let current = node;
248
+ while (Node.isPropertyAccessExpression(current)) {
249
+ parts.unshift(current.getName());
250
+ current = current.getExpression();
251
+ }
252
+
253
+ // Add the base identifier
254
+ if (Node.isIdentifier(current)) {
255
+ parts.unshift(current.getText());
256
+ }
257
+
258
+ return parts.join(".");
259
+ }
260
+
261
+ /**
262
+ * Extract a literal value from an expression
263
+ */
264
+ private extractValue(node: any): string | boolean | number | null | undefined {
265
+ if (Node.isStringLiteral(node)) {
266
+ return node.getLiteralValue();
267
+ }
268
+
269
+ if (Node.isNumericLiteral(node)) {
270
+ return node.getLiteralValue();
271
+ }
272
+
273
+ if (node.getKind() === SyntaxKind.TrueKeyword) {
274
+ return true;
275
+ }
276
+
277
+ if (node.getKind() === SyntaxKind.FalseKeyword) {
278
+ return false;
279
+ }
280
+
281
+ if (node.getKind() === SyntaxKind.NullKeyword) {
282
+ return null;
283
+ }
284
+
285
+ // For complex expressions, return undefined (can't extract)
286
+ return undefined;
287
+ }
288
+
289
+ /**
290
+ * Infer the context (background, content, popup, etc.) from file path
291
+ */
292
+ private inferContext(filePath: string): string {
293
+ const path = filePath.toLowerCase();
294
+
295
+ if (path.includes("/background/") || path.includes("\\background\\")) {
296
+ return "background";
297
+ }
298
+ if (path.includes("/content/") || path.includes("\\content\\")) {
299
+ return "content";
300
+ }
301
+ if (path.includes("/popup/") || path.includes("\\popup\\")) {
302
+ return "popup";
303
+ }
304
+ if (path.includes("/devtools/") || path.includes("\\devtools\\")) {
305
+ return "devtools";
306
+ }
307
+ if (path.includes("/options/") || path.includes("\\options\\")) {
308
+ return "options";
309
+ }
310
+ if (path.includes("/offscreen/") || path.includes("\\offscreen\\")) {
311
+ return "offscreen";
312
+ }
313
+
314
+ return "unknown";
315
+ }
316
+ }
317
+
318
+ export function extractHandlers(tsConfigPath: string): HandlerAnalysis {
319
+ const extractor = new HandlerExtractor(tsConfigPath);
320
+ return extractor.extractHandlers();
321
+ }
@@ -0,0 +1,9 @@
1
+ // Export all extraction utilities
2
+ export * from "./types";
3
+ export * from "./handlers";
4
+ export * from "./manifest";
5
+ export * from "./contexts";
6
+ export * from "./flows";
7
+ export * from "./integrations";
8
+ export * from "./adr";
9
+ export * from "./architecture";
@@ -0,0 +1,329 @@
1
+ // External integration detection - find external APIs, services, etc.
2
+
3
+ import { Project, Node } from "ts-morph";
4
+ import type { ExternalIntegration, ExternalAPICall } from "../types/architecture";
5
+
6
+ export class IntegrationAnalyzer {
7
+ private project: Project;
8
+
9
+ constructor(tsConfigPath: string) {
10
+ this.project = new Project({
11
+ tsConfigFilePath: tsConfigPath,
12
+ });
13
+ }
14
+
15
+ /**
16
+ * Analyze external integrations in the codebase
17
+ */
18
+ analyzeIntegrations(): ExternalIntegration[] {
19
+ const integrations = new Map<string, ExternalIntegration>();
20
+
21
+ // Find all fetch() calls
22
+ const fetchCalls = this.findFetchCalls();
23
+ for (const call of fetchCalls) {
24
+ this.addOrMergeIntegration(integrations, this.createAPIIntegration(call));
25
+ }
26
+
27
+ // Find WebSocket connections
28
+ const websockets = this.findWebSockets();
29
+ for (const ws of websockets) {
30
+ this.addOrMergeIntegration(integrations, this.createWebSocketIntegration(ws));
31
+ }
32
+
33
+ // Find external script imports
34
+ const externalScripts = this.findExternalScripts();
35
+ for (const script of externalScripts) {
36
+ this.addOrMergeIntegration(integrations, script);
37
+ }
38
+
39
+ return Array.from(integrations.values());
40
+ }
41
+
42
+ /**
43
+ * Find all fetch() API calls
44
+ */
45
+ private findFetchCalls(): Array<{
46
+ url: string;
47
+ method: string;
48
+ file: string;
49
+ line: number;
50
+ description?: string;
51
+ }> {
52
+ const calls: Array<{
53
+ url: string;
54
+ method: string;
55
+ file: string;
56
+ line: number;
57
+ description?: string;
58
+ }> = [];
59
+
60
+ for (const sourceFile of this.project.getSourceFiles()) {
61
+ sourceFile.forEachDescendant((node) => {
62
+ if (Node.isCallExpression(node)) {
63
+ const expression = node.getExpression();
64
+
65
+ // Check for fetch() calls
66
+ if (Node.isIdentifier(expression) && expression.getText() === "fetch") {
67
+ const args = node.getArguments();
68
+ if (args.length > 0) {
69
+ // Extract URL
70
+ const urlArg = args[0];
71
+ let url: string | null = null;
72
+
73
+ if (Node.isStringLiteral(urlArg)) {
74
+ url = urlArg.getLiteralValue();
75
+ } else if (Node.isTemplateExpression(urlArg)) {
76
+ // Try to extract base URL from template
77
+ url = this.extractBaseURL(urlArg.getText());
78
+ }
79
+
80
+ if (url) {
81
+ // Extract method
82
+ let method = "GET"; // Default
83
+ if (args.length > 1 && Node.isObjectLiteralExpression(args[1])) {
84
+ const options = args[1];
85
+ const methodProp = options.getProperty("method");
86
+ if (methodProp && Node.isPropertyAssignment(methodProp)) {
87
+ const initializer = methodProp.getInitializer();
88
+ if (initializer && Node.isStringLiteral(initializer)) {
89
+ method = initializer.getLiteralValue().toUpperCase();
90
+ }
91
+ }
92
+ }
93
+
94
+ // Extract description from JSDoc
95
+ const description = this.extractJSDocDescription(node);
96
+
97
+ calls.push({
98
+ url,
99
+ method,
100
+ file: sourceFile.getFilePath(),
101
+ line: node.getStartLineNumber(),
102
+ description,
103
+ });
104
+ }
105
+ }
106
+ }
107
+ }
108
+ });
109
+ }
110
+
111
+ return calls;
112
+ }
113
+
114
+ /**
115
+ * Find WebSocket connections
116
+ */
117
+ private findWebSockets(): Array<{
118
+ url: string;
119
+ file: string;
120
+ line: number;
121
+ description?: string;
122
+ }> {
123
+ const websockets: Array<{
124
+ url: string;
125
+ file: string;
126
+ line: number;
127
+ description?: string;
128
+ }> = [];
129
+
130
+ for (const sourceFile of this.project.getSourceFiles()) {
131
+ sourceFile.forEachDescendant((node) => {
132
+ if (Node.isNewExpression(node)) {
133
+ const expression = node.getExpression();
134
+
135
+ // Check for new WebSocket()
136
+ if (Node.isIdentifier(expression) && expression.getText() === "WebSocket") {
137
+ const args = node.getArguments();
138
+ if (args.length > 0 && Node.isStringLiteral(args[0])) {
139
+ const url = args[0].getLiteralValue();
140
+ const description = this.extractJSDocDescription(node);
141
+
142
+ websockets.push({
143
+ url,
144
+ file: sourceFile.getFilePath(),
145
+ line: node.getStartLineNumber(),
146
+ description,
147
+ });
148
+ }
149
+ }
150
+ }
151
+ });
152
+ }
153
+
154
+ return websockets;
155
+ }
156
+
157
+ /**
158
+ * Find external script dependencies (from imports)
159
+ */
160
+ private findExternalScripts(): ExternalIntegration[] {
161
+ const scripts: ExternalIntegration[] = [];
162
+ const seen = new Set<string>();
163
+
164
+ for (const sourceFile of this.project.getSourceFiles()) {
165
+ for (const importDecl of sourceFile.getImportDeclarations()) {
166
+ const moduleSpecifier = importDecl.getModuleSpecifierValue();
167
+
168
+ // Only consider external packages (not relative imports)
169
+ if (!moduleSpecifier.startsWith(".") && !moduleSpecifier.startsWith("/")) {
170
+ // Extract package name (handle scoped packages)
171
+ const packageName = moduleSpecifier.startsWith("@")
172
+ ? moduleSpecifier.split("/").slice(0, 2).join("/")
173
+ : moduleSpecifier.split("/")[0];
174
+
175
+ if (!seen.has(packageName)) {
176
+ seen.add(packageName);
177
+
178
+ scripts.push({
179
+ type: "external-script",
180
+ name: packageName,
181
+ technology: "npm package",
182
+ usedIn: [sourceFile.getFilePath()],
183
+ description: `External dependency: ${packageName}`,
184
+ });
185
+ }
186
+ }
187
+ }
188
+ }
189
+
190
+ return scripts;
191
+ }
192
+
193
+ /**
194
+ * Create API integration from fetch call
195
+ */
196
+ private createAPIIntegration(call: {
197
+ url: string;
198
+ method: string;
199
+ file: string;
200
+ line: number;
201
+ description?: string;
202
+ }): ExternalIntegration {
203
+ // Extract base URL
204
+ const baseURL = this.extractBaseURL(call.url);
205
+
206
+ // Infer name from URL
207
+ const name = this.inferAPIName(baseURL);
208
+
209
+ return {
210
+ type: "api",
211
+ name,
212
+ technology: "REST API",
213
+ url: baseURL,
214
+ usedIn: [call.file],
215
+ description: call.description || `External API: ${name}`,
216
+ calls: [
217
+ {
218
+ method: call.method,
219
+ endpoint: call.url,
220
+ location: {
221
+ file: call.file,
222
+ line: call.line,
223
+ },
224
+ description: call.description,
225
+ },
226
+ ],
227
+ };
228
+ }
229
+
230
+ /**
231
+ * Create WebSocket integration
232
+ */
233
+ private createWebSocketIntegration(ws: {
234
+ url: string;
235
+ file: string;
236
+ line: number;
237
+ description?: string;
238
+ }): ExternalIntegration {
239
+ const name = this.inferAPIName(ws.url);
240
+
241
+ return {
242
+ type: "websocket",
243
+ name,
244
+ technology: "WebSocket",
245
+ url: ws.url,
246
+ usedIn: [ws.file],
247
+ description: ws.description || `WebSocket connection: ${name}`,
248
+ };
249
+ }
250
+
251
+ /**
252
+ * Extract base URL from full URL or template
253
+ */
254
+ private extractBaseURL(url: string): string {
255
+ // Remove template parts
256
+ url = url.replace(/\$\{[^}]+\}/g, "");
257
+ url = url.replace(/`/g, "");
258
+
259
+ try {
260
+ const parsed = new URL(url);
261
+ return `${parsed.protocol}//${parsed.host}`;
262
+ } catch {
263
+ // If parsing fails, try to extract domain
264
+ const match = url.match(/https?:\/\/([^/]+)/);
265
+ return match ? match[0] : url;
266
+ }
267
+ }
268
+
269
+ /**
270
+ * Infer API name from URL
271
+ */
272
+ private inferAPIName(url: string): string {
273
+ try {
274
+ const parsed = new URL(url);
275
+ const hostname = parsed.hostname;
276
+
277
+ // Remove www. prefix
278
+ const cleanHost = hostname.replace(/^www\./, "");
279
+
280
+ // Take first part of domain
281
+ const parts = cleanHost.split(".");
282
+ if (parts.length > 0) {
283
+ // Capitalize first letter
284
+ return parts[0].charAt(0).toUpperCase() + parts[0].slice(1) + " API";
285
+ }
286
+ } catch {
287
+ // Fallback
288
+ }
289
+
290
+ return "External API";
291
+ }
292
+
293
+ /**
294
+ * Add or merge integration into map
295
+ */
296
+ private addOrMergeIntegration(
297
+ map: Map<string, ExternalIntegration>,
298
+ integration: ExternalIntegration
299
+ ): void {
300
+ const key = `${integration.type}:${integration.name}`;
301
+
302
+ if (map.has(key)) {
303
+ const existing = map.get(key)!;
304
+
305
+ // Merge usedIn
306
+ existing.usedIn = [...new Set([...existing.usedIn, ...integration.usedIn])];
307
+
308
+ // Merge calls
309
+ if (integration.calls && existing.calls) {
310
+ existing.calls.push(...integration.calls);
311
+ } else if (integration.calls) {
312
+ existing.calls = integration.calls;
313
+ }
314
+ } else {
315
+ map.set(key, integration);
316
+ }
317
+ }
318
+
319
+ /**
320
+ * Extract JSDoc description from node
321
+ */
322
+ private extractJSDocDescription(node: any): string | undefined {
323
+ const jsDocs = node.getJsDocs?.() || [];
324
+ if (jsDocs.length === 0) return undefined;
325
+
326
+ const comment = jsDocs[0].getDescription().trim();
327
+ return comment || undefined;
328
+ }
329
+ }