Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ claude mcp add -s user -t stdio codebase-intelligence -- npx -y codebase-intelli
- **18 CLI commands** for architecture analysis, dependency impact, improvement opportunities, dead code detection, search, CI rules, and agent setup
- **Machine-readable JSON output** (`--json`) for automation and CI pipelines
- **Auto-cached index** in `.codebase-intelligence/` for fast repeat queries
- **Cache migration facts** in JSON (`cacheDir`, `legacyCacheDir`, `migrated`, `gitignoreUpdated`, `warnings[]`)
- **11 architectural metrics** — PageRank, betweenness, coupling, cohesion, tension, churn, complexity, blast radius, dead exports, test coverage, escape velocity
- **Symbol-level analysis** — callers/callees, symbol importance, impact blast radius
- **BM25 search** — ranked keyword search across files and symbols
Expand Down Expand Up @@ -123,6 +124,8 @@ codebase-intelligence <command> <path> [options]

The scanner always excludes common generated and agent-workspace directories such as `.codebase-intelligence/`, legacy `.code-visualizer/`, `.next/`, `dist/`, `coverage/`, `.worktrees/`, and `.claude/worktrees/`.

Analysis commands with `--json` and `init --json` include a top-level `cache` object with the canonical cache path, legacy cache path, migration status, `.gitignore` update status, and non-fatal cache warnings.

For full command details, see [docs/cli-reference.md](docs/cli-reference.md).

## Agent Adoption
Expand Down
14 changes: 13 additions & 1 deletion docs/cli-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -199,7 +199,7 @@ The global skill is never installed unless `--skill` is passed.
codebase-intelligence init [path] [--agents <list>] [--all] [--skill] [--gitignore] [--yes] [--json]
```

**Output:** per-file actions (created / updated / unchanged) and skill install status.
**Output:** per-file actions (created / updated / unchanged), optional `.gitignore` action, skill install status, and cache facts in JSON mode.

## Flags

Expand Down Expand Up @@ -234,6 +234,18 @@ codebase-intelligence init [path] [--agents <list>] [--all] [--skill] [--gitigno

**Auto-caching:** First CLI invocation parses the codebase and saves the index to `.codebase-intelligence/`. Subsequent commands use the cache only when `git HEAD`, dirty/untracked file contents under the analyzed path, the CLI version, and parser cache settings match. Legacy `.code-visualizer/` is migrated when `.codebase-intelligence/` is absent. Add `.codebase-intelligence/` to `.gitignore` manually or run `codebase-intelligence init --gitignore`.

**Cache JSON facts:** Analysis commands with `--json` and `init --json` include a top-level `cache` object:

```json
{
"cacheDir": "/repo/.codebase-intelligence",
"legacyCacheDir": "/repo/.code-visualizer",
"migrated": false,
"gitignoreUpdated": false,
"warnings": []
}
```

**Default scanner excludes:** The parser always skips `.git`, `node_modules`, `.codebase-intelligence`, legacy `.code-visualizer`, `.next`, `dist`, `coverage`, `.turbo`, `.cache`, `.worktrees`, and `.claude/worktrees`, even if the target repo has no matching `.gitignore` entry.

**Large repo mode:** Repos above 1500 TypeScript files use a lightweight AST parser by default to avoid TypeScript program OOM. File/import/export/dependency metrics remain available; type-resolved call graph details are reduced. Set `CBI_FULL_PROGRAM_FILE_LIMIT=<n>` to tune the cutoff.
Expand Down
15 changes: 15 additions & 0 deletions docs/data-model.md
Original file line number Diff line number Diff line change
Expand Up @@ -116,3 +116,18 @@ CodebaseGraph {
}
}
```

## CLI Cache Facts

Analysis commands with `--json` and `init --json` include this top-level `cache`
object. Paths are absolute.

