How Incremental Analysis Works with Structural Fingerprints in Understand Anything

Understand Anything combines SHA-256 content hashing with AST-derived structural fingerprints to detect whether code changes affect the knowledge graph's topology, enabling targeted incremental updates that skip cosmetic edits.

Understand Anything, from the Egonex-AI repository, builds a comprehensive knowledge graph mapping functions, classes, imports, and exports across your codebase. When files change, the tool leverages incremental analysis with structural fingerprints to avoid rebuilding the entire graph. By generating deterministic signatures for each file's structural elements and comparing them between Git commits, the system precisely isolates which modifications require graph updates versus those that merely alter implementation details.

Creating Structural Fingerprints from Source Code

The fingerprinting engine resides in understand-anything-plugin/packages/core/src/fingerprint.ts, where each source file is converted into a compact, deterministic representation of its graph-relevant elements.

Extracting Signatures with extractFileFingerprint

The extractFileFingerprint function processes Tree-sitter parse results to build a FileFingerprint object containing function signatures, class definitions, imports, exports, and a SHA-256 hash of the raw content:

export function extractFileFingerprint(
  filePath: string,
  content: string,
  analysis: StructuralAnalysis,
): FileFingerprint {
  const hash = contentHash(content);                               // ← content hash
  const exportedNames = new Set(analysis.exports.map(e => e.name));

  const functions = analysis.functions.map(fn => ({
    name: fn.name,
    params: [...fn.params],
    returnType: fn.returnType,
    exported: exportedNames.has(fn.name),
    lineCount: fn.lineRange[1] - fn.lineRange[0] + 1,
  }));

  const classes = analysis.classes.map(cls => ({
    name: cls.name,
    methods: [...cls.methods],
    properties: [...cls.properties],
    exported: exportedNames.has(cls.name),
    lineCount: cls.lineRange[1] - cls.lineRange[0] + 1,
  }));

  const imports = analysis.imports.map(imp => ({
    source: imp.source,
    specifiers: [...imp.specifiers],
  }));

  return {
    filePath,
    contentHash: hash,
    functions,
    classes,
    imports,
    exports: analysis.exports.map(e => e.name),
    totalLines: content.split("\n").length,
    hasStructuralAnalysis: true,
  };
}

The Tree-sitter parser, registered in understand-anything-plugin/packages/core/src/plugins/registry.ts, supplies the StructuralAnalysis object containing AST-extracted elements. For unsupported languages, the system stores only the content hash and sets hasStructuralAnalysis: false, triggering a conservative full re-analysis on any change.

Persisting Fingerprints in the FingerprintStore

All fingerprints for a project scan are aggregated via buildFingerprintStore in fingerprint.ts:

export function buildFingerprintStore(
  projectDir: string,
  filePaths: string[],
  registry: PluginRegistry,
  gitCommitHash: string,
): FingerprintStore { … }

This store is serialized alongside the knowledge graph (typically in .understand-anything/knowledge-graph.json), preserving the state of the codebase at a specific Git commit.

Detecting Changes Between Analysis Runs

When a new analysis begins, the system determines repository staleness through Git operations and fingerprint comparison logic located in understand-anything-plugin/packages/core/src/staleness.ts.

Identifying Modified Files via Git

The getChangedFiles function calculates the delta between the stored Git commit and the current HEAD:

export function getChangedFiles(projectDir: string, lastCommitHash: string): string[] {
  // runs `git diff <last>..HEAD --name-only`
}

This returns a list of file paths modified since the last analysis, providing the input for the incremental comparison stage.

Classifying Changes with compareFingerprints

For each modified file, analyzeChanges (also in staleness.ts) generates a fresh fingerprint and invokes compareFingerprints to categorize the change:

export function compareFingerprints(
  oldFp: FileFingerprint,
  newFp: FileFingerprint,
): FileChangeResult {
  // ① identical content → NONE
  // ② missing structural analysis → STRUCTURAL (conservative)
  // ③ otherwise compare function & class signatures, imports & exports
  //      – any signature difference → STRUCTURAL
  //      – only content diff, signatures identical → COSMETIC
}

The function returns one of three change levels:

  • NONE: The file is byte-for-byte identical.
  • COSMETIC: Only implementation details changed (comments, internal logic); the knowledge graph requires no updates.
  • STRUCTURAL: Signatures changed (new parameters, added exports, modified imports); the graph must be refreshed for this file.

