@fairfox/polly 0.1.0 → 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 (40) hide show
  1. package/README.md +9 -9
  2. package/cli/polly.ts +9 -3
  3. package/package.json +2 -2
  4. package/vendor/analysis/src/extract/adr.ts +212 -0
  5. package/vendor/analysis/src/extract/architecture.ts +160 -0
  6. package/vendor/analysis/src/extract/contexts.ts +298 -0
  7. package/vendor/analysis/src/extract/flows.ts +309 -0
  8. package/vendor/analysis/src/extract/handlers.ts +321 -0
  9. package/vendor/analysis/src/extract/index.ts +9 -0
  10. package/vendor/analysis/src/extract/integrations.ts +329 -0
  11. package/vendor/analysis/src/extract/manifest.ts +298 -0
  12. package/vendor/analysis/src/extract/types.ts +389 -0
  13. package/vendor/analysis/src/index.ts +7 -0
  14. package/vendor/analysis/src/types/adr.ts +53 -0
  15. package/vendor/analysis/src/types/architecture.ts +245 -0
  16. package/vendor/analysis/src/types/core.ts +210 -0
  17. package/vendor/analysis/src/types/index.ts +18 -0
  18. package/vendor/verify/src/adapters/base.ts +164 -0
  19. package/vendor/verify/src/adapters/detection.ts +281 -0
  20. package/vendor/verify/src/adapters/event-bus/index.ts +480 -0
  21. package/vendor/verify/src/adapters/web-extension/index.ts +508 -0
  22. package/vendor/verify/src/adapters/websocket/index.ts +486 -0
  23. package/vendor/verify/src/cli.ts +430 -0
  24. package/vendor/verify/src/codegen/config.ts +354 -0
  25. package/vendor/verify/src/codegen/tla.ts +719 -0
  26. package/vendor/verify/src/config/parser.ts +303 -0
  27. package/vendor/verify/src/config/types.ts +113 -0
  28. package/vendor/verify/src/core/model.ts +267 -0
  29. package/vendor/verify/src/core/primitives.ts +106 -0
  30. package/vendor/verify/src/extract/handlers.ts +2 -0
  31. package/vendor/verify/src/extract/types.ts +2 -0
  32. package/vendor/verify/src/index.ts +150 -0
  33. package/vendor/verify/src/primitives/index.ts +102 -0
  34. package/vendor/verify/src/runner/docker.ts +283 -0
  35. package/vendor/verify/src/types.ts +51 -0
  36. package/vendor/visualize/src/cli.ts +365 -0
  37. package/vendor/visualize/src/codegen/structurizr.ts +770 -0
  38. package/vendor/visualize/src/index.ts +13 -0
  39. package/vendor/visualize/src/runner/export.ts +235 -0
  40. package/vendor/visualize/src/viewer/server.ts +485 -0