```typescript
CacheFacts {
cacheDir: string // Canonical .codebase-intelligence/ directory
legacyCacheDir: string // Legacy .code-visualizer/ directory
migrated: boolean // true when legacy-only cache moved this run
gitignoreUpdated: boolean // true when init --gitignore wrote/updated .gitignore
warnings: string[] // Non-fatal legacy/cache state warnings
}
```
2 changes: 2 additions & 0 deletions llms-full.txt
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ analyzeGraph(builtGraph, parsedFiles)
- **Dead export detection**: Cross-references parsed exports against edge symbol lists. May miss `import *` or re-exports.
- **Graceful degradation**: Non-git dirs get churn=0, no-test codebases get coverage=false. Never crashes.
- **Auto-caching**: CLI commands always cache the graph index to `.codebase-intelligence/`. MCP mode requires `--index` to persist. Legacy `.code-visualizer/` is migrated when canonical cache is absent.
- **Cache JSON facts**: Analysis commands with `--json` and `init --json` include top-level `cache`: `cacheDir`, `legacyCacheDir`, `migrated`, `gitignoreUpdated`, `warnings[]`.

---

Expand Down Expand Up @@ -426,6 +427,7 @@ Gate modes: all, new-only. Returns pass/warn/fail verdict, findings, and summary
## Global Behavior

- **Auto-caching**: First run parses and saves index to `.codebase-intelligence/`. Subsequent runs use cache only when HEAD, dirty/untracked file contents under the analyzed path, CLI version, and parser cache settings match. Legacy `.code-visualizer/` is migrated when canonical cache is absent.
- **Cache JSON facts**: Analysis commands with `--json` and `init --json` include top-level `cache`: `cacheDir`, `legacyCacheDir`, `migrated`, `gitignoreUpdated`, `warnings[]`.
- **Default scanner excludes**: `.git`, `node_modules`, `.codebase-intelligence`, legacy `.code-visualizer`, `.next`, `dist`, `coverage`, `.turbo`, `.cache`, `.worktrees`, and `.claude/worktrees`.
- **Monorepo imports**: Root tsconfig path aliases and local package.json package names resolve to source files before graph construction.
- **Large repo mode**: Above 1500 TypeScript files, use AST-only extraction to avoid TypeScript program OOM. Override with `CBI_FULL_PROGRAM_FILE_LIMIT`.
Expand Down
1 change: 1 addition & 0 deletions llms.txt
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ codebase-intelligence init . # make AI agents use CI

- CLI analysis commands cache to `.codebase-intelligence/`; legacy `.code-visualizer/` is migration input only. Run `codebase-intelligence init --gitignore` to ignore the canonical cache directory.
- `overview --json` includes `analysis.mode`, `analysis.callGraphPrecision`, and `analysis.fullProgramFileLimit`.
- Analysis commands with `--json` and `init --json` include `cache`: `cacheDir`, `legacyCacheDir`, `migrated`, `gitignoreUpdated`, `warnings[]`.
- Repos above the full-program file limit use AST-only extraction; dependency metrics remain available, call graph precision becomes syntax-only.

## Optional
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
"build": "tsc -p tsconfig.build.json",
"dev": "tsx src/cli.ts",
"start": "node dist/cli.js",
"test": "pnpm build && vitest run --pool threads --no-file-parallelism",
"test": "pnpm build && vitest run --no-cache --pool threads --no-file-parallelism --maxWorkers=1",
"test:coverage": "pnpm build && vitest run --coverage --pool threads --no-file-parallelism --exclude 'tests/*.e2e.test.ts'",
"verify:cli-real": "pnpm build && node tools/verify-cli-real-codebases.mjs",
"verify:cli-real:heavy": "pnpm build && CBI_REAL_PROFILE=heavy node tools/verify-cli-real-codebases.mjs",
Expand Down
14 changes: 6 additions & 8 deletions roadmap.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
> Future work only. Deterministic, graph-native codebase intelligence for TypeScript & JavaScript.
> Read-first, agent-native, architecture-aware. No invented findings: every claim is graph-backed evidence a human or agent can inspect.

