analyze_repository
Parses every TypeScript and JavaScript file in a repository to build an AST cache and return a structural summary of classes, functions, and exports for downstream analysis.
Instructions
Parses every TypeScript and JavaScript file in the current repository via ts-morph and returns a structural summary: total file count, class count, top-level function count, exported symbol count, plus a per-file breakdown. This is the first step in the Veris analysis chain — it builds the AST cache that all downstream tools (export_behavioral_graph, list_workflows, generate_verification_plan, etc.) consume. Call this once at the start of an agent session for any repo.
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
No arguments | |||
Implementation Reference
- src/mcp/McpServer.ts:223-226 (handler)The handler function for the 'analyze_repository' tool. It calls ensureReport() which creates a RepositoryIntelligenceEngine and invokes engine.analyze(), then returns a text response with file count.
private handleAnalyzeRepository() { const r = this.ensureReport(); return this.text(`Repository analyzed. Found ${r.files.length} files.`); } - src/mcp/McpServer.ts:87-89 (schema)Tool definition/schema for 'analyze_repository' in the toolDefinitions() method. Defines description, and empty inputSchema (no parameters required).
{ name: "analyze_repository", description: "Parses every TypeScript and JavaScript file in the current repository via ts-morph and returns a structural summary: total file count, class count, top-level function count, exported symbol count, plus a per-file breakdown. This is the first step in the Veris analysis chain — it builds the AST cache that all downstream tools (export_behavioral_graph, list_workflows, generate_verification_plan, etc.) consume. Call this once at the start of an agent session for any repo.", inputSchema: { type: "object", properties: {}, required: [] } }, - src/mcp/McpServer.ts:63-63 (registration)Registration of the tool in the switch-case handler within setupToolHandlers(). When tool name matches 'analyze_repository', it routes to handleAnalyzeRepository().
case "analyze_repository": return this.handleAnalyzeRepository(); - src/mcp/McpServer.ts:168-173 (helper)The ensureReport() helper method that lazily creates a RepositoryIntelligenceEngine and calls engine.analyze() to produce the report. This is called by handleAnalyzeRepository().
private ensureReport(): RepositoryIntelligenceReport { if (this.lastReport) return this.lastReport; const engine = new RepositoryIntelligenceEngine(this.projectRoot); this.lastReport = engine.analyze(); return this.lastReport; } - The RepositoryIntelligenceEngine class that performs the actual analysis. Its analyze() method uses ts-morph to parse all TS/JS files and extract classes, functions, imports, and call expressions.
export class RepositoryIntelligenceEngine { private project: Project; private security: SecurityBaselineConfig; constructor(private projectRoot: string, security?: Partial<SecurityBaselineConfig>) { this.security = { zeroRetentionMode: security?.zeroRetentionMode ?? true, airGapped: security?.airGapped ?? true, ignoredPaths: security?.ignoredPaths ?? ['node_modules', 'dist', '.git', 'veris-reports', 'bvi-reports', 'coverage', '.next', 'build'] }; this.project = new Project({ useInMemoryFileSystem: false, // Don't try to type-check; we just want AST. Massive perf win on big repos. compilerOptions: { allowJs: true, checkJs: false, noEmit: true, skipLibCheck: true } }); const includes = [`${projectRoot}/**/*.ts`, `${projectRoot}/**/*.tsx`, `${projectRoot}/**/*.js`, `${projectRoot}/**/*.jsx`, `${projectRoot}/**/*.mjs`, `${projectRoot}/**/*.cjs`]; const excludes = this.security.ignoredPaths.map(p => `!${projectRoot}/${p}/**/*`); this.project.addSourceFilesAtPaths([...includes, ...excludes]); } public analyze(): RepositoryIntelligenceReport { const sourceFiles = this.project.getSourceFiles(); const report: RepositoryIntelligenceReport = { projectPath: this.projectRoot, files: [], dependencyMap: {} }; for (const file of sourceFiles) { const verisFile = this.extractFileData(file); report.files.push(verisFile); report.dependencyMap[verisFile.filePath] = verisFile.imports; } return report; } private extractFileData(file: SourceFile): VerisFile { const filePath = file.getFilePath(); const imports = this.extractImports(file); const classes = this.extractClasses(file); const functions = this.extractFunctions(file, classes); return { filePath, classes, functions, imports }; } private extractImports(file: SourceFile): string[] { const out = new Set<string>(); // ES imports for (const imp of file.getImportDeclarations()) { const spec = imp.getModuleSpecifierValue(); if (spec) out.add(spec); } // CommonJS require() calls try { for (const call of file.getDescendantsOfKind(SyntaxKind.CallExpression)) { const callee = call.getExpression(); if (callee.getText() !== 'require') continue; const args = call.getArguments(); if (args.length === 0) continue; const first = args[0]; if (first.getKind() === SyntaxKind.StringLiteral) { const txt = first.getText(); // Strip quotes out.add(txt.replace(/^['"`]|['"`]$/g, '')); } } } catch { // resilient } return Array.from(out); } private extractClasses(file: SourceFile): VerisClass[] { const classes: VerisClass[] = file.getClasses().map(cls => ({ name: cls.getName() || 'AnonymousClass', methods: cls.getMethods().map(method => ({ name: method.getName(), isExported: cls.isExported(), calls: this.extractCalls(method) })) })); // CommonJS prototype assignments: ClassName.prototype.method = function() {} // Group by class name, attach methods. try { const protoAssignments = new Map<string, { methodName: string; node: Node }[]>(); for (const bin of file.getDescendantsOfKind(SyntaxKind.BinaryExpression)) { if (bin.getOperatorToken().getText() !== '=') continue; const left = bin.getLeft(); if (left.getKind() !== SyntaxKind.PropertyAccessExpression) continue; const leftText = left.getText(); // Match `Foo.prototype.bar` const m = leftText.match(/^([A-Z][\w$]*)\.prototype\.([\w$]+)$/); if (!m) continue; const [, className, methodName] = m; const right = bin.getRight(); if (right.getKind() !== SyntaxKind.FunctionExpression && right.getKind() !== SyntaxKind.ArrowFunction) continue; if (!protoAssignments.has(className)) protoAssignments.set(className, []); protoAssignments.get(className)!.push({ methodName, node: right }); } protoAssignments.forEach((methods, className) => { let cls = classes.find(c => c.name === className); if (!cls) { cls = { name: className, methods: [] }; classes.push(cls); } for (const m of methods) { if (cls.methods.find(x => x.name === m.methodName)) continue; cls.methods.push({ name: m.methodName, isExported: true, calls: this.extractCalls(m.node as any) }); } }); } catch { // ignore } return classes; } private extractFunctions(file: SourceFile, classes: VerisClass[]): VerisFunction[] { const functions: VerisFunction[] = []; const seen = new Set<string>(); const classNames = new Set(classes.map(c => c.name)); // Function declarations for (const fn of file.getFunctions()) { const name = fn.getName(); if (!name) continue; if (seen.has(name)) continue; seen.add(name); functions.push({ name, isExported: fn.isExported(), calls: this.extractCalls(fn) }); } // const X = function/() => ... AND var X = function/() => ... AND let X = ... try { for (const varDecl of file.getDescendantsOfKind(SyntaxKind.VariableDeclaration)) { const name = varDecl.getName(); if (!name || seen.has(name) || classNames.has(name)) continue; const init = varDecl.getInitializer(); if (!init) continue; const k = init.getKind(); if (k !== SyntaxKind.FunctionExpression && k !== SyntaxKind.ArrowFunction) continue; // Only top-level (parent.parent.parent should be SourceFile via VariableStatement) seen.add(name); functions.push({ name, isExported: true, calls: this.extractCalls(init as any) }); } } catch { // ignore } // module.exports.X = function() {} OR exports.X = function() {} try { for (const bin of file.getDescendantsOfKind(SyntaxKind.BinaryExpression)) { if (bin.getOperatorToken().getText() !== '=') continue; const left = bin.getLeft(); if (left.getKind() !== SyntaxKind.PropertyAccessExpression) continue; const leftText = left.getText(); let propName: string | null = null; const m1 = leftText.match(/^module\.exports\.([\w$]+)$/); const m2 = leftText.match(/^exports\.([\w$]+)$/); if (m1) propName = m1[1]; else if (m2) propName = m2[1]; if (!propName || seen.has(propName) || classNames.has(propName)) continue; const right = bin.getRight(); const k = right.getKind(); if (k !== SyntaxKind.FunctionExpression && k !== SyntaxKind.ArrowFunction) continue; seen.add(propName); functions.push({ name: propName, isExported: true, calls: this.extractCalls(right as any) }); } // `module.exports = function NAMED() {}` -> create a function with the file's base name const baseFnExports = file.getDescendantsOfKind(SyntaxKind.BinaryExpression).filter(b => { if (b.getOperatorToken().getText() !== '=') return false; const lt = b.getLeft().getText(); return lt === 'module.exports' || lt === 'exports'; }); for (const b of baseFnExports) { const right = b.getRight(); const k = right.getKind(); if (k !== SyntaxKind.FunctionExpression && k !== SyntaxKind.ArrowFunction) continue; // Use the file's base name (without extension) as the function name const base = file.getBaseNameWithoutExtension(); if (seen.has(base) || classNames.has(base)) continue; seen.add(base); functions.push({ name: base, isExported: true, calls: this.extractCalls(right as any) }); } } catch { // ignore } return functions; } private extractCalls(node: any): string[] { const calls: Set<string> = new Set(); try { const callExprs = node.getDescendantsOfKind(SyntaxKind.CallExpression); for (const c of callExprs) { const expr = c.getExpression(); const text = expr.getText(); const callee = text.split('.').pop(); if (callee && /^[A-Za-z_$][\w$]*$/.test(callee)) calls.add(callee); } } catch { // resilient } return Array.from(calls); } }