@@ -0,0 +1,298 @@
1
+ // Manifest.json parser for Chrome extensions
2
+
3
+ import * as fs from "node:fs";
4
+ import * as path from "node:path";
5
+ import type { ManifestInfo, ContextInfo } from "../types/architecture";
6
+
7
+ /**
8
+ * Parse manifest.json and extract context information
9
+ */
10
+ export class ManifestParser {
11
+ private manifestPath: string;
12
+ private manifestData: any;
13
+ private baseDir: string;
14
+
15
+ constructor(projectRoot: string) {
16
+ this.baseDir = projectRoot;
17
+ this.manifestPath = path.join(projectRoot, "manifest.json");
18
+
19
+ if (!fs.existsSync(this.manifestPath)) {
20
+ throw new Error(`manifest.json not found at ${this.manifestPath}`);
21
+ }
22
+
23
+ try {
24
+ const content = fs.readFileSync(this.manifestPath, "utf-8");
25
+ this.manifestData = JSON.parse(content);
26
+ } catch (error) {
27
+ throw new Error(`Failed to parse manifest.json: ${error}`);
28
+ }
29
+ }
30
+
31
+ /**
32
+ * Parse manifest and extract all information
33
+ */
34
+ parse(): ManifestInfo {
35
+ const manifest = this.manifestData;
36
+
37
+ return {
38
+ name: manifest.name || "Unknown Extension",
39
+ version: manifest.version || "0.0.0",
40
+ description: manifest.description,
41
+ manifestVersion: manifest.manifest_version || 2,
42
+ background: this.parseBackground(),
43
+ contentScripts: this.parseContentScripts(),
44
+ popup: this.parsePopup(),
45
+ options: this.parseOptions(),
46
+ devtools: this.parseDevtools(),
47
+ permissions: manifest.permissions || [],
48
+ hostPermissions: manifest.host_permissions || [],
49
+ };
50
+ }
51
+
52
+ /**
53
+ * Get context entry points from manifest
54
+ */
55
+ getContextEntryPoints(): Record<string, string> {
56
+ const entryPoints: Record<string, string> = {};
57
+
58
+ // Background
59
+ const background = this.parseBackground();
60
+ if (background) {
61
+ // Take first file as entry point
62
+ const entryFile = background.files[0];
63
+ if (entryFile) {
64
+ entryPoints.background = this.findSourceFile(entryFile);
65
+ }
66
+ }
67
+
68
+ // Content scripts
69
+ const contentScripts = this.parseContentScripts();
70
+ if (contentScripts && contentScripts.length > 0) {
71
+ const firstScript = contentScripts[0].js[0];
72
+ if (firstScript) {
73
+ entryPoints.content = this.findSourceFile(firstScript);
74
+ }
75
+ }
76
+
77
+ // Popup
78
+ const popup = this.parsePopup();
79
+ if (popup) {
80
+ // For HTML, we need to find the associated JS/TS file
81
+ const htmlPath = path.join(this.baseDir, popup.html);
82
+ const jsPath = this.findAssociatedJS(htmlPath);
83
+ if (jsPath) {
84
+ entryPoints.popup = jsPath;
85
+ }
86
+ }
87
+
88
+ // Options
89
+ const options = this.parseOptions();
90
+ if (options) {
91
+ const htmlPath = path.join(this.baseDir, options.page);
92
+ const jsPath = this.findAssociatedJS(htmlPath);
93
+ if (jsPath) {
94
+ entryPoints.options = jsPath;
95
+ }
96
+ }
97
+
98
+ // DevTools
99
+ const devtools = this.parseDevtools();
100
+ if (devtools) {
101
+ const htmlPath = path.join(this.baseDir, devtools.page);
102
+ const jsPath = this.findAssociatedJS(htmlPath);
103
+ if (jsPath) {
104
+ entryPoints.devtools = jsPath;
105
+ }
106
+ }
107
+
108
+ return entryPoints;
109
+ }
110
+
111
+ /**
112
+ * Parse background configuration
113
+ */
114
+ private parseBackground(): ManifestInfo["background"] {
115
+ const bg = this.manifestData.background;
116
+ if (!bg) return undefined;
117
+
118
+ // Manifest V3 - service worker
119
+ if (bg.service_worker) {
120
+ return {
121
+ type: "service_worker",
122
+ files: [bg.service_worker],
123
+ };
124
+ }
125
+
126
+ // Manifest V2 - scripts
127
+ if (bg.scripts) {
128
+ return {
129
+ type: "script",
130
+ files: bg.scripts,
131
+ };
132
+ }
133
+
134
+ // Manifest V2 - page
135
+ if (bg.page) {
136
+ return {
137
+ type: "script",
138
+ files: [bg.page],
139
+ };
140
+ }
141
+
142
+ return undefined;
143
+ }
144
+
145
+ /**
146
+ * Parse content scripts configuration
147
+ */
148
+ private parseContentScripts(): ManifestInfo["contentScripts"] {
149
+ const cs = this.manifestData.content_scripts;
150
+ if (!cs || !Array.isArray(cs)) return undefined;
151
+
152
+ return cs.map((script) => ({
153
+ matches: script.matches || [],
154
+ js: script.js || [],
155
+ css: script.css,
156
+ }));
157
+ }
158
+
159
+ /**
160
+ * Parse popup configuration
161
+ */
162
+ private parsePopup(): ManifestInfo["popup"] {
163
+ const action = this.manifestData.action || this.manifestData.browser_action;
164
+ if (!action) return undefined;
165
+
166
+ if (action.default_popup) {
167
+ return {
168
+ html: action.default_popup,
169
+ default: true,
170
+ };
171
+ }
172
+
173
+ return undefined;
174
+ }
175
+
176
+ /**
177
+ * Parse options configuration
178
+ */
179
+ private parseOptions(): ManifestInfo["options"] {
180
+ const options = this.manifestData.options_ui || this.manifestData.options_page;
181
+ if (!options) return undefined;
182
+
183
+ if (typeof options === "string") {
184
+ return {
185
+ page: options,
186
+ openInTab: false,
187
+ };
188
+ }
189
+
190
+ return {
191
+ page: options.page,
192
+ openInTab: options.open_in_tab,
193
+ };
194
+ }
195
+
196
+ /**
197
+ * Parse devtools configuration
198
+ */
199
+ private parseDevtools(): ManifestInfo["devtools"] {
200
+ const devtools = this.manifestData.devtools_page;
201
+ if (!devtools) return undefined;
202
+
203
+ return {
204
+ page: devtools,
205
+ };
206
+ }
207
+
208
+ /**
209
+ * Find source file from manifest reference
210
+ * Tries multiple locations: exact path, src/ directory, .ts extension
211
+ */
212
+ private findSourceFile(manifestPath: string): string {
213
+ const candidates = [
214
+ // Exact path from manifest
215
+ path.join(this.baseDir, manifestPath),
216
+ // Same path with .ts extension
217
+ path.join(this.baseDir, manifestPath.replace(/\.js$/, ".ts")),
218
+ // In src/ directory
219
+ path.join(this.baseDir, "src", manifestPath),
220
+ path.join(this.baseDir, "src", manifestPath.replace(/\.js$/, ".ts")),
221
+ // In src/ with .tsx extension
222
+ path.join(this.baseDir, "src", manifestPath.replace(/\.js$/, ".tsx")),
223
+ ];
224
+
225
+ for (const candidate of candidates) {
226
+ if (fs.existsSync(candidate)) {
227
+ return candidate;
228
+ }
229
+ }
230
+
231
+ // Fallback to manifest path (will error later if not found)
232
+ return path.join(this.baseDir, manifestPath);
233
+ }
234
+
235
+ /**
236
+ * Find associated JavaScript/TypeScript file for an HTML file
237
+ */
238
+ private findAssociatedJS(htmlPath: string): string | null {
239
+ if (!fs.existsSync(htmlPath)) {
240
+ return null;
241
+ }
242
+
243
+ // Read HTML and look for script tags
244
+ const html = fs.readFileSync(htmlPath, "utf-8");
245
+ const scriptMatch = html.match(/<script[^>]+src=["']([^"']+)["']/i);
246
+
247
+ if (scriptMatch && scriptMatch[1]) {
248
+ const scriptPath = scriptMatch[1];
249
+ const fullPath = path.resolve(path.dirname(htmlPath), scriptPath);
250
+
251
+ // Try with and without .js/.ts extension
252
+ if (fs.existsSync(fullPath)) return fullPath;
253
+ if (fs.existsSync(fullPath.replace(/\.js$/, ".ts"))) {
254
+ return fullPath.replace(/\.js$/, ".ts");
255
+ }
256
+ if (fs.existsSync(fullPath.replace(/\.js$/, ".tsx"))) {
257
+ return fullPath.replace(/\.js$/, ".tsx");
258
+ }
259
+ }
260
+
261
+ // Fallback: look for convention-based files
262
+ const baseName = path.basename(htmlPath, ".html");
263
+ const dir = path.dirname(htmlPath);
264
+
265
+ const candidates = [
266
+ path.join(dir, `${baseName}.ts`),
267
+ path.join(dir, `${baseName}.tsx`),
268
+ path.join(dir, `${baseName}.js`),
269
+ path.join(dir, "index.ts"),
270
+ path.join(dir, "index.tsx"),
271
+ path.join(dir, "index.js"),
272
+ ];
273
+
274
+ for (const candidate of candidates) {
275
+ if (fs.existsSync(candidate)) {
276
+ return candidate;
277
+ }
278
+ }
279
+
280
+ return null;
281
+ }
282
+ }
283
+
284
+ /**
285
+ * Parse manifest.json and extract information
286
+ */
287
+ export function parseManifest(projectRoot: string): ManifestInfo {
288
+ const parser = new ManifestParser(projectRoot);
289
+ return parser.parse();
290
+ }
291
+
292
+ /**
293
+ * Get context entry points from manifest.json
294
+ */
295
+ export function getContextEntryPoints(projectRoot: string): Record<string, string> {
296
+ const parser = new ManifestParser(projectRoot);
297
+ return parser.getContextEntryPoints();
298
+ }
@@ -0,0 +1,389 @@
1
+ // Type extraction from TypeScript using ts-morph
2
+
3
+ import { Project, SourceFile, Type, TypeFormatFlags } from "ts-morph";
4
+ import type { TypeInfo, TypeKind, FieldAnalysis, Confidence, CodebaseAnalysis } from "../types";
5
+ import { HandlerExtractor } from "./handlers";
6
+
7
+ export class TypeExtractor {
8
+ private project: Project;
9
+
10
+ constructor(tsConfigPath: string) {
11
+ this.project = new Project({
12
+ tsConfigFilePath: tsConfigPath,
13
+ });
14
+ }
15
+
16
+ /**
17
+ * Analyze the codebase and extract state types and message types
18
+ */
19
+ async analyzeCodebase(stateFilePath?: string): Promise<CodebaseAnalysis> {
20
+ // Find state type
21
+ const stateType = stateFilePath ? this.extractStateType(stateFilePath) : this.findStateType();
22
+
23
+ // Find message types
24
+ const messageTypes = this.findMessageTypes();
25
+
26
+ // Analyze fields
27
+ const fields = stateType ? this.analyzeFields(stateType) : [];
28
+
29
+ // Extract message handlers
30
+ const configFilePath = this.project.getCompilerOptions().configFilePath;
31
+ const tsConfigPath = typeof configFilePath === "string" ? configFilePath : "tsconfig.json";
32
+ const handlerExtractor = new HandlerExtractor(tsConfigPath);
33
+ const handlerAnalysis = handlerExtractor.extractHandlers();
34
+
35
+ return {
36
+ stateType,
37
+ messageTypes: Array.from(new Set([...messageTypes, ...handlerAnalysis.messageTypes])),
38
+ fields,
39
+ handlers: handlerAnalysis.handlers,
40
+ };
41
+ }
42
+
43
+ /**
44
+ * Extract state type from a specific file
45
+ */
46
+ private extractStateType(filePath: string): TypeInfo | null {
47
+ const sourceFile = this.project.getSourceFile(filePath);
48
+ if (!sourceFile) {
49
+ return null;
50
+ }
51
+
52
+ // Look for type alias named "AppState" or similar
53
+ const typeAlias =
54
+ sourceFile.getTypeAlias("AppState") ||
55
+ sourceFile.getTypeAlias("State") ||
56
+ sourceFile.getTypeAliases()[0];
57
+
58
+ if (!typeAlias) {
59
+ return null;
60
+ }
61
+
62
+ const type = typeAlias.getType();
63
+ return this.convertType(type, typeAlias.getName());
64
+ }
65
+
66
+ /**
67
+ * Find state type by searching common patterns
68
+ */
69
+ private findStateType(): TypeInfo | null {
70
+ // Search for files with "state" in the name
71
+ const stateFiles = this.project.getSourceFiles("**/state*.ts");
72
+
73
+ for (const file of stateFiles) {
74
+ const typeAlias = file.getTypeAlias("AppState") || file.getTypeAlias("State");
75
+
76
+ if (typeAlias) {
77
+ const type = typeAlias.getType();
78
+ return this.convertType(type, typeAlias.getName());
79
+ }
80
+ }
81
+
82
+ return null;
83
+ }
84
+
85
+ /**
86
+ * Find message types by searching for type unions
87
+ */
88
+ private findMessageTypes(): string[] {
89
+ const messageTypes: string[] = [];
90
+
91
+ // Search for files with "message" in the name
92
+ const messageFiles = this.project.getSourceFiles("**/message*.ts");
93
+
94
+ for (const file of messageFiles) {
95
+ // Look for type aliases that are unions
96
+ for (const typeAlias of file.getTypeAliases()) {
97
+ const type = typeAlias.getType();
98
+ if (type.isUnion()) {
99
+ // Extract message type literals
100
+ for (const unionType of type.getUnionTypes()) {
101
+ if (unionType.isObject()) {
102
+ const typeProperty = unionType.getProperty("type");
103
+ if (typeProperty) {
104
+ const typeType = typeProperty.getTypeAtLocation(file);
105
+ if (typeType.isStringLiteral()) {
106
+ messageTypes.push(typeType.getLiteralValue() as string);
107
+ }
108
+ }
109
+ }
110
+ }
111
+ }
112
+ }
113
+ }
114
+
115
+ return [...new Set(messageTypes)]; // Dedupe
116
+ }
117
+
118
+ /**
119
+ * Convert ts-morph Type to our TypeInfo
120
+ */
121
+ private convertType(type: Type, name: string): TypeInfo {
122
+ // Check for null/undefined
123
+ const nullable = type.isNullable();
124
+
125
+ // Boolean
126
+ if (type.isBoolean() || type.isBooleanLiteral()) {
127
+ return { name, kind: "boolean", nullable };
128
+ }
129
+
130
+ // Union types
131
+ if (type.isUnion()) {
132
+ const unionTypes = type.getUnionTypes();
133
+
134
+ // Check for string literal union (enum)
135
+ const allStringLiterals = unionTypes.every((t) => t.isStringLiteral());
136
+ if (allStringLiterals) {
137
+ const enumValues = unionTypes.map((t) => t.getLiteralValue() as string);
138
+ return {
139
+ name,
140
+ kind: "enum",
141
+ nullable,
142
+ enumValues,
143
+ };
144
+ }
145
+
146
+ // Check for nullable type (T | null | undefined)
147
+ const nonNullTypes = unionTypes.filter((t) => !t.isNull() && !t.isUndefined());
148
+
149
+ if (nonNullTypes.length === 1) {
150
+ // This is a nullable type: T | null or T | undefined
151
+ const baseType = this.convertType(nonNullTypes[0], name);
152
+ return {
153
+ ...baseType,
154
+ nullable: true,
155
+ };
156
+ }
157
+
158
+ // Generic union - keep as-is
159
+ return {
160
+ name,
161
+ kind: "union",
162
+ nullable,
163
+ unionTypes: unionTypes.map((t, i) => this.convertType(t, `${name}_${i}`)),
164
+ };
165
+ }
166
+
167
+ // String
168
+ if (type.isString() || type.isStringLiteral()) {
169
+ return { name, kind: "string", nullable };
170
+ }
171
+
172
+ // Number
173
+ if (type.isNumber() || type.isNumberLiteral()) {
174
+ return { name, kind: "number", nullable };
175
+ }
176
+
177
+ // Array
178
+ if (type.isArray()) {
179
+ const elementType = type.getArrayElementType();
180
+ return {
181
+ name,
182
+ kind: "array",
183
+ nullable,
184
+ elementType: elementType
185
+ ? this.convertType(elementType, `${name}_element`)
186
+ : { name: "unknown", kind: "unknown", nullable: false },
187
+ };
188
+ }
189
+
190
+ // Map/Set detection - must come before generic object handling
191
+ const symbol = type.getSymbol();
192
+ if (symbol) {
193
+ const symbolName = symbol.getName();
194
+
195
+ // Map<K, V>
196
+ if (symbolName === "Map") {
197
+ const typeArgs = type.getTypeArguments();
198
+ return {
199
+ name,
200
+ kind: "map",
201
+ nullable,
202
+ // Extract value type from Map<K, V>
203
+ valueType:
204
+ typeArgs && typeArgs[1] ? this.convertType(typeArgs[1], `${name}_value`) : undefined,
205
+ };
206
+ }
207
+
208
+ // Set<T>
209
+ if (symbolName === "Set") {
210
+ const typeArgs = type.getTypeArguments();
211
+ return {
212
+ name,
213
+ kind: "set",
214
+ nullable,
215
+ elementType:
216
+ typeArgs && typeArgs[0] ? this.convertType(typeArgs[0], `${name}_element`) : undefined,
217
+ };
218
+ }
219
+ }
220
+
221
+ // Object
222
+ if (type.isObject()) {
223
+ const properties: Record<string, TypeInfo> = {};
224
+
225
+ for (const prop of type.getProperties()) {
226
+ const propName = prop.getName();
227
+ const propType = prop.getTypeAtLocation(this.project.getSourceFiles()[0]);
228
+ properties[propName] = this.convertType(propType, propName);
229
+ }
230
+
231
+ return {
232
+ name,
233
+ kind: "object",
234
+ nullable,
235
+ properties,
236
+ };
237
+ }
238
+
239
+ // Null
240
+ if (type.isNull()) {
241
+ return { name, kind: "null", nullable: true };
242
+ }
243
+
244
+ // Unknown/Any
245
+ return { name, kind: "unknown", nullable };
246
+ }
247
+
248
+ /**
249
+ * Analyze fields and determine confidence/bounds
250
+ */
251
+ private analyzeFields(stateType: TypeInfo, prefix = ""): FieldAnalysis[] {
252
+ const fields: FieldAnalysis[] = [];
253
+
254
+ if (stateType.kind === "object" && stateType.properties) {
255
+ for (const [key, propType] of Object.entries(stateType.properties)) {
256
+ const path = prefix ? `${prefix}.${key}` : key;
257
+
258
+ // Recursively analyze nested objects (but not Map/Set - they're leaf nodes)
259
+ if (propType.kind === "object") {
260
+ // Don't add intermediate objects as fields, just recurse into them
261
+ fields.push(...this.analyzeFields(propType, path));
262
+ } else {
263
+ // This is a leaf field (or Map/Set), add it for configuration
264
+ const analysis = this.analyzeField(path, propType);
265
+ fields.push(analysis);
266
+ }
267
+ }
268
+ }
269
+
270
+ return fields;
271
+ }
272
+
273
+ /**
274
+ * Analyze a single field and determine configuration needs
275
+ */
276
+ private analyzeField(path: string, type: TypeInfo): FieldAnalysis {
277
+ const analysis: FieldAnalysis = {
278
+ path,
279
+ type,
280
+ confidence: "low",
281
+ evidence: [],
282
+ suggestions: [],
283
+ bounds: {},
284
+ };
285
+
286
+ // Boolean - high confidence, no config needed
287
+ if (type.kind === "boolean") {
288
+ analysis.confidence = "high";
289
+ analysis.evidence.push("Boolean type - auto-configured");
290
+ return analysis;
291
+ }
292
+
293
+ // Enum - high confidence
294
+ if (type.kind === "enum" && type.enumValues) {
295
+ analysis.confidence = "high";
296
+ analysis.evidence.push(`Enum with ${type.enumValues.length} values`);
297
+ analysis.bounds!.values = type.enumValues;
298
+ return analysis;
299
+ }
300
+
301
+ // Array - needs manual configuration
302
+ if (type.kind === "array") {
303
+ analysis.confidence = "low";
304
+ analysis.suggestions.push("Choose maxLength: 5 (fast), 10 (balanced), or 20 (thorough)");
305
+ analysis.bounds!.maxLength = undefined;
306
+
307
+ // Try to find bounds in code
308
+ const foundBound = this.findArrayBound(path);
309
+ if (foundBound) {
310
+ analysis.confidence = "medium";
311
+ analysis.evidence.push(`Found array check: ${foundBound.evidence}`);
312
+ analysis.bounds!.maxLength = foundBound.value;
313
+ }
314
+
315
+ return analysis;
316
+ }
317
+
318
+ // Number - needs manual configuration
319
+ if (type.kind === "number") {
320
+ analysis.confidence = "low";
321
+ analysis.suggestions.push("Provide min and max values based on your application logic");
322
+ analysis.bounds!.min = undefined;
323
+ analysis.bounds!.max = undefined;
324
+
325
+ // Try to find bounds in code
326
+ const foundBound = this.findNumberBound(path);
327
+ if (foundBound) {
328
+ analysis.confidence = "high";
329
+ analysis.evidence.push(`Found comparison: ${foundBound.evidence}`);
330
+ analysis.bounds = { ...analysis.bounds!, ...foundBound.bounds };
331
+ }
332
+
333
+ return analysis;
334
+ }
335
+
336
+ // String - needs manual configuration
337
+ if (type.kind === "string") {
338
+ analysis.confidence = "low";
339
+ analysis.suggestions.push(
340
+ 'Provide 2-3 example values: ["value1", "value2", "value3"]',
341
+ "Or use { abstract: true } for symbolic verification"
342
+ );
343
+ analysis.bounds!.values = undefined;
344
+ return analysis;
345
+ }
346
+
347
+ // Map/Set - needs manual configuration
348
+ if (type.kind === "map" || type.kind === "set") {
349
+ analysis.confidence = "low";
350
+ analysis.suggestions.push("Provide maxSize (recommended: 3-5)");
351
+ analysis.bounds!.maxSize = undefined;
352
+ return analysis;
353
+ }
354
+
355
+ return analysis;
356
+ }
357
+
358
+ /**
359
+ * Try to find array bounds by searching for length checks
360
+ */
361
+ private findArrayBound(path: string): { value: number; evidence: string } | null {
362
+ // TODO: Search source code for patterns like:
363
+ // - if (array.length < N)
364
+ // - array.slice(0, N)
365
+ // This would require analyzing the actual usage in code
366
+ return null;
367
+ }
368
+
369
+ /**
370
+ * Try to find number bounds by searching for comparisons
371
+ */
372
+ private findNumberBound(
373
+ path: string
374
+ ): { bounds: { min?: number; max?: number }; evidence: string } | null {
375
+ // TODO: Search source code for patterns like:
376
+ // - if (counter < 100)
377
+ // - if (value >= 0 && value <= 100)
378
+ // This would require analyzing the actual usage in code
379
+ return null;
380
+ }
381
+ }
382
+
383
+ export async function analyzeCodebase(options: {
384
+ tsConfigPath: string;
385
+ stateFilePath?: string;
386
+ }): Promise<CodebaseAnalysis> {
387
+ const extractor = new TypeExtractor(options.tsConfigPath);
388
+ return extractor.analyzeCodebase(options.stateFilePath);
389
+ }
@@ -0,0 +1,7 @@
1
+ // Main entry point for @fairfox/polly-analysis
2
+
3
+ // Export types
4
+ export * from "./types";
5
+
6
+ // Export extractors
7
+ export * from "./extract";