**Last updated:** 2026-06-30
**Last updated:** 2026-07-01

## Direction

Expand Down Expand Up @@ -60,7 +60,7 @@ Guiding constraints for all future work:

| Item | Dev-ready contract |
|---|---|
| Cache migration | All cache states are covered by tests: only legacy, only canonical, both same signature, both different signature, neither. Scanner excludes both folders. Writes go only to `.codebase-intelligence/`. `init --gitignore` is idempotent. |
| Cache migration | All cache states are covered by tests: only legacy, only canonical, both same signature, both different signature, neither. Scanner excludes both folders. Writes go only to `.codebase-intelligence/`. `init --gitignore` is idempotent. JSON surfaces expose `cacheDir`, `legacyCacheDir`, `migrated`, `gitignoreUpdated`, and `warnings[]`. |
| Docs/help alignment | README, CLI help, `docs/cli-reference.md`, `docs/mcp-tools.md`, `docs/data-model.md`, `llms.txt`, and `llms-full.txt` describe `.codebase-intelligence/` as canonical and `.code-visualizer/` as legacy migration input. |
| Test-runner determinism | `pnpm test` exits 0 locally after assertions pass; the Vitest worker `onTaskUpdate` timeout is gone or the runner configuration is made deterministic. |
| Targeted framework fixes | Any remaining framework/config false positive has a minimal fixture proving the bug and a focused fix. No broad plugin framework lands in `2.5.0`. |
Expand Down Expand Up @@ -144,7 +144,7 @@ Purpose: prove the CLI, MCP server, library outputs, docs, CI behavior, and agen

| Chain | User story | Required actions |
|---|---|---|
| CH-P0-01 cache migration | As US-MIGRATOR, I can upgrade without losing or duplicating cache state. | Create fixture with only `.code-visualizer/` -> run real CLI command -> assert `.codebase-intelligence/` exists -> assert legacy handling warning -> rerun command -> assert canonical cache is used -> run `--status` -> run `--clean`. |
| CH-P0-01 cache migration | As US-MIGRATOR, I can upgrade without losing or duplicating cache state. | Create fixture with only `.code-visualizer/` -> run real CLI command -> assert `.codebase-intelligence/` exists -> assert JSON `cache.migrated` -> rerun command -> assert canonical cache is used -> run `--status` -> run `--clean`. |
| CH-P0-02 new cache gitignore | As US-CLI-HUMAN, I can initialize ignore rules safely. | Run init/gitignore command in temp repo -> assert `.gitignore` gets `.codebase-intelligence/` once -> rerun -> assert idempotent -> assert legacy folder is documented as migration input only. |
| CH-P0-03 docs/help alignment | As US-CLI-AGENT, I can learn the canonical cache path from every surface. | Build CLI -> capture `--help` and command help -> scan README/docs/LLM docs -> assert `.codebase-intelligence/` canonical wording -> assert `.code-visualizer/` appears only in legacy migration context. |
| CH-P0-04 deterministic test runner | As US-MAINTAINER, I can trust release gates. | Run `pnpm test` -> assert exit 0 -> assert no Vitest `onTaskUpdate` timeout -> rerun focused long E2E suite -> assert no open-handle/worker timeout. |
Expand Down Expand Up @@ -204,7 +204,7 @@ Purpose: prove the CLI, MCP server, library outputs, docs, CI behavior, and agen

Rename the legacy index/cache folder from `.code-visualizer/` to `.codebase-intelligence/` without breaking existing users.

**Status:** Partially shipped in the P0 canary PR.
**Status:** Shipped across P0 canary PRs.

**Shipped:**