The analyzeChanges function aggregates these results into a ChangeAnalysis object containing arrays for newFiles, deletedFiles, structurallyChangedFiles, cosmeticOnlyFiles, and unchangedFiles.

Incrementally Updating the Knowledge Graph

After change detection, mergeGraphUpdate (implemented in staleness.ts) performs a targeted merge:

  • Removes nodes and edges associated with structurallyChangedFiles, newFiles, or deletedFiles.
  • Adds fresh nodes and edges generated from re-parsing the structurally changed files.
  • Preserves all existing graph elements linked to unchanged or cosmetically modified files.

This selective update strategy ensures that incremental analysis touches only the minimal subgraph affected by structural changes, dramatically reducing analysis time for large codebases.

Practical Implementation Examples

Running an Incremental Scan

import { readFileSync } from "fs";
import {
  buildFingerprintStore,
  analyzeChanges,
  compareFingerprints,
} from "./packages/core/src/fingerprint.js";
import { getChangedFiles } from "./packages/core/src/staleness.js";
import { registry } from "./packages/core/src/plugins/registry.js";

const projectDir = "/my/project";
const lastStore = JSON.parse(readFileSync("./.understand-anything/fingerprint-store.json", "utf-8"));
const changed = getChangedFiles(projectDir, lastStore.gitCommitHash);

const changeReport = analyzeChanges(projectDir, changed, lastStore, registry);
console.log("Structural changes:", changeReport.structurallyChangedFiles);

Manually Comparing Two Fingerprints

import { compareFingerprints } from "./packages/core/src/fingerprint.js";

const oldFp = …; // loaded from previous store
const newFp = …; // freshly extracted

const result = compareFingerprints(oldFp, newFp);
console.log(`File ${result.filePath} → ${result.changeLevel}`);

Building a Fresh Fingerprint Store

import { glob } from "fast-glob";
import { buildFingerprintStore } from "./packages/core/src/fingerprint.js";
import { registry } from "./packages/core/src/plugins/registry.js";

const allTs = await glob("src/**/*.ts", { cwd: projectDir });
const store = buildFingerprintStore(projectDir, allTs, registry, "currentGitHash");
await writeFile("./.understand-anything/fingerprint-store.json", JSON.stringify(store));

Summary

  • Structural fingerprints in Understand Anything capture SHA-256 content hashes alongside AST-derived signatures for functions, classes, imports, and exports.
  • The extractFileFingerprint function in fingerprint.ts generates these deterministic snapshots, while compareFingerprints classifies changes as NONE, COSMETIC, or STRUCTURAL.
  • Git integration via getChangedFiles identifies modified files, triggering targeted re-analysis only for structurally changed components.
  • The mergeGraphUpdate process preserves existing knowledge graph nodes for cosmetic changes while rebuilding only affected subgraphs, optimizing performance for incremental runs.

Frequently Asked Questions

What constitutes a structural fingerprint in Understand Anything?

A structural fingerprint is a deterministic object generated by extractFileFingerprint in packages/core/src/fingerprint.ts that combines a SHA-256 hash of the file's content with serialized signatures of its functions (names, parameters, return types), classes (methods, properties), imports, and exports. This compact representation allows the system to detect whether changes affect the knowledge graph's topology without re-parsing unchanged files.

How does the system distinguish between cosmetic and structural code changes?

The compareFingerprints function compares old and new fingerprints: if the content hashes differ but all function signatures, class definitions, import statements, and export declarations remain identical, the change is classified as COSMETIC. If any structural element differs, or if the file lacks parser support (triggering conservative analysis), the change is marked STRUCTURAL, requiring a knowledge graph update.

What happens when Understand Anything encounters an unsupported file type?

According to the fallback logic in extractFileFingerprint, files without Tree-sitter parser support store only the SHA-256 content hash and set hasStructuralAnalysis: false. During comparison, these files automatically trigger a STRUCTURAL classification on any content change, ensuring the knowledge graph remains accurate even when detailed AST analysis is unavailable.

Where are fingerprints stored between analysis runs?

The buildFingerprintStore function aggregates all file fingerprints into a FingerprintStore object, which is serialized alongside the knowledge graph (typically in .understand-anything/knowledge-graph.json). This store includes the Git commit hash, enabling getChangedFiles to calculate deltas between the stored state and the current repository HEAD during subsequent incremental analysis runs.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →