From d37459e5bff3a2d19e16b857b7297a24d4b114dd Mon Sep 17 00:00:00 2001 From: bntvllnt <32437578+bntvllnt@users.noreply.github.com> Date: Wed, 1 Jul 2026 00:08:14 +0200 Subject: [PATCH 1/2] feat(cache): migrate canonical cache directory --- .gitignore | 1 + README.md | 4 +- docs/architecture.md | 5 +- docs/cli-reference.md | 13 +- llms-full.txt | 11 +- llms.txt | 1 + package.json | 2 +- roadmap.md | 744 ++++++++++++++++++++++------------ src/cli.ts | 48 ++- src/install/index.test.ts | 26 ++ src/install/index.ts | 34 +- src/parser/index.test.ts | 1 + src/parser/index.ts | 1 + src/persistence/cache-key.ts | 6 +- src/persistence/index-dir.ts | 69 ++++ tests/cache-key.test.ts | 24 +- tests/cli-cache.e2e.test.ts | 75 ++++ tests/cli-init.e2e.test.ts | 11 + tests/index-directory.test.ts | 116 ++++++ tests/phase6-red.test.ts | 18 +- 20 files changed, 912 insertions(+), 298 deletions(-) create mode 100644 src/persistence/index-dir.ts create mode 100644 tests/cli-cache.e2e.test.ts create mode 100644 tests/index-directory.test.ts diff --git a/.gitignore b/.gitignore index a51c03d..adb96d9 100644 --- a/.gitignore +++ b/.gitignore @@ -12,6 +12,7 @@ test-results/ scripts/ # analysis + coverage artifacts +.codebase-intelligence/ .code-visualizer/ coverage/ diff --git a/README.md b/README.md index e2276bf..21b37bc 100644 --- a/README.md +++ b/README.md @@ -58,7 +58,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 `.code-visualizer/` for fast repeat queries +- **Auto-cached index** in `.codebase-intelligence/` for fast repeat queries - **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 @@ -121,7 +121,7 @@ codebase-intelligence [options] | `--metric ` | Select ranking metric for `hotspots` | | `--scope ` | Select git diff scope for `changes`: `staged`, `unstaged`, `all` | -The scanner always excludes common generated and agent-workspace directories such as `.code-visualizer/`, `.next/`, `dist/`, `coverage/`, `.worktrees/`, and `.claude/worktrees/`. +The scanner always excludes common generated and agent-workspace directories such as `.codebase-intelligence/`, legacy `.code-visualizer/`, `.next/`, `dist/`, `coverage/`, `.worktrees/`, and `.claude/worktrees/`. For full command details, see [docs/cli-reference.md](docs/cli-reference.md). diff --git a/docs/architecture.md b/docs/architecture.md index a00b35f..5be8db7 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -43,7 +43,8 @@ src/ search/index.ts <- BM25 search engine process/index.ts <- Entry point detection + call chain tracing community/index.ts <- Louvain clustering - persistence/index.ts <- Graph export/import to .code-visualizer/ + persistence/index.ts <- Graph export/import to .codebase-intelligence/ + persistence/index-dir.ts <- Canonical cache path + legacy .code-visualizer/ migration persistence/cache-key.ts <- Cache signature from HEAD, worktree content, CLI version, parser settings install/index.ts <- Agent adoption: managed-block engine + per-agent file targets + skill server/graph-store.ts <- Global graph state (shared by CLI + MCP) @@ -80,7 +81,7 @@ startMcpServer(codebaseGraph) - **Dead export detection**: Cross-references parsed exports against edge symbol lists. May miss `import *` or re-exports (known limitation). - **Graceful degradation**: Non-git dirs get churn=0, no-test codebases get coverage=false. Never crashes. - **Built-in scanner excludes**: Generated indexes, build outputs, coverage, framework caches, and agent worktrees are skipped before TypeScript program creation. -- **Graph persistence**: CLI commands always cache the graph index to `.code-visualizer/`. Cache reuse requires matching HEAD, dirty/untracked file-content fingerprint, CLI version, and parser cache settings. MCP mode (`codebase-intelligence `) requires `--index` to persist the cache. +- **Graph persistence**: CLI commands always cache the graph index to `.codebase-intelligence/`. A legacy `.code-visualizer/` cache is migrated when canonical cache is absent. Cache reuse requires matching HEAD, dirty/untracked file-content fingerprint, CLI version, and parser cache settings. MCP mode (`codebase-intelligence `) requires `--index` to persist the cache. ## Adding a New Metric diff --git a/docs/cli-reference.md b/docs/cli-reference.md index cdc072b..f14edeb 100644 --- a/docs/cli-reference.md +++ b/docs/cli-reference.md @@ -1,6 +1,6 @@ # CLI Reference -18 commands for terminal and CI use. The 16 analysis commands have full parity with MCP tools and auto-cache the index to `.code-visualizer/`; `check` runs the rules gate; `init` sets up agent adoption. +18 commands for terminal and CI use. The 16 analysis commands have full parity with MCP tools and auto-cache the index to `.codebase-intelligence/`; `check` runs the rules gate; `init` sets up agent adoption. ## Commands @@ -196,7 +196,7 @@ preselected). Non-interactively (or with `--yes`/`--json`) it defaults to those The global skill is never installed unless `--skill` is passed. ```bash -codebase-intelligence init [path] [--agents ] [--all] [--skill] [--yes] [--json] +codebase-intelligence init [path] [--agents ] [--all] [--skill] [--gitignore] [--yes] [--json] ``` **Output:** per-file actions (created / updated / unchanged) and skill install status. @@ -227,13 +227,14 @@ codebase-intelligence init [path] [--agents ] [--all] [--skill] [--yes] [- | `--agents ` | init | Comma-separated agents, non-interactive (default: agents,claude) | | `--all` | init | Target every agent (non-interactive) | | `--skill` | init | Also install the global Claude skill (opt-in) | +| `--gitignore` | init | Add `.codebase-intelligence/` to `.gitignore` idempotently | | `-y, --yes` | init | Accept defaults without prompting | ## Behavior -**Auto-caching:** First CLI invocation parses the codebase and saves the index to `.code-visualizer/`. 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. Add `.code-visualizer/` to `.gitignore`. +**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`. -**Default scanner excludes:** The parser always skips `.git`, `node_modules`, `.code-visualizer`, `.next`, `dist`, `coverage`, `.turbo`, `.cache`, `.worktrees`, and `.claude/worktrees`, even if the target repo has no matching `.gitignore` entry. +**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=` to tune the cutoff. @@ -245,7 +246,7 @@ codebase-intelligence init [path] [--agents ] [--all] [--skill] [--yes] [- - `2` — bad args or usage error **MCP mode:** Running `codebase-intelligence ` without a subcommand starts the MCP stdio server (backward compatible). MCP-specific flags: -- `--index` — persist graph index to `.code-visualizer/` (CLI auto-caches, MCP requires this flag) +- `--index` — persist graph index to `.codebase-intelligence/` (CLI auto-caches, MCP requires this flag) - `--status` — print index status and exit -- `--clean` — remove `.code-visualizer/` index and exit +- `--clean` — remove `.codebase-intelligence/` and legacy `.code-visualizer/` indexes and exit - `--force` — re-index even if the cache signature matches diff --git a/llms-full.txt b/llms-full.txt index 5ec29d9..3c47f4e 100644 --- a/llms-full.txt +++ b/llms-full.txt @@ -44,7 +44,8 @@ src/ search/index.ts <- BM25 search engine process/index.ts <- Entry point detection + call chain tracing community/index.ts <- Louvain clustering - persistence/index.ts <- Graph export/import to .code-visualizer/ + persistence/index.ts <- Graph export/import to .codebase-intelligence/ + persistence/index-dir.ts <- Canonical cache path + legacy .code-visualizer/ migration persistence/cache-key.ts <- Cache signature from HEAD, worktree content, CLI version, parser settings server/graph-store.ts <- Global graph state (shared by CLI + MCP) cli.ts <- Entry point, CLI commands + MCP fallback @@ -73,7 +74,7 @@ analyzeGraph(builtGraph, parsedFiles) - **Batch git churn**: Single `git log --all --name-only` call, parsed for all files. Avoids O(n) subprocess spawning. - **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 `.code-visualizer/`. MCP mode requires `--index` to persist. +- **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. --- @@ -403,7 +404,7 @@ Community-detected file clusters (Louvain algorithm). ### init ```bash -codebase-intelligence init [path] [--agents ] [--all] [--skill] [--yes] [--json] +codebase-intelligence init [path] [--agents ] [--all] [--skill] [--gitignore] [--yes] [--json] ``` Set up AI agents to use codebase-intelligence. Writes an idempotent, marked instruction block ("query CI before grep/read") into each selected agent's repo file — @@ -424,8 +425,8 @@ Gate modes: all, new-only. Returns pass/warn/fail verdict, findings, and summary ## Global Behavior -- **Auto-caching**: First run parses and saves index to `.code-visualizer/`. Subsequent runs use cache only when HEAD, dirty/untracked file contents under the analyzed path, CLI version, and parser cache settings match. -- **Default scanner excludes**: `.git`, `node_modules`, `.code-visualizer`, `.next`, `dist`, `coverage`, `.turbo`, `.cache`, `.worktrees`, and `.claude/worktrees`. +- **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. +- **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`. - **Real-repo verification**: `pnpm verify:cli-real` runs the default matrix. `pnpm verify:cli-real:heavy` sets `CBI_REAL_PROFILE=heavy` and verifies large `/home/ubuntu` repos with minimum file/dependency thresholds. diff --git a/llms.txt b/llms.txt index 75bad22..800327d 100644 --- a/llms.txt +++ b/llms.txt @@ -41,6 +41,7 @@ codebase-intelligence init . # make AI agents use CI ## Behavior Notes +- 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`. - Repos above the full-program file limit use AST-only extraction; dependency metrics remain available, call graph precision becomes syntax-only. diff --git a/package.json b/package.json index 41eb71e..443adb2 100644 --- a/package.json +++ b/package.json @@ -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 forks --no-file-parallelism", + "test": "pnpm build && vitest run --pool threads --no-file-parallelism", "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", diff --git a/roadmap.md b/roadmap.md index 8c57619..2a81e4f 100644 --- a/roadmap.md +++ b/roadmap.md @@ -1,369 +1,601 @@ # Codebase Intelligence — Roadmap -> 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. +> 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-10 · shipped through v2.4.1 + rules engine (#42) +**Last updated:** 2026-06-30 -This roadmap has two tracks: +## Direction -- **Parity track** — ship the deterministic static-analysis baseline the TS/JS ecosystem expects, so adopting us is never a downgrade. -- **Differentiation track** — ship analysis no competing static tool has, built on our graph engine: **data-flow convergence ("Highways")**, deep architecture intelligence, and an agent-native MCP surface. +Two tracks: -The bar: **isomorphic on the basics, materially better on architecture + reuse intelligence.** +- **Parity track** — close the static-analysis gaps TS/JS teams expect from a serious code-quality CLI. +- **Differentiation track** — ship graph-native analysis no token-only tool can offer: Highways, content drift, scope maps, and agent-ready context packs. + +Guiding constraints for all future work: + +1. **Read-only analyzer.** The tool reports and advises; source changes are applied by humans or agents. +2. **Deterministic evidence.** Findings come from AST, type, graph, git, config, and filesystem facts. No LLM-derived findings. +3. **Agent-native outputs.** Every finding needs stable IDs, JSON, evidence, and `actions[]` hints. +4. **Graph-first, visual-second.** 2D/3D views are derived from queryable graph data, never the product source of truth. +5. **CLI help is a product surface.** LLMs will use shell commands; help, examples, errors, and `--json` must stay predictable. +6. **Open-source naming hygiene.** Competitor/product names belong only in documentation comparison sections. Source, tests, CLI help, MCP output, fixtures, and generated agent instructions use generic capability labels unless naming a real dependency, standard, or integration. --- -## 1. Positioning +## 2.5.0 Canary Release Train +`2.5.0` is a multi-PR canary release train, not a single PR. Everything in this roadmap is allowed to land under `2.5.0-canary.*`; stable `2.5.0` is published only after the whole train has soaked in canary mode and the release gates pass. + +**Release goal:** ship the full codebase-intelligence direction as one cohesive `2.5.0` stable: cache identity cleanup, analyzer foundations, parity gaps, Highways, map/drift intelligence, agent-ready outputs, CI/dev ergonomics, and ecosystem hardening. + +**In scope for `2.5.0`:** + +1. P0 Stabilization. +2. P1 Analysis Foundations. +3. P2 Product Parity + Flagship Intelligence. +4. P3 Depth + Ergonomics. +5. P4 Ecosystem Hardening. + +**Out of scope for `2.5.0`:** + +- Auto-fix or source mutation. +- Production-runtime tracing or hosted cloud analysis. +- Visual-only graph product without structured graph outputs. +- Per-agent analyzer forks. +- Universal framework plugin catalog beyond targeted/configurable awareness. + +**Implementation order:** + +1. **Stabilization first** — cache migration, docs/help alignment, targeted entry fixes, deterministic tests. +2. **Operation registry second** — reduce CLI/MCP duplication before adding more surfaces. +3. **Analysis foundations third** — Type/Shape, duplication, dead-code expansion, suppression hygiene. +4. **Flagship intelligence fourth** — Highways, scope graph/map, content drift, health, boundaries. +5. **Agent/CI surfaces fifth** — CI wrapper, doctor, output formats, `actions[]`, context packs. +6. **Editor/depth sixth** — cognitive complexity, ownership/cohorting, LSP, architecture recommendations. +7. **Ecosystem hardening seventh** — watch, monorepo scope, migration, hooks, local history, production mode. +8. **Release hardening last** — canary soak, docs sync, real-repo verification, stable publish gates. + +**Per-item acceptance contracts:** + +| 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. | +| 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`. | +| Analysis foundations | Operation registry, Type/Shape, duplication, dead-code expansion, and suppression hygiene each ship with CLI/MCP parity or an explicit documented exception. | +| Flagship intelligence | Highways, map, drift, health, and boundaries produce deterministic JSON with evidence and stable IDs before any visual or prose-only surface is treated as complete. | +| Agent/CI surfaces | CI, doctor, output formats, `actions[]`, and context packs are usable by LLM-driven CLI workflows without scraping human prose. | +| Ecosystem hardening | Watch/monorepo/migration/hooks/history/production mode are gated by fixtures or real-repo verification, not demo-only behavior. | + +**2.5.0 release gates:** + +```bash +pnpm lint +pnpm typecheck +pnpm build +pnpm test +pnpm verify:cli-real ``` -┌──────────────────────────────────────────────────────────────┐ -│ The market expects: dead code, duplication, circular deps, │ -│ complexity hotspots, architecture boundaries, CI gates. │ -│ → We MUST match this (Parity track). Table stakes. │ -└──────────────────────────────────────────────────────────────┘ - │ - ▼ -┌──────────────────────────────────────────────────────────────┐ -│ Our moat: a real dependency + call + type graph, and the │ -│ metrics on top of it (PageRank, betweenness, tension, │ -│ bridges, communities, force analysis, process tracing). │ -│ → We do what graph-blind, token-only tools cannot: │ -│ reason about how DATA FLOWS and where it SHOULD converge. │ -└──────────────────────────────────────────────────────────────┘ -``` -**Where we do NOT compete:** +Every material PR publishes or validates a `2.5.0-canary.*` build before it is considered integrated. `pnpm test` must exit 0 before stable publish. The prior Vitest fork-pool `onTaskUpdate` timeout was reproduced and the release gate now uses the thread pool; any future runner-level timeout after passing assertions still blocks stable publish. + +**Docs required before stable:** -- **Raw scan speed.** Native-compiled competitors will out-scan a Node + TS Compiler API tool on millisecond benchmarks. We compete on *insight density per run*, not microseconds. (If speed ever blocks adoption, the answer is incremental/cached parsing and a possible native core — not a feature retreat.) -- **Production-runtime tracing** (V8/Istanbul hot-path, cold-path deletion evidence from live traffic, cloud ingestion). That is a separate product with separate infrastructure. Out of scope here; see §7. -- **A plugin for every framework on earth.** We ship framework *awareness* for the high-value few (§5.1), not a permanent plugin-maintenance treadmill. +- README command list and examples. +- `docs/cli-reference.md`. +- `docs/mcp-tools.md`. +- `docs/data-model.md` for cache migration output fields. +- `llms.txt` and `llms-full.txt`. +- Migration note for `.code-visualizer/` to `.codebase-intelligence/`. +- New command docs for every shipped CLI/MCP surface. +- Upgrade notes for any JSON shape additions. + +**Blocked decisions for `2.5.0`:** open decisions below must be resolved before implementing the feature they affect; unresolved late-phase decisions do not block earlier canaries. --- -## 2. Current capabilities (baseline) +## 2.5.0 Test Strategy — User Stories + Chained Actions -CLI + MCP, single shared core (`src/core`) for parity: +Purpose: prove the CLI, MCP server, library outputs, docs, CI behavior, and agent workflows work for every user type. Tests should exercise real code paths. Mock only true third-party edges; a mocked own-code seam is a gap. -`overview` · `hotspots` · `file` · `search` (BM25) · `changes` · `dependents` · `modules` · `forces` · `dead-exports` · `groups` · `symbol` · `impact` · `rename` (dry-run) · `processes` · `clusters` (Louvain) · `init` · `check` +**Detected surfaces:** -Graph engine: file dependency graph + call graph (`file::symbol`, type-resolved + text-inferred) + symbol nodes. Metrics: PageRank, betweenness, coupling, cohesion, tension, bridges, escape velocity, seams, locality risks, blast radius, churn (git), cyclomatic complexity, dead exports. Persisted index keyed by HEAD. +| Surface | Evidence | E2E meaning | +|---|---|---| +| CLI/tool | `package.json` `bin`, `src/cli.ts`, command docs | Spawn the real built binary with real args, assert stdout/stderr/exit/files. | +| MCP server | `src/mcp/`, tool schemas, stdio lifecycle tests | Start real stdio server, call tools with a real MCP client, assert schema/result/error envelopes. | +| Library/package surface | `exports`, `types`, `bin`, package-entrypoint metadata | Import the built package from a tiny consumer fixture and assert public API/types. | +| CI/release | `check`, SARIF, canary publish flow, GitHub Actions | Run the same commands CI runs and assert exit codes/artifacts. | +| Agent docs/instructions | `llms.txt`, `llms-full.txt`, generated agent instructions | An AI-agent persona can discover commands, run them, and parse machine output without scraping prose. | + +**Personas:** + +| ID | Persona | Surface | Need | +|---|---|---|---| +| US-CLI-HUMAN | Human terminal user | CLI | Clear help, predictable commands, actionable errors. | +| US-CLI-CI | Script/CI runner | CLI/CI | Stable exit codes, non-interactive behavior, machine-readable output. | +| US-CLI-AGENT | AI coding agent | CLI/docs | Deterministic `--json`, small context, next-step hints, no noisy prose in JSON. | +| US-MCP-AGENT | MCP agent client | MCP | Valid tool schemas, stable envelopes, parity with CLI. | +| US-LIB-DEV | Consuming developer | Library/package | Public entrypoints, types, and docs match runtime behavior. | +| US-DOWNSTREAM | Downstream app/runtime | Library/package | Built package works when imported from outside the repo. | +| US-MAINTAINER | Maintainer/releaser | CI/release | Canary/stable gates fail only on real blockers and produce useful artifacts. | +| US-MIGRATOR | Existing user with legacy cache | CLI/filesystem | `.code-visualizer/` migrates safely and never loses data silently. | +| US-ADVERSARY | Hostile input user | CLI/MCP/library | Bad paths, oversized args, malformed config, and hostile JSON fail safely. | + +**Surface x persona x layer matrix:** + +| Persona | Unit | Integration | E2E | UI | Chaining | +|---|---|---|---|---|---| +| US-CLI-HUMAN | Required | Required | Required | N/A - no TUI | Required | +| US-CLI-CI | Required | Required | Required | N/A - no TUI | Required | +| US-CLI-AGENT | Required | Required | Required | N/A - no TUI | Required | +| US-MCP-AGENT | Required | Required | Required | N/A - protocol surface | Required | +| US-LIB-DEV | Required | Required | Required consumer fixture | N/A - library | Required | +| US-DOWNSTREAM | Required | Required | Required consumer fixture | N/A - library | Required | +| US-MAINTAINER | Required | Required | Required CI command run | N/A - CI | Required | +| US-MIGRATOR | Required | Required filesystem fixture | Required CLI run | N/A - CLI | Required | +| US-ADVERSARY | Required | Required | Required negative CLI/MCP/library runs | N/A - no UI | Required | + +**Feature PR rule:** every material PR must add or update at least one chained test below. A chain is complete only when it proves setup -> action -> machine output -> downstream use or recovery. Happy-path-only tests do not close a chain. + +### P0 Chains — Stabilization + +| 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-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. | +| CH-P0-05 targeted entry fix | As US-LIB-DEV, I do not get false dead-code findings for supported entrypoints. | Add real fixture for proven false positive -> run `dead-exports`/`check` JSON -> assert entrypoint confidence/reason -> assert unsupported patterns remain configurable, not hardcoded magic. | -**Shipped since this roadmap was written (June 2026):** +### P1 Chains — Analysis Foundations -- **Config + rules engine foundation** (#40–#42, v2.4.x) — `codebase-intelligence.json` config (JSON Schema in `schema.json`), ESLint-style rules engine (`src/rules/`: registry + per-rule modules) with three rules (`no-comments`, `no-dead-exports`, `no-circular-deps`), a `check` command, exhaustive CLI+MCP E2E tests, and a CI merge gate. This is the substrate §5.5 (boundaries), §5.6 (audit gate), and §5.8 (suppressions) build on. -- **`init` opt-in agent selection** (#36, #38, v2.4.0–v2.4.1) — interactive picker, de-duplicated `--help`, init E2E coverage. +| Chain | User story | Required actions | +|---|---|---| +| CH-P1-01 operation parity | As US-MCP-AGENT and US-CLI-AGENT, I get equivalent facts from CLI and MCP. | Run one operation through descriptor registry -> invoke matching CLI command -> invoke matching MCP tool -> compare normalized JSON -> assert shared validation errors and hints. | +| CH-P1-02 graph-load pipeline | As US-MAINTAINER, new operations do not duplicate load/cache/error logic. | Trigger success, invalid input, parse failure, and cache reuse through registry adapter -> assert CLI/MCP adapters differ only in presentation envelope. | +| CH-P1-03 type/shape facts | As US-LIB-DEV, I can inspect producers/consumers of a type shape. | Parse fixture with aliases/generics/default exports/unresolved types -> assert symbol parameter/return facts -> query file/symbol/MCP surfaces -> assert additive JSON compatibility. | +| CH-P1-04 duplication families | As US-CLI-HUMAN, I can find duplicate logic with deterministic IDs. | Fixture exact clone + renamed clone + near-miss + below-threshold noise -> run CLI/MCP -> assert family IDs, thresholds, trace output, stable ordering, and no local-skip false positive. | +| CH-P1-05 dead code expansion | As US-CLI-CI, I can gate unused files/types/members/deps without framework false positives. | Fixture unused file/type/member/dependency plus supported entrypoint -> run check JSON/SARIF -> assert real findings, confidence, and ignored entrypoint evidence. | +| CH-P1-06 suppression hygiene | As US-MAINTAINER, suppressions do not hide stale debt forever. | Add active suppression -> assert finding suppressed and reported -> remove underlying finding -> assert stale suppression warning -> assert JSDoc `@public`/`@internal`/`@expected-unused` behavior. | ---- +### P2 Chains — Product Parity + Flagship Intelligence -## 3. Guiding principles +| Chain | User story | Required actions | +|---|---|---| +| CH-P2-01 highways reroute | As US-CLI-AGENT, I can identify divergent routes and get a safe reroute proposal. | Fixture with 3 entrypoints converging on one sink -> run `highways --json` -> assert cowpath/bypass findings, route chains, evidence, blast radius, proposed canonical node -> call MCP `analyze_highways` and compare. | +| CH-P2-02 highway synthesis | As US-CLI-HUMAN, I can understand the proposed canonical path before coding. | Fixture with no existing canonical node -> run `highways --propose --trace` -> assert synthesized name/location/signature/skeleton/reroute plan and cycle-safety check. | +| CH-P2-03 codebase map context pack | As US-MCP-AGENT, I can request only the files needed for one task. | Run `map --focus --context-budget --json` -> assert nodes/edges/evidence IDs -> request MCP context pack -> assert token-bounded ranked files/symbols/tests. | +| CH-P2-04 content drift | As US-MAINTAINER, I can find files whose names lie about behavior. | Fixture name/scope/side-effect/test mismatch -> run `drift --json` -> assert drift score, deterministic evidence, recommendation, baseline report-only first run. | +| CH-P2-05 health/boundaries | As US-CLI-CI, I can gate new architecture debt. | Fixture boundary violation + complexity/churn hotspot -> run health/boundary commands -> assert score, rule evidence, baseline/new-only behavior, stable exit codes. | +| CH-P2-06 CI wrapper | As US-MAINTAINER, I can add one PR gate. | Run `ci --base origin/main --new-only --format sarif` in temp git repo -> assert exit code, SARIF artifact, PR markdown, compact summary, and no failure on pre-existing debt. | +| CH-P2-07 doctor onboarding | As US-CLI-AGENT, I can self-diagnose setup. | Run `doctor --json` in repo missing config/cache/agent docs -> assert checks, levels, exact fix commands, docs links, read-only behavior. | +| CH-P2-08 output actions | As US-CLI-AGENT, every finding gives safe next steps. | For each analyzer finding kind -> assert stable ID, evidence, `actions[]`, no source mutation, and JSON schema compatibility. | + +### P3 Chains — Depth + Ergonomics + +| Chain | User story | Required actions | +|---|---|---| +| CH-P3-01 cognitive complexity | As US-CLI-HUMAN, I can distinguish branchy code from simple long code. | Fixture nested branches + flat long function -> run metrics surfaces -> assert cognitive vs cyclomatic values and hotspot ranking. | +| CH-P3-02 ownership/cohorting | As US-MAINTAINER, I can see risky owner concentration. | Temp git history + CODEOWNERS fixture -> run owner grouping -> assert bus-factor, owner hotspots, and effort filters. | +| CH-P3-03 LSP diagnostics | As US-LIB-DEV, editor diagnostics match batch analysis. | Start LSP against fixture -> open file -> assert diagnostics/hover facts match CLI JSON for same graph -> assert code actions are advisory only. | +| CH-P3-04 architecture recommendations | As US-CLI-AGENT, I can act on extraction recommendations. | Fixture bridge/tension/seam -> run architecture recommendations -> assert effort, evidence, affected files, and context pack. | -1. **Read-only.** The tool never mutates source. There is no auto-fix. Findings carry agent-applicable `action` hints (what a fix *would* change), but applying them is the agent's or human's job. This keeps the trust model simple and safe in CI and agent loops. -2. **Deterministic.** Same input → same output. No AI-invented findings. Evidence is inspectable. -3. **Agent-native.** Every finding is machine-actionable JSON with an `actions[]` array and stable IDs. MCP is a first-class surface, not an afterthought. -4. **Graph-native differentiation.** If a token-only linter could compute it, it's parity, not moat. Our unique value is graph + type reasoning. -5. **Boundary-only validation, surgical scope.** New analysis = new module under `src//`, wired through `src/core`. Never edit hubs. -6. **Maximum test coverage, real fixtures.** No mocking internal modules; real `.ts` fixtures through the real pipeline. +### P4 Chains — Ecosystem Hardening + +| Chain | User story | Required actions | +|---|---|---| +| CH-P4-01 watch mode | As US-CLI-HUMAN, I can keep analysis fresh while editing. | Start watch on fixture -> change file -> assert incremental re-analysis -> assert debounce, cache update, and clean shutdown. | +| CH-P4-02 monorepo scope | As US-CLI-CI, I can analyze only changed workspaces. | Multi-package fixture -> change one package -> run `--changed-workspaces` -> assert scoped graph, cross-package cycle detection, stable summary. | +| CH-P4-03 config migration | As US-CLI-HUMAN, I can migrate analyzer config safely. | Fixture with supported external config -> run migrate dry-run -> assert generated config, warnings, no source mutation, idempotency. | +| CH-P4-04 hooks install | As US-MAINTAINER, local hooks run the same gate as CI. | Temp git repo -> install hooks -> stage bad file -> assert hook blocks with same finding ID as `check` -> uninstall/cleanup. | +| CH-P4-05 local finding history | As US-MAINTAINER, I can see trend changes without cloud. | Run analysis twice with changed findings -> assert local history file, trend summary, gitignored storage, no sensitive source capture. | +| CH-P4-06 production mode | As US-CLI-CI, I can exclude test/dev files from production risk. | Fixture with prod + test-only files -> run `--production` -> assert test/dev exclusions, prod findings retained, docs state behavior. | + +**Chain closeout rule:** stable `2.5.0` cannot ship until every chain for shipped features is green in CI or explicitly marked deferred with reason, owner, and follow-up issue. --- -## 4. Differentiation track (the moat) +## P0 — 2.5.0 Stabilization -### 4.1 Highways — Data-Flow Convergence, Consolidation & Path Synthesis ⭐ FLAGSHIP +### Cache Directory Migration -**The problem.** As a codebase grows, every new feature that creates, transforms, or moves a piece of data tends to wear its own *cowpath* — an ad-hoc route through the call graph — instead of reusing an established route. Over time you get N divergent paths performing the same logical operation on the same data shape. The result: duplicated logic spread across paths, inconsistent validation, drift, and high blast radius when the shape changes. The fix a senior engineer applies by intuition: **pave a highway** — one canonical, reusable path every feature routes through. +Rename the legacy index/cache folder from `.code-visualizer/` to `.codebase-intelligence/` without breaking existing users. -**Highways detects this divergence automatically AND proposes the highway to build.** Detection alone is half the value; the system also *synthesizes the recommended unified path* — its name, location, signature, and a step-by-step reroute plan — so a human or agent can act immediately. +**Status:** Partially shipped in the P0 canary PR. -This is *not* token duplication (§5.2). Token-dupes find two functions that look alike. Highways finds **structurally divergent routes that accomplish the same data operation** — even when the code looks different — and recommends the canonical node to consolidate them. +**Shipped:** -``` -Today (cowpaths — divergent routes to one logical sink): +- Auto-migrate legacy-only `.code-visualizer/` to `.codebase-intelligence/` when safe. +- Prefer `.codebase-intelligence/` when both folders exist, including same-signature and different-signature states. +- Keep both folders excluded from scanning and cache fingerprints during the migration window. +- Write new cache files only to `.codebase-intelligence/`. +- 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. - handleSignup ─► buildUserPayload ─────────────────────┐ - ▼ - importUsers ─► mapCsvRow ─► normalizeUser ──────► POST /users - ▲ - adminCreate ─► (inline validate + shape) ────────────┘ +**Remaining:** - 3 features, 3 hand-rolled "create User" paths, 0 shared logic. +- Expose migration facts in JSON: `cacheDir`, `legacyCacheDir`, `migrated`, `gitignoreUpdated`, `warnings[]`. -Highways recommendation (synthesized + paved highway): +### Docs + Help Alignment - handleSignup ┐ - importUsers ┼─► createUser(input): User ─► POST /users - adminCreate ┘ ▲ canonical highway (PROPOSED by the system) +Make every user-facing instruction reflect the canonical cache name. - 1 path. Validation, shaping, side-effects defined once. Reused 3×. -``` +**Status:** Shipped in the P0 canary PR. -**What it computes** (built entirely on existing graph + planned dupes engine — no new external dependency): +- Replace canonical `.code-visualizer/` references with `.codebase-intelligence/`. +- Mention `.code-visualizer/` only as legacy migration input. +- Update `--index`, `--status`, and `--clean` help text. +- Update README, docs, `llms.txt`, and `llms-full.txt`. +- Keep competitor/product names out of runtime help and generated agent instructions. -1. **Operation classification.** Tag symbols by data-operation intent: verb class (`create|read|update|delete|fetch|map|transform|parse|validate|serialize|normalize`) inferred from names, plus *sink convergence* — which symbols ultimately reach the same terminal node (a DB module export, an API client call, a store mutation, a shared type constructor). -2. **Shape grouping.** Group operations by `(verb-class, data-shape)`. Data-shape comes from the type layer (§4.2 prerequisite): parameter and return types resolved via the checker. v1 can run name-verb + sink only; v2 adds type-shape for precision. -3. **Path enumeration.** Reuse process tracing: for each operation group, enumerate the distinct call chains from entry points (or nearest stable callers) to the shared sink. -4. **Convergence ratio.** For a group of paths, `convergence = sharedIntermediateNodes / totalNodesAcrossPaths`. Low ratio = the paths re-implement the operation independently (cowpaths). High ratio = already converged (good). -5. **Step similarity.** Overlay duplication fingerprints (§5.2): if the *intermediate steps* across paths are near-duplicates, that's a strong "these should be one function" signal even when the route shapes differ. -6. **Bypass detection.** If a canonical node already exists (a high-fan-in node most callers in the group use) but some callers reach the sink *without* passing through it, flag the **bypassers** — they're skipping the existing highway. +### Test Runner Release Gate -**What it proposes (Path Synthesis — the system designs the highway):** +Fix the repeated Vitest runner timeout so release gates are deterministic. -For every `cowpath-cluster`, the system emits a concrete proposal, in one of two modes: +**Status:** Shipped in the P0 canary PR. -- **Reroute to existing** — when a viable canonical node already exists in the group (highest fan-in, lowest complexity, broadest shape coverage), recommend it as the highway and list every route that should be rerouted through it. -- **Synthesize new highway** — when no node covers the whole group, *design a new one*: - - **Name** — derived from `(verb-class + shape)` → `createUser`, `normalizeOrder`, `fetchInvoice`. - - **Location** — the lowest-common-ancestor module of the participating files, or the shape's owning module (so it's reachable from all routes without new circular deps). - - **Signature** — synthesized from the union/intersection of the divergent routes' input/return types (type layer §4.2): `createUser(input: UserInput): User`. - - **Body skeleton** — the common ordered steps extracted across routes (validate → shape → side-effect), with per-route deltas flagged as parameters/options. - - **Reroute plan** — ordered, per-route edits: "in `handleSignup`, replace `buildUserPayload(...) → post(...)` with `createUser(...)`." Includes a circular-dependency safety check (proposed location must not introduce a cycle — validated against the existing graph) and a blast-radius estimate for the change. +- Reproduce `Timeout calling "onTaskUpdate"` after all tests pass. +- Isolate the issue to the Vitest fork worker pool used by the release gate. +- Switch `pnpm test` to `--pool threads --no-file-parallelism`. +- Keep `429` assertions passing and make `pnpm test` exit 0 locally. -**Findings emitted:** +### Targeted Framework Entry Fixes -| Finding | Meaning | Proposal | -|---|---|---| -| `cowpath-cluster` | K divergent paths for one `(operation, shape)` | Build/route highway X; reroute the K paths | -| `bypass-route` | Caller reaches sink without using the existing canonical node | Route through the canonical node | -| `reuse-gap` | Near-duplicate intermediate steps on different paths | Extract one shared step | -| `shape-drift` | Same logical shape constructed inconsistently across paths | Centralize the shape constructor/validator | -| `highway-proposal` | Synthesized canonical path (name + location + signature + reroute plan) | Create new unified highway | - -**Prioritization.** `consolidationValue = pathCount × stepSimilarity × shapeSimilarity × churn(involvedFiles)`. Many divergent, churny, near-identical paths = pave this highway first. The output is a *ranked work list*: "consolidate these 5 routes into one `createUser` highway — highest payoff in the repo." - -**Output shape (JSON, agent-actionable):** - -```jsonc -{ - "highways": [ - { - "id": "hw_create_user", - "operation": "create", - "shape": "User", - "sink": "src/api/client.ts::post", - "convergenceRatio": 0.12, - "consolidationValue": 0.87, - "routes": [ - { "entry": "src/auth/signup.ts::handleSignup", "chain": ["handleSignup","buildUserPayload","post"] }, - { "entry": "src/import/csv.ts::importUsers", "chain": ["importUsers","mapCsvRow","normalizeUser","post"] }, - { "entry": "src/admin/users.ts::adminCreate", "chain": ["adminCreate","post"] } - ], - "proposal": { - "mode": "synthesize-new", - "name": "createUser", - "location": "src/user/create-user.ts", - "signature": "createUser(input: UserInput): User", - "commonSteps": ["validate", "shape", "post"], - "introducesCycle": false, - "estimatedBlastRadius": 3, - "reroute": [ - { "site": "src/auth/signup.ts::handleSignup", "replace": "buildUserPayload→post", "with": "createUser" }, - { "site": "src/import/csv.ts::importUsers", "replace": "normalizeUser→post", "with": "createUser" }, - { "site": "src/admin/users.ts::adminCreate", "replace": "inline→post", "with": "createUser" } - ] - }, - "evidence": { "stepSimilarity": 0.71, "churn": 23, "duplicatedTokens": 184 }, - "actions": [ - { "kind": "extract-canonical", "auto_fixable": false, "reroute": ["handleSignup","importUsers","adminCreate"] } - ] - } - ] -} -``` +Package public entrypoints already have coverage. Only fix remaining framework/config false positives with proof. -**Surfaces:** -- CLI: `codebase-intelligence highways ` with `--operation `, `--shape `, `--min-routes `, `--propose` (emit synthesized highways, default on), `--trace ` (deep-dive one opportunity), `--json`. -- MCP: `analyze_highways` — "where should this codebase consolidate data paths, and what canonical path should I build?" Perfect agent question before a refactor; the agent gets both the diagnosis and the proposed highway. +**To do:** -**Why only we can do this.** It composes our call graph + type layer + duplication fingerprints + churn + process tracing into one analysis. A token-only or dead-code-only tool has none of these as a connected graph. This is the single most defensible feature on the roadmap. +- Add minimal fixtures only for verified remaining false positives. +- Prefer config-driven entrypoint declarations over hardcoded broad framework plugins. +- Do not ship a universal framework plugin system in `2.5.0`. -**Phasing:** -- **H1** — name-verb classification + sink convergence + path enumeration + cowpath/bypass findings + reroute-to-existing proposals (no type layer required). -- **H2** — add type-shape grouping (depends §4.2), shape-drift findings, step-similarity from dupes engine, and **synthesize-new** proposals (name + location + signature + skeleton + cycle-safe reroute plan). -- **H3** — `hotspots --metric reuse` (files ranked by divergent-path participation); highway opportunities cross-linked into `forces` output. +--- -### 4.2 Type/Shape layer (prerequisite + standalone value) +## P1 — 2.5.0 Analysis Foundations -Extend the parser to capture, per symbol, resolved parameter types and return type (checker already available in `parseFile`). Stored compactly on `ParsedFile`. Unlocks: -- Highways shape grouping and signature synthesis (§4.1 H2). -- Type-aware dead code (unused types, params). -- Future: "which functions produce/consume shape X" queries via MCP. +### Operation Registry -### 4.3 Architecture intelligence depth (extend, don't restart) +Collapse CLI + MCP operation duplication into one descriptor registry before adding more analyzers. -We already have tension, bridges, seams, communities. Push further into *prescriptive* output: ranked extraction/consolidation recommendations with effort estimates, layering inference, and "this module is doing two jobs — split here" seam proposals tied to real fan-in/fan-out evidence. +**To do:** ---- +- Add `Operation` descriptors per analysis operation. +- Use one input schema per operation for CLI coercion and MCP schemas. +- Return discriminated result/error unions instead of process exits in shared code. +- Use one graph-load pipeline with progress callbacks. +- Type hint keys against the operation-name union. +- Move text/SARIF/markdown formatting over result objects into formatters. +- Target tests at `op.run(graph, input)` instead of duplicating CLI and MCP behavior tests for every operation. -## 5. Parity track (table stakes) +### Type/Shape Layer -### 5.1 Framework & entry-point awareness (P0 — fixes real false positives) +Capture resolved parameter and return types per symbol. -Today, framework-consumed exports and path-aliased imports produce false dead-export reports. Ship lightweight awareness for the high-value few — **not** a universal plugin zoo: +**To do:** -- Next.js (route exports, `generateMetadata`, `generateStaticParams`, server actions), React Router/Remix loaders/actions, Convex functions, Vite/Vitest config & test conventions, package.json `exports`/`bin`/`scripts` entry inference. -- Resolve `tsconfig` path aliases and config aliases as real entry points. +- Store compact type signatures on parsed symbols. +- Support "which functions produce/consume shape X" queries. +- Unlock type-aware dead code and Highways H2 shape grouping. +- Use type facts for synthesized highway signatures. -This is a **correctness fix**, prioritized first. +### Duplication Detection -### 5.2 Duplication detection (P0) +Add deterministic clone detection on the existing parser path. -Token/structure clone detection on the existing AST path (TS scanner; no LSP — batch problem, full Program access already in hand): +**To do:** -- Tiers/modes: `strict` (exact), `mild`/`weak` (renamed-identifier + near-miss via k-gram shingling + bucketing). Clone *families* (groups), `--min-tokens` noise floor, `--skip-local` (cross-directory only), `--trace ` deep-dive. -- A `semantic` mode (same behavior, different structure) is a later addition once the type/shape layer (§4.2) lands — it matches on shape signatures, **not** embeddings, to stay deterministic. (Open — §8.) -- Feeds the Highways step-similarity signal (§4.1). +- Implement `strict`, `mild`, and `weak` clone modes. +- Emit clone families, not isolated pair findings. +- Support `--min-tokens`, `--skip-local`, `--trace `, and `--json`. +- Feed step-similarity signals into Highways. +- Defer semantic/shape-based duplication until the Type/Shape layer exists. -### 5.3 Dead code beyond exports (P1) +### Dead Code Beyond Exports -Extend dead-exports to: unused files, unused types, unused enum/class members, and dependency hygiene (unused / unlisted / type-only / test-only `package.json` deps). Reuses the existing import graph. +Extend deletion intelligence beyond exported symbols. -### 5.4 Health score, maintainability & CRAP (P1) +**To do:** -Single composite **0–100 + letter grade**, computed from existing + new metrics (complexity, duplication, dead code, circular deps, tension). CI-gateable: `health --score --min-score `. One number agents and pipelines can fail on. +- Detect unused files. +- Detect unused types. +- Detect unused enum/class members. +- Detect unused / unlisted / type-only / test-only dependencies. +- Keep findings framework-aware to avoid false positives. -- **Per-file maintainability index** (`--file-scores`) — Halstead/complexity-based 0–100 per file. -- **CRAP score** — change-risk anti-pattern = cyclomatic × (1 − coverage)². Reads static test reachability, or an Istanbul `coverage.json` when provided (`--coverage `). Deterministic, no runtime tracing. -- **Refactor hotspots** — composite complexity × churn × coupling × size ranking (extends existing `hotspots`). +### Suppression Hygiene -### 5.5 Declarative architecture boundaries (P1) +Finish suppression management without hiding stale debt. -Layer the existing module/cluster graph with *rules*: +**Already exists:** `ci-ignore-next-line` and `ci-ignore-file`. -- Presets (bulletproof, layered, hexagonal, feature-sliced) + custom `zones` (with `autoDiscover`) and `from → allow/forbid` rules. `list --boundaries` prints the expanded rule set. -- Findings: boundary violations, forbidden cross-edges, re-export chains. We already compute the graph — this adds rule evaluation on top. +**To do:** -### 5.6 Audit gate (PR risk) (P1) +- Add JSDoc `@public`, `@internal`, and `@expected-unused` semantics. +- Add stale-suppression detection. +- Report suppressions in JSON and CI summaries. -Extend `changes` into a gate: `--base `, `--gate new-only|all`, baseline files, `--fail-on-regression`, `--tolerance `, and **new-vs-pre-existing attribution** so PRs only fail on what they introduced. +--- -### 5.7 Output formats & actionability (P1) +## P2 — 2.5.0 Product Parity + Flagship Intelligence -- Add `--format` outputs: SARIF (GitHub Code Scanning), CodeClimate (GitLab Code Quality), PR-comment (GitHub/GitLab), inline review envelopes (GitHub/GitLab), CI annotations, health badge, markdown, compact — over the existing result objects. -- Ship a vendored **GitLab CI template** alongside the GitHub Action. -- `--diff-file ` / `--changed-since ` for line-level filtering of findings to changed code. -- Add a typed `actions[]` array with stable finding IDs to every finding's JSON. Actions are **advisory hints** — they describe what a fix would change so an agent or human can apply it; the tool itself never writes (§3, §7). This makes findings agent-actionable without making the tool mutating. +### Highways -### 5.8 Suppressions (P1) +Detect repeated data routes that should converge on one canonical path. -`// ci-ignore-next-line `, `// ci-ignore-file`, JSDoc `@public`/`@internal`/`@expected-unused`, and **stale-suppression detection** (flag suppressions that no longer match a finding). +**Mental model:** -### 5.9 Cognitive complexity (P2) +``` +synapse = one edge +highway = reusable multi-step route -Add cognitive complexity alongside cyclomatic in the parser. +entry ─► transform ─► validate ─► sink + ▲ canonical node agents should reuse +``` -### 5.10 Cohorting & ownership (P2) +**Vocabulary:** -`--group-by owner|package|directory`, bus-factor / ownership via git blame + CODEOWNERS, refactor targets with `--effort` filtering, static coverage-gap detection (we already match test files to sources). +| Term | Meaning | +|---|---| +| `route` | Ordered call chain from an entry point toward a sink | +| `sink` | Terminal side effect or canonical boundary: DB write, API call, queue publish, store mutation, file write | +| `shape` | Type/DTO/domain object moving through the route | +| `canonical node` | Shared function/module most routes should pass through for one operation | +| `cowpath` | Ad-hoc route that recreates a canonical operation outside the shared path | +| `bypass` | Route that reaches a sink while skipping an existing canonical node | +| `highway` | Approved canonical route for one `(operation, shape, sink)` inside a bounded scope | -### 5.11 Ecosystem & adoption (P2–P3) +**To do:** -Watch mode, monorepo workspace scoping (cross-package circular deps, `--changed-workspaces`), config migration from common existing tools (`migrate`), `explain ` docs, opt-in secret-leak scan. +- H1: classify operation verbs, enumerate entry-to-sink routes, detect cowpaths and bypasses. +- H1: propose reroutes to existing canonical nodes. +- H2: add type-shape grouping once Type/Shape layer lands. +- H2: detect shape drift and near-duplicate intermediate steps. +- H2: synthesize new highway proposals: name, location, signature, skeleton, cycle-safe reroute plan. +- H3: add reuse hotspot metrics and cross-link opportunities into `forces` / `hotspots`. +- MCP: add `analyze_highways`. +- CLI: add `highways ` with `--operation`, `--shape`, `--min-routes`, `--propose`, `--trace`, `--json`. +- Every opportunity should emit a token-budgeted context pack: summary, affected routes, evidence, blast radius, proposed canonical node, next safe command. -- **`init` config generator** — scaffold `codebase-intelligence.json` with detected entry points and sensible rule defaults. -- **`hooks install`** — pre-commit / agent gate that runs `check` on staged files. -- **`impact`** — local, gitignored history of which findings surfaced and gated, for trend reporting. -- **`--production`** — exclude test/dev files from any analysis. +### Scope Graph + Codebase Map -### 5.12 LSP server — live editor diagnostics (P2) +Represent the codebase as a compound graph agents can query, not just a folder tree or screenshot. -A Language Server Protocol server so findings appear **live in the editor as you type**, in any LSP-capable editor — without shipping a bespoke per-editor extension. +``` +codebase graph + ├─ files + ├─ symbols + ├─ types + ├─ scopes + ├─ routes + ├─ sinks + ├─ tests + └─ owners +``` -- Surfaces rule findings (dead exports, complexity, boundary violations, comments, …) as diagnostics. -- **Hover** shows graph facts on a symbol: blast radius, fan-in/out, PageRank, dead/clone status. -- **Code actions** are navigational/advisory only (go to dependents, trace a highway, insert a suppression) — never mutating, consistent with the read-only principle (§3). -- Inline reference/usage counts. +**Organization concepts to integrate:** -This is the editor surface. It does **not** change how batch analysis runs — duplication and Highways still use the Compiler API directly (§7). The LSP is a thin presentation layer over the same engine. +| Concept | Finding/query it unlocks | +|---|---| +| Bounded contexts | "Who owns this shape, and which files bypass that context?" | +| Canonical data model | `shape-drift`, unsafe DTO leakage | +| Anti-corruption layer | External type crossing an internal boundary | +| Ports & adapters | Route reaches side effect without gateway | +| Dependency direction | Boundary violation, layering inversion | +| Public vs internal API | Forbidden deep import, bypass route | +| Scope cohesion | Junk-drawer module, extraction candidate | +| Escape velocity | File/module pulled by too many scopes | +| Ownership/bus factor | Orphaned critical path | +| Test proximity | Important route has no proof nearby | +| Runtime surface | User-facing entrypoint route maps | -### 5.13 Operation registry — one engine, N surfaces (P1 — enabler for §5.7 + §5.12) +**To do:** -From the 2026-06-10 architecture sweep (self-analysis, graph-backed): the 15 analysis operations are exposed twice — CLI command + MCP tool — and each operation's lifecycle (input validation, analyzer call, error routing, next-step hints, serialization) is smeared across `cli.ts` (1205 LOC, fan-in 0 / fan-out 13, coupling 0.93 — the top hotspot), `mcp/index.ts` (446 LOC, cohesion 0.17 — JUNK_DRAWER verdict), and `src/core`. Concretely: validation exists twice (`parseInt`+`isNaN` guards in CLI, zod in MCP), error routing twice (`process.exit` vs `{ isError: true }`), hints are MCP-only with untyped string keys (a typo silently returns `[]`), and the parse→build→analyze→cache pipeline is duplicated verbatim inside `cli.ts` (`loadGraph` vs `runMcpMode`). Every new command (latest: `check`) re-smears the pattern. +- CLI: add `map ` with `--focus`, `--scope`, `--depth`, `--format json|dot|graphml|markdown`, `--context-budget`, `--json`. +- MCP: add `get_codebase_map`, `get_scope_graph`, `get_context_pack`. +- Emit `overview`, `focus`, `route`, `contextPack`, and `evidence` shapes. +- Add optional 2D/3D graph export/viewer only after structured graph outputs are useful. -The fix is the shape `src/rules/` already paved in #42 — a registry of deep modules ("add a rule = add a file + an entry"). Apply it to operations: +### Content Drift -- **`Operation` descriptor per analysis op** — one zod input schema (feeds both CLI option coercion and the MCP tool schema), compute fn returning a discriminated result/error union, hints, and a text formatter. The registry is the single seam; CLI and MCP collapse into two thin adapters over it. -- **One graph-load pipeline** — single `loadGraph` returning a result (no internal `process.exit`), progress-callback seam so CLI logs and MCP stays silent. -- **Typed hint keys** — hint key typed against the operation-name union; misspelling becomes a compile error. -- **Fold core's presentational reshapers into op formatters** — `computeForces` (6 of 9 fields are verbatim pass-throughs), `computeGroups`/`computeProcesses`/`computeClusters` (filter+rename wrappers) stop being a middle layer. +Detect mismatch between file/folder labels and actual behavior. -Payoff beyond hygiene: §5.7 output formats become *formatters over the same registry* — SARIF/CodeClimate/markdown written once cover all operations — and the §5.12 LSP server becomes a third thin adapter instead of a third hand-rolled surface. Tests target `op.run(graph, input)` directly instead of driving the CLI process or MCP server end-to-end. +``` +declared intent = path + filename + exports + docs +actual behavior = imports + calls + types + side effects + tests + churn +drift score = mismatch(declared intent, actual behavior) +``` ---- +**To do:** + +- Emit `name-drift`, `scope-drift`, `mixed-responsibility`, `hidden-side-effect`, `shape-drift`, `orphan-scope`, and `misplaced-test`. +- CLI: add `drift ` with `--focus`, `--scope`, `--min-score`, `--json`. +- MCP: add `detect_content_drift`. +- Make first run report-only; allow CI gating only after a baseline exists. +- Keep implementation deterministic: tokenized names, resolved symbols/types, imports/calls, side-effect sinks, tests, churn. + +### Health Score + Maintainability + +Provide one CI-gateable quality score plus file-level risk metrics. + +**To do:** + +- Add composite `health --score --min-score `. +- Add per-file maintainability index. +- Add CRAP score using static test reachability and optional Istanbul `coverage.json`. +- Extend `hotspots` with complexity x churn x coupling x size. + +### Architecture Boundaries + +Evaluate repo dependency boundaries using the existing graph. -## 6. Sequencing +**To do:** +- Add presets: bulletproof, layered, hexagonal, feature-sliced. +- Add custom `zones` with `autoDiscover`. +- Add `from -> allow/forbid` rules. +- Add `list --boundaries`. +- Emit boundary violations, forbidden cross-edges, and risky re-export chains. + +### CI PR Quality Gate + +Make PR enforcement easy and local-first. + +**Already exists:** `check`, config-level `ci`, `new-only` file gating, text/json/SARIF output. + +**To do:** + +```bash +codebase-intelligence ci . +codebase-intelligence ci . --base origin/main --new-only +codebase-intelligence ci . --format sarif --output codebase-intelligence.sarif +codebase-intelligence ci . --comment markdown --summary +codebase-intelligence ci . --baseline .codebase-intelligence/baseline.json ``` -P0 (correctness + flagship foundations) - ├─ Framework/entry-point awareness §5.1 (fixes FP bugs) - ├─ Duplication detection §5.2 - └─ Type/Shape layer §4.2 (Highways prereq) - -P1 (parity bar + flagship H1/H2) - ├─ Highways H1 (cowpaths/bypass/reroute) §4.1 - ├─ Dead code beyond exports §5.3 - ├─ Health + maintainability + CRAP §5.4 - ├─ Architecture boundaries (+ presets) §5.5 - ├─ Audit gate (+ diff-file) §5.6 - ├─ Operation registry (engine seam) §5.13 (before 5.7 — formats ride on it) - ├─ Output formats + actions[] (advisory) §5.7 - ├─ Suppressions §5.8 - └─ Highways H2 (type-shape, drift, synth) §4.1 - -P2 (depth + ergonomics) - ├─ Highways H3 (reuse hotspot metric) §4.1 - ├─ Cognitive complexity §5.9 - ├─ Ownership / cohorting / targets §5.10 - ├─ LSP live diagnostics §5.12 - └─ Architecture intelligence depth §4.3 - -P3 (ecosystem) - └─ Watch, monorepo scope, migrate, explain, - secrets, init, hooks, impact §5.11 + +- Add first-class `ci` wrapper around `check`, `changes`, and selected analyzers behind one PR-friendly contract. +- Default to new findings only on PRs. +- Support `--fail-on error|warn|score`, `--min-score `, `--max-new `, and `--baseline `. +- Keep SARIF output and add GitHub annotations, PR markdown, JSON, and compact terminal summaries. +- Stable exit codes: `0 pass`, `1 gate failed`, `2 invalid config/runtime`, `3 analyzer error`. +- Vendor minimal GitHub/GitLab CI templates. +- Add `doctor --profile ci` checks for workflow wiring, token permissions, baseline path, and SARIF upload setup. + +### Doctor + Agent Onboarding + +Add a read-only setup auditor for humans, CI, MCP, and coding agents. + +**To do:** + +```bash +codebase-intelligence doctor +codebase-intelligence doctor --agent codex +codebase-intelligence doctor --agent claude +codebase-intelligence doctor --profile ci +codebase-intelligence doctor --json ``` -**Rule of sequencing:** correctness before breadth (P0 fixes existing false positives), the flagship rides on P0 foundations, parity fills out P1, depth and ecosystem follow. +- Check runtime, package manager, CLI version, project roots, config schema, graph build, cache writability, CLI help, MCP server, CI workflow, docs freshness, and agent instructions. +- Emit JSON with `status`, `checks[]`, `fix`, and `docs`. +- Generate/update Codex, Claude Code, Cursor, MCP, and CI instructions without forking analyzer logic. +- Keep doctor read-only: exact commands, no automatic mutation. + +### Output Formats + Actionability + +Make findings portable across CI, review, and agents. + +**Already exists:** text/json/SARIF for `check`. + +**To do:** + +- Add missing formatters: CodeClimate, PR-comment, inline review envelopes, CI annotations, health badge, markdown, compact. +- Add line-level filtering via `--diff-file ` / `--changed-since `. +- Add typed `actions[]` to every finding. +- Keep `actions[]` advisory only. --- -## 7. Explicitly out of scope +## P3 — 2.5.0 Depth + Ergonomics + +### Cognitive Complexity + +- Add cognitive complexity alongside cyclomatic complexity. +- Expose it in `hotspots`, `file`, `health`, and CI outputs. + +### Cohorting + Ownership -**Deliberate non-features** (we could, we won't): +- Add `--group-by owner|package|directory`. +- Use CODEOWNERS + git blame for ownership and bus-factor signals. +- Add static coverage-gap detection. +- Add refactor target filtering with `--effort`. -- **Auto-fix / source mutation.** The tool is read-only (§3). It reports and advises; it never rewrites code. `actions[]` are hints for an agent/human to apply, not edits the tool performs. -- **First-party VS Code extension.** The LSP server (§5.12) delivers editor diagnostics to *any* LSP-capable editor; we don't maintain a bespoke per-editor extension. -- **Production-runtime layer** — live-traffic hot/cold path tracing, V8/Istanbul runtime ingestion, cloud sync, deletion-from-traffic evidence. Different product, different infra, different trust model. -- **Universal framework plugin catalog** — we do the high-value few (§5.1) and let config cover the rest, not a 100+ plugin treadmill. -- **LSP as an analysis engine** — duplication and Highways are whole-program *batch* problems run on the Compiler API. The LSP server (§5.12) is a presentation layer only; it does not run batch analysis per keystroke. +### LSP Diagnostics -**Parity gaps consciously deferred** (free in competing tools; not yet committed — see §8): +- Ship an LSP server over the same operation registry. +- Surface diagnostics for dead code, complexity, boundaries, comments, and future Highways findings. +- Add hover facts: blast radius, fan-in/out, PageRank, dead/clone status. +- Keep code actions navigational/advisory only. -- **CSS / utility-class unused analysis** (Tailwind/PostCSS/UnoCSS). Niche; off our graph/architecture moat. -- **Template-aware dead code** (Vue/Svelte/Angular templates). Requires parsing beyond `.ts/.tsx` — a real engine expansion. Until done, dead-code accuracy on those stacks is lower. -- **Node.js programmatic bindings** (library API). We ship CLI + MCP; a stable embeddable API is a separate commitment. +### Architecture Intelligence Depth + +- Add ranked extraction/consolidation recommendations. +- Add effort estimates. +- Add layering inference. +- Add seam proposals tied to fan-in/fan-out evidence. --- -## 8. Open decisions +## P4 — 2.5.0 Ecosystem Hardening + +**To do:** + +- Watch mode. +- Monorepo workspace scoping. +- Cross-package circular dependency checks. +- `--changed-workspaces`. +- Config migration from common analyzer configs. +- `explain `. +- Opt-in secret-leak scan. +- `hooks install`. +- Local finding history for trends. +- `--production` mode that excludes test/dev files. + +--- -**Resolved:** read-only — no auto-fix (§3, §7) · LSP server yes (§5.12) · first-party VS Code extension no (§7). +## Market-Derived Gap List -Still open: +Competitor names stay in docs only. This section exists to preserve comparison context without leaking names into runtime surfaces. + +| Tool | Strong surface | Gap/opportunity for us | +|---|---|---| +| Knip | Unused files, dependencies, exports, monorepo/workspace awareness | Match unused/dependency parity, then beat it with graph evidence, context packs, and route/scope ownership. | +| dependency-cruiser | Dependency rule validation, circular dependency checks, visualization, baselines | Match declarative boundaries and baselines; beat it with symbol/type graph, Highways, and prescriptive reroute proposals. | +| Madge | Quick dependency graphs and circular dependency discovery | Make graph export/view UX easy; keep graph richer than file imports. | +| jscpd | Copy/paste and duplicate-code thresholds | Match strict/near clone detection; beat it with semantic shape/route duplication via Highways. | +| Biome | `ci` command ergonomics and fast terminal UX | Add first-class `ci`; keep help/examples predictable for agents. | +| SonarQube/SonarCloud | Quality gates, PR analysis, new-code policy, hosted dashboards | Add local-first gates, baseline/new-only PR behavior, SARIF/annotations, no hosted dependency. | +| ESLint boundaries plugin | Import boundary enforcement in lint pipelines | Offer repo-level graph rules beyond imports: symbols, types, scopes, routes, sinks. | +| Expo/React Native/npm doctor | Setup diagnostics | Make `doctor` product-grade, read-only, JSON-capable, and agent-aware. | +| publint / Are The Types Wrong | Package public-surface validation | Add package-entrypoint/public API health checks for libraries. | + +--- -1. **CSS unused analysis** — commit to parity, or leave deferred (§7)? -2. **Template-aware dead code** (Vue/Svelte/Angular) — commit (engine expansion beyond `.ts/.tsx`), or leave deferred? Highest-cost parity gap. -3. **Node.js programmatic bindings** — expose the core as an embeddable library, or stay CLI + MCP only? -4. **Semantic duplication mode** — ship the 4th (shape-based, deterministic) mode in H2, or keep to exact/renamed/near-miss? -5. **Highways naming** — `highways` / `analyze_highways`, or `convergence` / `consolidation`? (Leaning `highways`.) -6. **Highway synthesis depth** — proposal metadata only (name + location + signature + reroute plan), or also emit a code skeleton an agent can apply? (Tool stays read-only either way.) -7. **`schema.json` source of truth** — generate from zod (`zod-to-json-schema`) or hand-maintain with a drift test? -8. **Config file name** — `codebase-intelligence.json` primary, or a shorter brand? -9. **v1 framework awareness scope** — the 5 listed (§5.1), or a config-driven mini-plugin contract from day one? +## Open Decisions + +1. Commit to CSS / utility-class unused analysis, or leave deferred? +2. Commit to template-aware dead code for Vue/Svelte/Angular, or leave deferred? +3. Expose a stable Node.js programmatic API, or stay CLI + MCP? +4. Ship deterministic semantic duplication in H2, or keep exact/renamed/near-miss only? +5. Keep `highways` naming, or use `convergence` / `consolidation`? +6. Should highway synthesis emit proposal metadata only, or also a code skeleton? +7. Generate `schema.json` from zod or hand-maintain it with a drift test? +8. Keep `codebase-intelligence.json`, or choose a shorter config name? +9. Start `map` with JSON only, or include DOT/GraphML/Obsidian-compatible export immediately? +10. Build a first-party local 2D/3D viewer, or export only? +11. Rank context packs by graph metrics first, or by task intent? +12. Name drift command `drift`, `content-drift`, or fold into `check --rule naming/*`? +13. Use report-only first run for drift, then gate only new severe drift? +14. Which doctor profiles ship first: `local`, `ci`, `agent`, `mcp`? +15. Generate agent docs only, or publish first-party Codex/Claude Code skills/plugins from this repo? +16. Keep `.code-visualizer/` ignored forever, or remove it from generated `.gitignore` after one stable release? +17. Name gitignore support `init --gitignore`, a separate command, or something else? +18. Ship `ci` as first-class command, or `check --ci` wrapper? +19. Generate PR markdown only, or integrate with GitHub/GitLab APIs directly? +20. Store baselines in `.codebase-intelligence/baseline.json`, config path, or external artifact? +21. Use one global quality score first, or per-scope scores plus global rollup? --- -## 9. Success criteria +## Success Criteria -- **Parity:** on a representative repo, our dead-code, duplication, circular-dep, and boundary findings are a superset-or-equal of what a developer would expect from a mainstream analyzer, with fewer false positives on framework code. -- **Differentiation:** Highways surfaces at least one *real, accepted* consolidation opportunity on a mature repo that no token/dead-code tool reports — **and proposes a canonical path the team actually builds.** -- **Agent-native:** every finding has a stable ID + `actions[]`; an agent can run one MCP call and get a ranked, machine-actionable work list including the proposed highway. -- **Determinism:** identical inputs → identical outputs, every finding traceable to graph evidence. +- **Parity:** dead-code, duplication, circular-dependency, boundary, and PR-gate behavior meets expected TS/JS analyzer baseline with fewer framework false positives. +- **Highways:** the tool finds at least one accepted consolidation opportunity on a mature repo and proposes a canonical path a team can build. +- **Map:** an agent can answer "what owns this shape?", "what routes reach this sink?", and "what files do I need for this task?" from structured graph output. +- **Drift:** the tool flags real file/folder/content mismatch with evidence from names, imports, calls, types, side effects, tests, and churn. +- **Doctor:** a fresh repo gets a complete read-only setup report with exact fix commands for config, graph build, MCP, CI, and agent instructions. +- **Migration:** `.code-visualizer/` migrates to `.codebase-intelligence/` without data loss; new repos can add `.codebase-intelligence/` to `.gitignore`. +- **CI:** maintainers can add one generated workflow and fail PRs only on new severe findings, with SARIF/annotations/summary and stable exit codes. +- **Determinism:** identical inputs produce identical outputs; every finding traces to graph evidence. diff --git a/src/cli.ts b/src/cli.ts index ed009fe..e87c10e 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -23,7 +23,8 @@ import { analyzeGraph } from "./analyzer/index.js"; import { startMcpServer } from "./mcp/index.js"; import { setIndexedHead, setRoot } from "./server/graph-store.js"; import { exportGraph, importGraph } from "./persistence/index.js"; -import { getCacheKey, INDEX_DIR_NAME } from "./persistence/cache-key.js"; +import { getCacheKey } from "./persistence/cache-key.js"; +import { cleanIndexDirectories, prepareIndexDirectory, type IndexDirectoryResolution } from "./persistence/index-dir.js"; import { computeOverview, computeFileContext, @@ -49,6 +50,7 @@ import { import { installRepoFiles, installGlobalSkill, + installGitignoreEntry, resolveInitPlan, ALL_AGENT_IDS, } from "./install/index.js"; @@ -60,8 +62,13 @@ import type { CodebaseGraph, OutputFormat } from "./types/index.js"; // ── Helpers ───────────────────────────────────────────────── -function getIndexDir(targetPath: string): string { - return path.join(path.resolve(targetPath), INDEX_DIR_NAME); +function reportIndexMigration(resolution: IndexDirectoryResolution): void { + if (resolution.migration === "migrated-legacy") { + progress(`Migrated legacy index ${resolution.legacyDir} to ${resolution.canonicalDir}`); + } + if (resolution.migration === "ignored-legacy") { + progress(`Using ${resolution.canonicalDir}; legacy index remains at ${resolution.legacyDir}`); + } } function getHeadHash(targetPath: string): string { @@ -98,7 +105,9 @@ function loadGraph(targetPath: string, force = false): { graph: CodebaseGraph; h } setRoot(resolved); - const indexDir = getIndexDir(targetPath); + const indexResolution = prepareIndexDirectory(targetPath); + reportIndexMigration(indexResolution); + const indexDir = indexResolution.activeDir; const headHash = getHeadHash(targetPath); const cacheKey = getCacheKey(targetPath, { headHash, cliVersion: pkg.version }); @@ -204,6 +213,7 @@ interface InitOptions { agents?: string; all?: boolean; skill?: boolean; + gitignore?: boolean; yes?: boolean; json?: boolean; } @@ -1018,6 +1028,7 @@ program .option("--agents ", `Comma-separated agents, non-interactive. Available: ${ALL_AGENT_IDS.join(", ")}`) .option("--all", "Target every agent (non-interactive)") .option("--skill", "Also install the global Claude skill (opt-in)") + .option("--gitignore", "Add .codebase-intelligence/ to .gitignore") .option("-y, --yes", "Accept defaults without prompting") .option("--json", "Output as JSON (implies non-interactive)") .action(async (targetPath: string, options: InitOptions) => { @@ -1050,16 +1061,19 @@ program installSkill = selection.skill; } - if (agents.length === 0 && !installSkill) { + const installGitignore = plan.installGitignore; + + if (agents.length === 0 && !installSkill && !installGitignore) { output("Nothing selected — nothing to do."); return; } const repoResults = installRepoFiles(resolved, { agents }); + const gitignoreResult = installGitignore ? installGitignoreEntry(resolved) : undefined; const skillResult = installSkill ? installGlobalSkill() : undefined; if (options.json) { - outputJson({ repoFiles: repoResults, skill: skillResult ?? null }); + outputJson({ repoFiles: repoResults, gitignore: gitignoreResult ?? null, skill: skillResult ?? null }); return; } @@ -1076,6 +1090,11 @@ program output(`Global skill:`); output(` ${skillResult.action.padEnd(9)} ${skillResult.path}`); } + if (gitignoreResult) { + output(``); + output(`Git ignore:`); + output(` ${gitignoreResult.action.padEnd(9)} ${gitignoreResult.path}`); + } output(``); output(`Done. Selected agents will be told to query codebase-intelligence first.`); output(`Re-run anytime — writes are idempotent (managed blocks only).`); @@ -1187,19 +1206,22 @@ program }); async function runMcpMode(targetPath: string, options: McpOptions): Promise { - const indexDir = getIndexDir(targetPath); setRoot(path.resolve(targetPath)); if (options.clean) { - if (fs.existsSync(indexDir)) { - fs.rmSync(indexDir, { recursive: true, force: true }); - progress(`Removed index at ${indexDir}`); - } else { + const removed = cleanIndexDirectories(targetPath); + if (removed.length === 0) { progress("No index found."); + } else { + for (const dir of removed) progress(`Removed index at ${dir}`); } return; } + const indexResolution = prepareIndexDirectory(targetPath); + reportIndexMigration(indexResolution); + const indexDir = indexResolution.activeDir; + if (options.status) { const result = importGraph(indexDir); if (!result) { @@ -1267,10 +1289,10 @@ async function runMcpMode(targetPath: string, options: McpOptions): Promise { if (!targetPath) { program.help(); diff --git a/src/install/index.test.ts b/src/install/index.test.ts index c5a234c..2cac0e6 100644 --- a/src/install/index.test.ts +++ b/src/install/index.test.ts @@ -9,6 +9,7 @@ import { renderBlock, renderSkill, installRepoFiles, + installGitignoreEntry, installGlobalSkill, isAgentId, resolveInitPlan, @@ -169,6 +170,24 @@ describe("installRepoFiles", () => { expect(after).toEqual(before); }); + it("adds the canonical cache directory to .gitignore idempotently", () => { + const first = installGitignoreEntry(tmp); + const second = installGitignoreEntry(tmp); + + expect(first).toEqual({ path: ".gitignore", action: "created" }); + expect(second).toEqual({ path: ".gitignore", action: "unchanged" }); + expect(fs.readFileSync(path.join(tmp, ".gitignore"), "utf-8")).toBe(".codebase-intelligence/\n"); + }); + + it("preserves existing .gitignore content when adding the cache directory", () => { + fs.writeFileSync(path.join(tmp, ".gitignore"), "dist/\n", "utf-8"); + + const result = installGitignoreEntry(tmp); + + expect(result).toEqual({ path: ".gitignore", action: "updated" }); + expect(fs.readFileSync(path.join(tmp, ".gitignore"), "utf-8")).toBe("dist/\n.codebase-intelligence/\n"); + }); + it("covers all registry ids in ALL_AGENT_IDS", () => { expect([...ALL_AGENT_IDS].sort()).toEqual(AGENT_TARGETS.map((t) => t.id).sort()); }); @@ -207,6 +226,7 @@ describe("resolveInitPlan", () => { expect(plan.mode).toBe("explicit"); expect(plan.agents).toEqual([...ALL_AGENT_IDS]); expect(plan.installSkill).toBe(false); + expect(plan.installGitignore).toBe(false); }); it("--agents picks the listed agents", () => { @@ -214,6 +234,12 @@ describe("resolveInitPlan", () => { expect(plan.mode).toBe("explicit"); expect(plan.agents).toEqual(["claude", "gemini"]); expect(plan.invalidAgents).toEqual([]); + expect(plan.installGitignore).toBe(false); + }); + + it("--gitignore requests .gitignore installation", () => { + const plan = resolveInitPlan({ gitignore: true }, false); + expect(plan.installGitignore).toBe(true); }); it("--agents reports unknown ids and keeps valid ones", () => { diff --git a/src/install/index.ts b/src/install/index.ts index f4c8623..000078e 100644 --- a/src/install/index.ts +++ b/src/install/index.ts @@ -127,6 +127,8 @@ export interface InitFlags { all?: boolean; /** `--skill`: install the global skill (opt-in). */ skill?: boolean; + /** `--gitignore`: add the cache directory to .gitignore. */ + gitignore?: boolean; /** `--json`: machine output, implies non-interactive. */ json?: boolean; /** `--yes`: accept defaults without prompting. */ @@ -140,6 +142,8 @@ export interface InitPlan { agents: AgentId[]; /** Whether to install the global skill (default when mode is "interactive"). */ installSkill: boolean; + /** Whether to add the canonical cache directory to .gitignore. */ + installGitignore: boolean; /** How the selection was decided. "interactive" → caller should prompt. */ mode: InitMode; /** Unknown ids passed to `--agents`. */ @@ -154,9 +158,10 @@ export interface InitPlan { */ export function resolveInitPlan(flags: InitFlags, isTty: boolean): InitPlan { const installSkill = flags.skill === true; + const installGitignore = flags.gitignore === true; if (flags.all === true) { - return { agents: [...ALL_AGENT_IDS], installSkill, mode: "explicit", invalidAgents: [] }; + return { agents: [...ALL_AGENT_IDS], installSkill, installGitignore, mode: "explicit", invalidAgents: [] }; } if (flags.agents !== undefined) { @@ -167,6 +172,7 @@ export function resolveInitPlan(flags: InitFlags, isTty: boolean): InitPlan { return { agents: requested.filter(isAgentId), installSkill, + installGitignore, mode: "explicit", invalidAgents: requested.filter((a) => !isAgentId(a)), }; @@ -176,6 +182,7 @@ export function resolveInitPlan(flags: InitFlags, isTty: boolean): InitPlan { return { agents: [...DEFAULT_AGENTS], installSkill, + installGitignore, mode: interactive ? "interactive" : "default", invalidAgents: [], }; @@ -264,6 +271,8 @@ export interface InstallRepoOptions { agents?: readonly AgentId[]; } +const GITIGNORE_ENTRY = ".codebase-intelligence/"; + /** Write the instruction block into each selected agent's repo file. */ export function installRepoFiles(repoRoot: string, options: InstallRepoOptions = {}): InstallResult[] { const selected = options.agents ?? ALL_AGENT_IDS; @@ -293,6 +302,29 @@ export function installRepoFiles(repoRoot: string, options: InstallRepoOptions = return results; } +/** + * Add the canonical cache directory to a repo .gitignore without duplicates. + * + * @param repoRoot - Repository root where `.gitignore` should be updated. + * @returns File action for CLI/user reporting. + */ +export function installGitignoreEntry(repoRoot: string): InstallResult { + const filePath = path.join(repoRoot, ".gitignore"); + const existedBefore = fs.existsSync(filePath); + const original = existedBefore ? fs.readFileSync(filePath, "utf-8") : ""; + const lines = original.split(/\r?\n/).map((line) => line.trim()); + + if (lines.includes(GITIGNORE_ENTRY) || lines.includes(GITIGNORE_ENTRY.replace(/\/$/, ""))) { + return { path: ".gitignore", action: "unchanged" }; + } + + const separator = original === "" || original.endsWith("\n") ? "" : "\n"; + const next = `${original}${separator}${GITIGNORE_ENTRY}\n`; + + fs.writeFileSync(filePath, next, "utf-8"); + return { path: ".gitignore", action: existedBefore ? "updated" : "created" }; +} + /** Install the portable skill into the per-user Claude skills directory. */ export function installGlobalSkill(homeDir: string = os.homedir()): InstallResult { const dir = path.join(homeDir, ".claude", "skills", "codebase-intelligence"); diff --git a/src/parser/index.test.ts b/src/parser/index.test.ts index f661db0..b2be92c 100644 --- a/src/parser/index.test.ts +++ b/src/parser/index.test.ts @@ -519,6 +519,7 @@ export { Runner }; fs.writeFileSync(path.join(projectDir, "index.ts"), `export const x = 1;\n`); const ignoredFiles = [ + [".codebase-intelligence", "graph.ts"], [".code-visualizer", "graph.ts"], [".next", "types", "generated.ts"], ["dist", "cli.ts"], diff --git a/src/parser/index.ts b/src/parser/index.ts index adae545..73e5f55 100644 --- a/src/parser/index.ts +++ b/src/parser/index.ts @@ -14,6 +14,7 @@ interface PathAlias { const BUILT_IN_IGNORE_PATTERNS = [ ".git", "node_modules", + ".codebase-intelligence", ".code-visualizer", ".next", "dist", diff --git a/src/persistence/cache-key.ts b/src/persistence/cache-key.ts index 14553bb..ad08b22 100644 --- a/src/persistence/cache-key.ts +++ b/src/persistence/cache-key.ts @@ -4,13 +4,15 @@ import { execFileSync } from "child_process"; import { createHash } from "crypto"; import { getFullProgramFileLimit } from "../parser/index.js"; -export const INDEX_DIR_NAME = ".code-visualizer"; +export const CANONICAL_INDEX_DIR_NAME = ".codebase-intelligence"; +export const LEGACY_INDEX_DIR_NAME = ".code-visualizer"; const CACHE_SCHEMA_VERSION = 3; const CACHE_FINGERPRINT_IGNORE_PATTERNS = [ ".git", "node_modules", - INDEX_DIR_NAME, + CANONICAL_INDEX_DIR_NAME, + LEGACY_INDEX_DIR_NAME, ".next", "dist", "coverage", diff --git a/src/persistence/index-dir.ts b/src/persistence/index-dir.ts new file mode 100644 index 0000000..aef88e1 --- /dev/null +++ b/src/persistence/index-dir.ts @@ -0,0 +1,69 @@ +import fs from "fs"; +import path from "path"; +import { CANONICAL_INDEX_DIR_NAME, LEGACY_INDEX_DIR_NAME } from "./cache-key.js"; + +type IndexDirectoryMigration = "none" | "migrated-legacy" | "ignored-legacy"; + +export interface IndexDirectoryResolution { + activeDir: string; + canonicalDir: string; + legacyDir: string; + migration: IndexDirectoryMigration; +} + +/** + * Return canonical and legacy cache paths for a target repo. + * + * @param targetPath - Repo/codebase root passed to the CLI. + * @returns Absolute paths for canonical and legacy cache directories. + */ +export function getIndexDirs(targetPath: string): Pick { + const root = path.resolve(targetPath); + return { + canonicalDir: path.join(root, CANONICAL_INDEX_DIR_NAME), + legacyDir: path.join(root, LEGACY_INDEX_DIR_NAME), + }; +} + +/** + * Resolve the cache directory and migrate a legacy-only cache when safe. + * + * @param targetPath - Repo/codebase root passed to the CLI. + * @returns The active canonical cache directory plus migration status. + */ +export function prepareIndexDirectory(targetPath: string): IndexDirectoryResolution { + const { canonicalDir, legacyDir } = getIndexDirs(targetPath); + const canonicalExists = fs.existsSync(canonicalDir); + const legacyExists = fs.existsSync(legacyDir); + + if (!canonicalExists && legacyExists && fs.statSync(legacyDir).isDirectory()) { + fs.renameSync(legacyDir, canonicalDir); + return { activeDir: canonicalDir, canonicalDir, legacyDir, migration: "migrated-legacy" }; + } + + return { + activeDir: canonicalDir, + canonicalDir, + legacyDir, + migration: canonicalExists && legacyExists ? "ignored-legacy" : "none", + }; +} + +/** + * Remove canonical and legacy cache directories for explicit clean commands. + * + * @param targetPath - Repo/codebase root passed to the CLI. + * @returns Absolute paths removed from disk. + */ +export function cleanIndexDirectories(targetPath: string): string[] { + const { canonicalDir, legacyDir } = getIndexDirs(targetPath); + const removed: string[] = []; + + for (const dir of [canonicalDir, legacyDir]) { + if (!fs.existsSync(dir)) continue; + fs.rmSync(dir, { recursive: true, force: true }); + removed.push(dir); + } + + return removed; +} diff --git a/tests/cache-key.test.ts b/tests/cache-key.test.ts index 1ae89bf..5c4c3f3 100644 --- a/tests/cache-key.test.ts +++ b/tests/cache-key.test.ts @@ -3,7 +3,7 @@ import fs from "fs"; import os from "os"; import path from "path"; import { describe, expect, it } from "vitest"; -import { getCacheKey, INDEX_DIR_NAME } from "../src/persistence/cache-key.js"; +import { CANONICAL_INDEX_DIR_NAME, getCacheKey, LEGACY_INDEX_DIR_NAME } from "../src/persistence/cache-key.js"; const GIT_ENV = { ...process.env, @@ -36,14 +36,32 @@ function cacheKey(dir: string, headHash: string): string { } describe("cache key", () => { - it("ignores generated index files", () => { + it("ignores generated canonical index files", () => { const dir = fs.mkdtempSync(path.join(os.tmpdir(), "ci-cache-key-index-")); try { fs.writeFileSync(path.join(dir, "index.ts"), "export const value = 1;\n"); const headHash = initRepo(dir); const before = cacheKey(dir, headHash); - const indexDir = path.join(dir, INDEX_DIR_NAME); + const indexDir = path.join(dir, CANONICAL_INDEX_DIR_NAME); + fs.mkdirSync(indexDir); + fs.writeFileSync(path.join(indexDir, "graph.json"), "{}\n"); + fs.writeFileSync(path.join(indexDir, "meta.json"), "{}\n"); + + expect(cacheKey(dir, headHash)).toBe(before); + } finally { + fs.rmSync(dir, { recursive: true, force: true }); + } + }); + + it("ignores generated legacy index files", () => { + const dir = fs.mkdtempSync(path.join(os.tmpdir(), "ci-cache-key-index-")); + try { + fs.writeFileSync(path.join(dir, "index.ts"), "export const value = 1;\n"); + const headHash = initRepo(dir); + const before = cacheKey(dir, headHash); + + const indexDir = path.join(dir, LEGACY_INDEX_DIR_NAME); fs.mkdirSync(indexDir); fs.writeFileSync(path.join(indexDir, "graph.json"), "{}\n"); fs.writeFileSync(path.join(indexDir, "meta.json"), "{}\n"); diff --git a/tests/cli-cache.e2e.test.ts b/tests/cli-cache.e2e.test.ts new file mode 100644 index 0000000..1dc525d --- /dev/null +++ b/tests/cli-cache.e2e.test.ts @@ -0,0 +1,75 @@ +import { spawnSync, execSync } from "node:child_process"; +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; +import { beforeAll, describe, expect, it } from "vitest"; +import { CANONICAL_INDEX_DIR_NAME, LEGACY_INDEX_DIR_NAME } from "../src/persistence/cache-key.js"; + +const here = path.dirname(fileURLToPath(import.meta.url)); +const repoRoot = path.resolve(here, ".."); +const cli = path.join(repoRoot, "dist", "cli.js"); + +function run(args: readonly string[]): { status: number | null; stdout: string; stderr: string } { + const res = spawnSync("node", [cli, ...args], { + cwd: repoRoot, + env: { ...process.env }, + encoding: "utf-8", + }); + return { status: res.status, stdout: res.stdout, stderr: res.stderr }; +} + +function fixtureRepo(): string { + const dir = fs.mkdtempSync(path.join(os.tmpdir(), "ci-cache-e2e-")); + fs.writeFileSync(path.join(dir, "index.ts"), "export const value = 1;\n", "utf-8"); + return dir; +} + +beforeAll(() => { + if (!fs.existsSync(cli)) execSync("pnpm build", { cwd: repoRoot, stdio: "inherit" }); +}, 120_000); + +describe("cache CLI lifecycle", () => { + it("writes canonical cache, reports status, and cleans canonical cache", () => { + const repo = fixtureRepo(); + try { + const overview = run(["overview", repo, "--json", "--force"]); + expect(overview.status).toBe(0); + expect(fs.existsSync(path.join(repo, CANONICAL_INDEX_DIR_NAME, "graph.json"))).toBe(true); + expect(fs.existsSync(path.join(repo, LEGACY_INDEX_DIR_NAME))).toBe(false); + + const status = run([repo, "--status"]); + expect(status.status).toBe(0); + expect(status.stderr).toContain("Index status:"); + expect(status.stderr).toContain("Files:"); + + fs.mkdirSync(path.join(repo, LEGACY_INDEX_DIR_NAME)); + const clean = run([repo, "--clean"]); + expect(clean.status).toBe(0); + expect(clean.stderr).toContain(CANONICAL_INDEX_DIR_NAME); + expect(fs.existsSync(path.join(repo, CANONICAL_INDEX_DIR_NAME))).toBe(false); + expect(fs.existsSync(path.join(repo, LEGACY_INDEX_DIR_NAME))).toBe(false); + } finally { + fs.rmSync(repo, { recursive: true, force: true }); + } + }); + + it("migrates legacy cache directory before writing canonical cache", () => { + const repo = fixtureRepo(); + try { + const legacy = path.join(repo, LEGACY_INDEX_DIR_NAME); + const canonical = path.join(repo, CANONICAL_INDEX_DIR_NAME); + fs.mkdirSync(legacy); + fs.writeFileSync(path.join(legacy, "marker.txt"), "legacy\n", "utf-8"); + + const overview = run(["overview", repo, "--json", "--force"]); + expect(overview.status).toBe(0); + expect(overview.stderr).toContain("Migrated legacy index"); + expect(fs.existsSync(legacy)).toBe(false); + expect(fs.readFileSync(path.join(canonical, "marker.txt"), "utf-8")).toBe("legacy\n"); + expect(fs.existsSync(path.join(canonical, "graph.json"))).toBe(true); + } finally { + fs.rmSync(repo, { recursive: true, force: true }); + } + }); +}); diff --git a/tests/cli-init.e2e.test.ts b/tests/cli-init.e2e.test.ts index 1c1916a..5596c87 100644 --- a/tests/cli-init.e2e.test.ts +++ b/tests/cli-init.e2e.test.ts @@ -178,6 +178,17 @@ describe("init lifecycle (e2e)", () => { expect(read("AGENTS.md")).toBe(before); }); + it("--gitignore adds the canonical cache directory once", () => { + const first = run(["init", "--agents", "", "--gitignore", repo], home); + const second = run(["init", "--agents", "", "--gitignore", repo], home); + + expect(first.status).toBe(0); + expect(first.stdout).toContain("created .gitignore"); + expect(second.status).toBe(0); + expect(second.stdout).toContain("unchanged .gitignore"); + expect(read(".gitignore")).toBe(".codebase-intelligence/\n"); + }); + it("exits 1 when the target path does not exist", () => { const missing = path.join(os.tmpdir(), "ci-init-missing-zzz-do-not-create"); const { status, stderr } = run(["init", missing], home); diff --git a/tests/index-directory.test.ts b/tests/index-directory.test.ts new file mode 100644 index 0000000..84eae10 --- /dev/null +++ b/tests/index-directory.test.ts @@ -0,0 +1,116 @@ +import fs from "fs"; +import os from "os"; +import path from "path"; +import { describe, expect, it } from "vitest"; +import { CANONICAL_INDEX_DIR_NAME, LEGACY_INDEX_DIR_NAME } from "../src/persistence/cache-key.js"; +import { cleanIndexDirectories, prepareIndexDirectory } from "../src/persistence/index-dir.js"; + +function tempDir(): string { + return fs.mkdtempSync(path.join(os.tmpdir(), "ci-index-dir-")); +} + +describe("index directory migration", () => { + it("uses the canonical directory when no cache exists", () => { + const dir = tempDir(); + try { + const result = prepareIndexDirectory(dir); + expect(result.activeDir).toBe(path.join(dir, CANONICAL_INDEX_DIR_NAME)); + expect(result.migration).toBe("none"); + expect(fs.existsSync(result.activeDir)).toBe(false); + } finally { + fs.rmSync(dir, { recursive: true, force: true }); + } + }); + + it("keeps an existing canonical directory", () => { + const dir = tempDir(); + try { + const canonical = path.join(dir, CANONICAL_INDEX_DIR_NAME); + fs.mkdirSync(canonical); + + const result = prepareIndexDirectory(dir); + expect(result.activeDir).toBe(canonical); + expect(result.migration).toBe("none"); + expect(fs.existsSync(canonical)).toBe(true); + } finally { + fs.rmSync(dir, { recursive: true, force: true }); + } + }); + + it("migrates a legacy-only directory to the canonical name", () => { + const dir = tempDir(); + try { + const legacy = path.join(dir, LEGACY_INDEX_DIR_NAME); + const canonical = path.join(dir, CANONICAL_INDEX_DIR_NAME); + fs.mkdirSync(legacy); + fs.writeFileSync(path.join(legacy, "marker.txt"), "legacy\n"); + + const result = prepareIndexDirectory(dir); + expect(result.activeDir).toBe(canonical); + expect(result.migration).toBe("migrated-legacy"); + expect(fs.existsSync(legacy)).toBe(false); + expect(fs.readFileSync(path.join(canonical, "marker.txt"), "utf-8")).toBe("legacy\n"); + } finally { + fs.rmSync(dir, { recursive: true, force: true }); + } + }); + + it("prefers canonical and leaves legacy untouched when both have the same signature", () => { + const dir = tempDir(); + try { + const canonical = path.join(dir, CANONICAL_INDEX_DIR_NAME); + const legacy = path.join(dir, LEGACY_INDEX_DIR_NAME); + fs.mkdirSync(canonical); + fs.mkdirSync(legacy); + fs.writeFileSync(path.join(canonical, "marker.txt"), "canonical\n"); + fs.writeFileSync(path.join(legacy, "marker.txt"), "legacy\n"); + fs.writeFileSync(path.join(canonical, "meta.json"), JSON.stringify({ cacheKey: "same" })); + fs.writeFileSync(path.join(legacy, "meta.json"), JSON.stringify({ cacheKey: "same" })); + + const result = prepareIndexDirectory(dir); + expect(result.activeDir).toBe(canonical); + expect(result.migration).toBe("ignored-legacy"); + expect(fs.readFileSync(path.join(canonical, "marker.txt"), "utf-8")).toBe("canonical\n"); + expect(fs.readFileSync(path.join(legacy, "marker.txt"), "utf-8")).toBe("legacy\n"); + } finally { + fs.rmSync(dir, { recursive: true, force: true }); + } + }); + + it("prefers canonical and leaves legacy untouched when both have different signatures", () => { + const dir = tempDir(); + try { + const canonical = path.join(dir, CANONICAL_INDEX_DIR_NAME); + const legacy = path.join(dir, LEGACY_INDEX_DIR_NAME); + fs.mkdirSync(canonical); + fs.mkdirSync(legacy); + fs.writeFileSync(path.join(canonical, "meta.json"), JSON.stringify({ cacheKey: "canonical" })); + fs.writeFileSync(path.join(legacy, "meta.json"), JSON.stringify({ cacheKey: "legacy" })); + + const result = prepareIndexDirectory(dir); + expect(result.activeDir).toBe(canonical); + expect(result.migration).toBe("ignored-legacy"); + expect(JSON.parse(fs.readFileSync(path.join(canonical, "meta.json"), "utf-8"))).toEqual({ cacheKey: "canonical" }); + expect(JSON.parse(fs.readFileSync(path.join(legacy, "meta.json"), "utf-8"))).toEqual({ cacheKey: "legacy" }); + } finally { + fs.rmSync(dir, { recursive: true, force: true }); + } + }); + + it("clean removes canonical and legacy index directories", () => { + const dir = tempDir(); + try { + const canonical = path.join(dir, CANONICAL_INDEX_DIR_NAME); + const legacy = path.join(dir, LEGACY_INDEX_DIR_NAME); + fs.mkdirSync(canonical); + fs.mkdirSync(legacy); + + const removed = cleanIndexDirectories(dir); + expect(removed.sort()).toEqual([canonical, legacy].sort()); + expect(fs.existsSync(canonical)).toBe(false); + expect(fs.existsSync(legacy)).toBe(false); + } finally { + fs.rmSync(dir, { recursive: true, force: true }); + } + }); +}); diff --git a/tests/phase6-red.test.ts b/tests/phase6-red.test.ts index 4dfde1e..c7c413f 100644 --- a/tests/phase6-red.test.ts +++ b/tests/phase6-red.test.ts @@ -11,12 +11,13 @@ beforeAll(() => { }); describe("6.1 — CLI persistence commands", () => { - it("--index writes graph.json and meta.json to .code-visualizer/", async () => { + it("--index writes graph.json and meta.json to .codebase-intelligence/", async () => { const { exportGraph } = await import("../src/persistence/index.js"); + const { CANONICAL_INDEX_DIR_NAME } = await import("../src/persistence/cache-key.js"); const { codebaseGraph } = getFixturePipeline(); const tmpDir = path.join(os.tmpdir(), `cv-cli-test-${Date.now()}`); - const indexDir = path.join(tmpDir, ".code-visualizer"); + const indexDir = path.join(tmpDir, CANONICAL_INDEX_DIR_NAME); try { exportGraph(codebaseGraph, indexDir, "test-head-hash", "test-cache-key"); @@ -39,12 +40,13 @@ describe("6.1 — CLI persistence commands", () => { } }); - it("--status reads index info from .code-visualizer/", async () => { + it("--status reads index info from .codebase-intelligence/", async () => { const { exportGraph, importGraph } = await import("../src/persistence/index.js"); + const { CANONICAL_INDEX_DIR_NAME } = await import("../src/persistence/cache-key.js"); const { codebaseGraph } = getFixturePipeline(); const tmpDir = path.join(os.tmpdir(), `cv-status-test-${Date.now()}`); - const indexDir = path.join(tmpDir, ".code-visualizer"); + const indexDir = path.join(tmpDir, CANONICAL_INDEX_DIR_NAME); try { exportGraph(codebaseGraph, indexDir, "status-hash-abc", "status-cache-key"); @@ -61,12 +63,13 @@ describe("6.1 — CLI persistence commands", () => { } }); - it("--clean removes .code-visualizer/ directory", async () => { + it("--clean removes .codebase-intelligence/ directory", async () => { const { exportGraph } = await import("../src/persistence/index.js"); + const { CANONICAL_INDEX_DIR_NAME } = await import("../src/persistence/cache-key.js"); const { codebaseGraph } = getFixturePipeline(); const tmpDir = path.join(os.tmpdir(), `cv-clean-test-${Date.now()}`); - const indexDir = path.join(tmpDir, ".code-visualizer"); + const indexDir = path.join(tmpDir, CANONICAL_INDEX_DIR_NAME); try { exportGraph(codebaseGraph, indexDir, "clean-hash"); @@ -81,10 +84,11 @@ describe("6.1 — CLI persistence commands", () => { it("--force re-indexes even when HEAD unchanged", async () => { const { exportGraph, importGraph } = await import("../src/persistence/index.js"); + const { CANONICAL_INDEX_DIR_NAME } = await import("../src/persistence/cache-key.js"); const { codebaseGraph } = getFixturePipeline(); const tmpDir = path.join(os.tmpdir(), `cv-force-test-${Date.now()}`); - const indexDir = path.join(tmpDir, ".code-visualizer"); + const indexDir = path.join(tmpDir, CANONICAL_INDEX_DIR_NAME); try { exportGraph(codebaseGraph, indexDir, "same-hash"); From 5165eb59907d8bbd76bdf61bbd43e97a22ad7532 Mon Sep 17 00:00:00 2001 From: bntvllnt <32437578+bntvllnt@users.noreply.github.com> Date: Wed, 1 Jul 2026 00:29:57 +0200 Subject: [PATCH 2/2] ci(test): bound best-effort coverage --- .github/workflows/ci.yml | 9 +++++---- roadmap.md | 1 + 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6720b02..24a58d0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -41,13 +41,14 @@ jobs: - name: Test run: pnpm test - # Coverage is best-effort: vitest v3 + v8 provider has a known worker - # timeout bug (onTaskUpdate) on CI runners. Tests pass above; coverage - # collection may timeout without affecting correctness. + # Coverage is best-effort: vitest v3 + v8 provider can hang after tests + # pass on CI runners. The blocking Test step above proves correctness; + # this step is bounded so coverage cannot block the release gate. - name: Coverage if: matrix.node-version == 22 continue-on-error: true - run: pnpm vitest run --coverage --pool forks --no-file-parallelism + timeout-minutes: 4 + run: timeout 180s pnpm vitest run --coverage --pool forks --no-file-parallelism || true - name: Upload coverage if: matrix.node-version == 22 && always() diff --git a/roadmap.md b/roadmap.md index 2a81e4f..b16f123 100644 --- a/roadmap.md +++ b/roadmap.md @@ -242,6 +242,7 @@ Fix the repeated Vitest runner timeout so release gates are deterministic. - Isolate the issue to the Vitest fork worker pool used by the release gate. - 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. ### Targeted Framework Entry Fixes