144 lines
4.6 KiB
TypeScript
144 lines
4.6 KiB
TypeScript
import { dirname } from "node:path";
|
|
import type { ChangeAnalysis } from "./fingerprint.js";
|
|
|
|
export interface UpdateDecision {
|
|
action: "SKIP" | "PARTIAL_UPDATE" | "ARCHITECTURE_UPDATE" | "FULL_UPDATE";
|
|
filesToReanalyze: string[];
|
|
rerunArchitecture: boolean;
|
|
rerunTour: boolean;
|
|
reason: string;
|
|
}
|
|
|
|
/**
|
|
* Classify the type of graph update needed based on structural change analysis.
|
|
*
|
|
* Decision matrix:
|
|
* - SKIP: all files NONE or COSMETIC only
|
|
* - PARTIAL_UPDATE: some STRUCTURAL, same directories
|
|
* - ARCHITECTURE_UPDATE: new/deleted directories or >10 structural files
|
|
* - FULL_UPDATE: >30 structural files or >50% of total files changed structurally
|
|
*/
|
|
export function classifyUpdate(
|
|
analysis: ChangeAnalysis,
|
|
totalFilesInGraph: number,
|
|
allKnownFiles: string[] = [],
|
|
): UpdateDecision {
|
|
const { newFiles, deletedFiles, structurallyChangedFiles, cosmeticOnlyFiles } = analysis;
|
|
const structuralCount = structurallyChangedFiles.length + newFiles.length + deletedFiles.length;
|
|
|
|
// No structural changes at all — skip
|
|
if (structuralCount === 0) {
|
|
const cosmeticCount = cosmeticOnlyFiles.length;
|
|
const reason = cosmeticCount > 0
|
|
? `${cosmeticCount} file(s) have cosmetic-only changes (no structural impact)`
|
|
: "No changes detected";
|
|
|
|
return {
|
|
action: "SKIP",
|
|
filesToReanalyze: [],
|
|
rerunArchitecture: false,
|
|
rerunTour: false,
|
|
reason,
|
|
};
|
|
}
|
|
|
|
// Too many structural changes — suggest full rebuild
|
|
const triggeredByCount = structuralCount > 30;
|
|
const triggeredByPercentage = totalFilesInGraph > 0 && structuralCount / totalFilesInGraph > 0.5;
|
|
if (triggeredByCount || triggeredByPercentage) {
|
|
const thresholdReason =
|
|
triggeredByCount && triggeredByPercentage
|
|
? ">30 files and >50% of project"
|
|
: triggeredByCount
|
|
? ">30 files"
|
|
: ">50% of project";
|
|
return {
|
|
action: "FULL_UPDATE",
|
|
filesToReanalyze: [...structurallyChangedFiles, ...newFiles],
|
|
rerunArchitecture: true,
|
|
rerunTour: true,
|
|
reason: `${structuralCount} files have structural changes (${thresholdReason}) — full rebuild recommended`,
|
|
};
|
|
}
|
|
|
|
// Check if directory structure changed (new/deleted top-level directories)
|
|
const hasDirectoryChanges = detectDirectoryChanges(newFiles, deletedFiles, allKnownFiles);
|
|
|
|
if (hasDirectoryChanges || structuralCount > 10) {
|
|
return {
|
|
action: "ARCHITECTURE_UPDATE",
|
|
filesToReanalyze: [...structurallyChangedFiles, ...newFiles],
|
|
rerunArchitecture: true,
|
|
rerunTour: true,
|
|
reason: hasDirectoryChanges
|
|
? `Directory structure changed (${newFiles.length} new, ${deletedFiles.length} deleted files)`
|
|
: `${structuralCount} files have structural changes — architecture re-analysis needed`,
|
|
};
|
|
}
|
|
|
|
// Localized structural changes — partial update
|
|
return {
|
|
action: "PARTIAL_UPDATE",
|
|
filesToReanalyze: [...structurallyChangedFiles, ...newFiles],
|
|
rerunArchitecture: false,
|
|
rerunTour: false,
|
|
reason: `${structuralCount} file(s) have structural changes: ${summarizeChanges(analysis)}`,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Detect if the changes affect the directory structure (new or removed directories).
|
|
* Uses all known files in the project as the baseline for existing directories,
|
|
* then checks if any new/deleted files introduce or remove a top-level source directory.
|
|
*/
|
|
function detectDirectoryChanges(
|
|
newFiles: string[],
|
|
deletedFiles: string[],
|
|
allKnownFiles: string[],
|
|
): boolean {
|
|
const existingDirs = new Set(
|
|
allKnownFiles.map((f) => topDirectory(f)).filter(Boolean),
|
|
);
|
|
|
|
for (const f of newFiles) {
|
|
const dir = topDirectory(f);
|
|
if (dir && !existingDirs.has(dir)) return true;
|
|
}
|
|
|
|
for (const f of deletedFiles) {
|
|
const dir = topDirectory(f);
|
|
if (dir && !existingDirs.has(dir)) return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Get the top-level directory of a file path (first path segment).
|
|
*/
|
|
function topDirectory(filePath: string): string | null {
|
|
const dir = dirname(filePath);
|
|
if (dir === "." || dir === "") return null;
|
|
const segments = dir.split("/");
|
|
return segments[0] || null;
|
|
}
|
|
|
|
/**
|
|
* Produce a concise human-readable summary of structural changes.
|
|
*/
|
|
function summarizeChanges(analysis: ChangeAnalysis): string {
|
|
const parts: string[] = [];
|
|
|
|
if (analysis.newFiles.length > 0) {
|
|
parts.push(`${analysis.newFiles.length} new`);
|
|
}
|
|
if (analysis.deletedFiles.length > 0) {
|
|
parts.push(`${analysis.deletedFiles.length} deleted`);
|
|
}
|
|
if (analysis.structurallyChangedFiles.length > 0) {
|
|
parts.push(`${analysis.structurallyChangedFiles.length} modified`);
|
|
}
|
|
|
|
return parts.join(", ");
|
|
}
|