mr4's picture
Upload 136 files
fd8cdf5 verified
import * as fs from 'node:fs';
import * as path from 'node:path';
import { execSync } from 'node:child_process';
import { scanProject } from './scanner.js';
import { parseFile } from './parser.js';
import { buildGraph } from './graph-builder.js';
import { detectLayers } from './layer-detector.js';
import { buildTour } from './tour-builder.js';
/**
* Reads the existing meta.json from the target path.
* Returns null if the file doesn't exist or can't be parsed.
*/
function readExistingMeta(targetPath) {
try {
const metaPath = path.join(targetPath, '.understand-anything', 'meta.json');
const content = fs.readFileSync(metaPath, 'utf-8');
return JSON.parse(content);
}
catch {
return null;
}
}
/**
* Gets the list of files changed since a given git commit hash.
* Returns null if git diff fails or the commit is not found.
*/
function getChangedFiles(targetPath, sinceCommit) {
try {
const output = execSync(`git diff --name-only ${sinceCommit} HEAD`, {
cwd: targetPath,
encoding: 'utf-8',
stdio: ['pipe', 'pipe', 'pipe'],
});
const files = output.trim().split('\n').filter(f => f.length > 0);
return files.length > 0 ? files : null;
}
catch {
return null;
}
}
/**
* Reads the existing knowledge-graph.json and extracts nodes and edges.
* Returns null if the file doesn't exist or can't be parsed.
*/
function readExistingGraph(targetPath) {
try {
const graphPath = path.join(targetPath, '.understand-anything', 'knowledge-graph.json');
const content = fs.readFileSync(graphPath, 'utf-8');
const data = JSON.parse(content);
if (data.nodes && data.edges) {
return { nodes: data.nodes, edges: data.edges };
}
return null;
}
catch {
return null;
}
}
/**
* Computes statistics from the analysis results.
*/
function computeStats(filesAnalyzed, nodes, edges, layers) {
const nodesByType = {};
for (const node of nodes) {
nodesByType[node.type] = (nodesByType[node.type] || 0) + 1;
}
return {
filesAnalyzed,
nodesByType,
edgesCreated: edges.length,
layersIdentified: layers.length,
};
}
/**
* Creates an empty result with zero stats.
*/
function createEmptyResult() {
const emptyMetadata = {
name: '',
description: '',
languages: [],
frameworks: [],
analyzedAt: new Date().toISOString(),
gitCommitHash: '',
};
return {
dashboard: {
version: '1.0.0',
project: emptyMetadata,
nodes: [],
edges: [],
layers: [],
tour: [],
},
stats: {
filesAnalyzed: 0,
nodesByType: {},
edgesCreated: 0,
layersIdentified: 0,
},
files: [],
};
}
/**
* Parses all files and returns a map of relativePath → ParseResult.
* Skips files that fail to parse (logs warning to stderr).
*/
function parseFiles(files) {
const parseResults = new Map();
for (const file of files) {
try {
const result = parseFile(file.path, file.language);
parseResults.set(file.relativePath, result);
}
catch (err) {
const message = err instanceof Error ? err.message : String(err);
process.stderr.write(`Warning: Failed to parse ${file.relativePath}: ${message}\n`);
}
}
return parseResults;
}
/**
* Runs the full analysis pipeline on all files.
*/
function runFullPipeline(scanResult) {
const { files, metadata } = scanResult;
if (files.length === 0) {
return createEmptyResult();
}
// Parse all files
const parseResults = parseFiles(files);
// Build graph
const { nodes, edges } = buildGraph(files, parseResults);
// Detect layers
const layers = detectLayers(nodes, edges);
// Build tour
const tour = buildTour(nodes, edges, layers);
// Assemble dashboard
const dashboard = {
version: '1.0.0',
project: {
...metadata,
analyzedAt: new Date().toISOString(),
},
nodes,
edges,
layers,
tour,
};
const stats = computeStats(files.length, nodes, edges, layers);
return { dashboard, stats, files };
}
/**
* Main entry point for the static analysis engine.
*
* Scans the target directory, parses source files, builds a knowledge graph,
* detects architectural layers, and generates a guided tour — all without
* requiring an AI agent.
*
* Supports incremental mode: when an existing meta.json is found and --full
* is not specified, only re-analyzes files changed since the last commit hash.
*
* @param targetPath - Absolute path to the directory to analyze
* @param options - Analysis options (e.g., full rebuild vs incremental)
* @returns AnalyzeResult with the generated dashboard and statistics
*/
export async function analyze(targetPath, options) {
try {
// Scan the project
const scanResult = scanProject(targetPath);
if (scanResult.files.length === 0) {
return createEmptyResult();
}
// Determine if we should use incremental mode
if (!options.full) {
const existingMeta = readExistingMeta(targetPath);
if (existingMeta && existingMeta.gitCommitHash) {
const changedFiles = getChangedFiles(targetPath, existingMeta.gitCommitHash);
if (changedFiles) {
// Attempt incremental analysis
const incrementalResult = runIncrementalPipeline(targetPath, scanResult, changedFiles);
if (incrementalResult) {
return incrementalResult;
}
// Fall through to full rebuild if incremental fails
}
}
}
// Full rebuild
return runFullPipeline(scanResult);
}
catch (err) {
const message = err instanceof Error ? err.message : String(err);
process.stderr.write(`Error during analysis: ${message}\n`);
return createEmptyResult();
}
}
/**
* Runs incremental analysis: only re-parses changed files, reuses existing
* nodes/edges for unchanged files, then rebuilds the full graph.
*
* Returns null if incremental analysis cannot be completed (caller should
* fall back to full rebuild).
*/
function runIncrementalPipeline(targetPath, scanResult, changedFiles) {
// Read existing graph for unchanged file data
const existingGraph = readExistingGraph(targetPath);
if (!existingGraph) {
return null; // Fall back to full rebuild
}
const changedSet = new Set(changedFiles);
const { files, metadata } = scanResult;
// Split files into changed and unchanged
const changedFileEntries = files.filter(f => changedSet.has(f.relativePath));
const unchangedFileEntries = files.filter(f => !changedSet.has(f.relativePath));
// Parse only changed files
const changedParseResults = parseFiles(changedFileEntries);
// For unchanged files, we need their parse results too for graph building.
// Re-parse them (this is cheaper than storing full parse results in the graph).
const unchangedParseResults = parseFiles(unchangedFileEntries);
// Combine all parse results
const allParseResults = new Map();
for (const [key, value] of changedParseResults) {
allParseResults.set(key, value);
}
for (const [key, value] of unchangedParseResults) {
allParseResults.set(key, value);
}
// Rebuild the full graph from combined results
const { nodes, edges } = buildGraph(files, allParseResults);
// Detect layers
const layers = detectLayers(nodes, edges);
// Build tour
const tour = buildTour(nodes, edges, layers);
// Assemble dashboard
const dashboard = {
version: '1.0.0',
project: {
...metadata,
analyzedAt: new Date().toISOString(),
},
nodes,
edges,
layers,
tour,
};
// Stats reflect only the changed files as "analyzed"
const stats = computeStats(changedFileEntries.length, nodes, edges, layers);
return { dashboard, stats, files };
}
export { scanProject } from './scanner.js';
export { parseFile } from './parser.js';
export { buildGraph } from './graph-builder.js';
export { detectLayers } from './layer-detector.js';
export { buildTour } from './tour-builder.js';
export { generateDomainContext } from './domain-context.js';