Expand All @@ -215,10 +215,7 @@ Rename the legacy index/cache folder from `.code-visualizer/` to `.codebase-inte
- Add `init --gitignore` to append `.codebase-intelligence/` idempotently.
- Add `--clean` behavior that removes canonical and legacy cache directories.
- Update docs, README, CLI help, tests, and fixtures to mention legacy auto-migration.

**Remaining:**

- Expose migration facts in JSON: `cacheDir`, `legacyCacheDir`, `migrated`, `gitignoreUpdated`, `warnings[]`.
- Expose migration facts in JSON on analysis commands and `init --json`: `cacheDir`, `legacyCacheDir`, `migrated`, `gitignoreUpdated`, `warnings[]`.

### Docs + Help Alignment

Expand All @@ -243,6 +240,7 @@ Fix the repeated Vitest runner timeout so release gates are deterministic.
- Switch `pnpm test` to `--pool threads --no-file-parallelism`.
- Keep `429` assertions passing and make `pnpm test` exit 0 locally.
- Bound non-blocking CI coverage collection so coverage hangs cannot block the release gate.
- Bound `pnpm test` to one Vitest worker and disabled Vitest duration cache so host-level memory pressure and stale file ordering do not terminate the release gate.

### Targeted Framework Entry Fixes

Expand Down
28 changes: 24 additions & 4 deletions src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,13 @@ import { startMcpServer } from "./mcp/index.js";
import { setIndexedHead, setRoot } from "./server/graph-store.js";
import { exportGraph, importGraph } from "./persistence/index.js";
import { getCacheKey } from "./persistence/cache-key.js";
import { cleanIndexDirectories, prepareIndexDirectory, type IndexDirectoryResolution } from "./persistence/index-dir.js";
import {
cleanIndexDirectories,
getCacheFacts,
getCacheFactsForTarget,
prepareIndexDirectory,
type IndexDirectoryResolution,
} from "./persistence/index-dir.js";
import {
computeOverview,
computeFileContext,
Expand Down Expand Up @@ -58,10 +64,12 @@ import { promptSelection } from "./install/prompt.js";
import { runCheck, exitCodeFor } from "./rules/check.js";
import { formatResult, formatSummaryLine } from "./rules/format.js";
import { ConfigError } from "./config/index.js";
import type { CodebaseGraph, OutputFormat } from "./types/index.js";
import type { CacheFacts, CodebaseGraph, OutputFormat } from "./types/index.js";

// ── Helpers ─────────────────────────────────────────────────

let activeCacheFacts: CacheFacts | null = null;

function reportIndexMigration(resolution: IndexDirectoryResolution): void {
if (resolution.migration === "migrated-legacy") {
progress(`Migrated legacy index ${resolution.legacyDir} to ${resolution.canonicalDir}`);
Expand Down Expand Up @@ -92,8 +100,13 @@ function output(data: string): void {
process.stdout.write(`${data}\n`);
}

function isJsonObject(data: unknown): data is Record<string, unknown> {
return typeof data === "object" && data !== null && !Array.isArray(data);
}

function outputJson(data: unknown): void {
process.stdout.write(`${JSON.stringify(data, null, 2)}\n`);
const payload = activeCacheFacts && isJsonObject(data) ? { ...data, cache: activeCacheFacts } : data;
process.stdout.write(`${JSON.stringify(payload, null, 2)}\n`);
}

/** Load (or parse+cache) the codebase graph for a target path. */
Expand All @@ -107,6 +120,7 @@ function loadGraph(targetPath: string, force = false): { graph: CodebaseGraph; h

const indexResolution = prepareIndexDirectory(targetPath);
reportIndexMigration(indexResolution);
activeCacheFacts = getCacheFacts(indexResolution);
const indexDir = indexResolution.activeDir;
const headHash = getHeadHash(targetPath);
const cacheKey = getCacheKey(targetPath, { headHash, cliVersion: pkg.version });
Expand Down Expand Up @@ -1073,7 +1087,12 @@ program
const skillResult = installSkill ? installGlobalSkill() : undefined;

if (options.json) {
outputJson({ repoFiles: repoResults, gitignore: gitignoreResult ?? null, skill: skillResult ?? null });
outputJson({
repoFiles: repoResults,
gitignore: gitignoreResult ?? null,
skill: skillResult ?? null,
cache: getCacheFactsForTarget(resolved, gitignoreResult?.action === "created" || gitignoreResult?.action === "updated"),
});
return;
}

Expand Down Expand Up @@ -1220,6 +1239,7 @@ async function runMcpMode(targetPath: string, options: McpOptions): Promise<void

const indexResolution = prepareIndexDirectory(targetPath);
reportIndexMigration(indexResolution);
activeCacheFacts = getCacheFacts(indexResolution);
const indexDir = indexResolution.activeDir;

if (options.status) {
Expand Down
56 changes: 56 additions & 0 deletions src/persistence/index-dir.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import fs from "fs";
import path from "path";
import { CANONICAL_INDEX_DIR_NAME, LEGACY_INDEX_DIR_NAME } from "./cache-key.js";
import type { CacheFacts } from "../types/index.js";

type IndexDirectoryMigration = "none" | "migrated-legacy" | "ignored-legacy";

Expand Down Expand Up @@ -49,6 +50,61 @@ export function prepareIndexDirectory(targetPath: string): IndexDirectoryResolut
};
}

function getLegacyWarnings(resolution: IndexDirectoryResolution): string[] {
if (resolution.migration === "ignored-legacy") {
return [`Legacy cache directory exists but ${resolution.canonicalDir} is active: ${resolution.legacyDir}`];
}

if (!fs.existsSync(resolution.legacyDir)) return [];

try {
if (!fs.statSync(resolution.legacyDir).isDirectory()) {
return [`Legacy cache path exists but is not a directory and was left unchanged: ${resolution.legacyDir}`];
}
} catch {
return [`Legacy cache path could not be inspected and was left unchanged: ${resolution.legacyDir}`];
}

return [];
}

/**
* Convert cache path resolution into the stable JSON facts emitted by CLI surfaces.
*
* @param resolution - Cache directory resolution returned by `prepareIndexDirectory`.
* @param gitignoreUpdated - Whether the current command updated `.gitignore`.
* @returns Machine-readable cache migration facts.
*/
export function getCacheFacts(resolution: IndexDirectoryResolution, gitignoreUpdated = false): CacheFacts {
return {
cacheDir: resolution.canonicalDir,
legacyCacheDir: resolution.legacyDir,
migrated: resolution.migration === "migrated-legacy",
gitignoreUpdated,
warnings: getLegacyWarnings(resolution),
};
}

/**
* Return cache facts for commands that do not resolve or migrate the index first.
*
* @param targetPath - Repo/codebase root passed to the CLI.
* @param gitignoreUpdated - Whether the current command updated `.gitignore`.
* @returns Machine-readable cache facts with migration set to false.
*/
export function getCacheFactsForTarget(targetPath: string, gitignoreUpdated = false): CacheFacts {
const { canonicalDir, legacyDir } = getIndexDirs(targetPath);
return getCacheFacts(
{
activeDir: canonicalDir,
canonicalDir,
legacyDir,
migration: canonicalDir !== legacyDir && fs.existsSync(canonicalDir) && fs.existsSync(legacyDir) ? "ignored-legacy" : "none",
},
gitignoreUpdated,
);
}

/**
* Remove canonical and legacy cache directories for explicit clean commands.
*
Expand Down
8 changes: 8 additions & 0 deletions src/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -286,6 +286,14 @@ export interface BoundariesConfig {

export type OutputFormat = "text" | "json" | "sarif";

export interface CacheFacts {
cacheDir: string;
legacyCacheDir: string;
migrated: boolean;
gitignoreUpdated: boolean;
warnings: string[];
}

export interface CodebaseIntelligenceConfig {
root?: string;
include?: string[];
Expand Down
Loading
Loading