Skip to content
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
322b527
feat: unified AST analysis framework with pluggable visitor pattern
carlos-alm Mar 9, 2026
e4a5ac0
merge: resolve BACKLOG.md conflict, renumber dynamic import item to I…
carlos-alm Mar 9, 2026
7480476
revert: restore original check-dead-exports.sh hook
carlos-alm Mar 9, 2026
cd7f0e1
fix: address Greptile review — indexOf→m.index, precise publicAPI reg…
carlos-alm Mar 9, 2026
445a04a
Merge branch 'main' into feat/unified-ast-analysis-framework
carlos-alm Mar 9, 2026
c196c8e
fix: address Greptile review — halsteadSkip depth counter, debug logg…
carlos-alm Mar 9, 2026
89344de
Merge remote-tracking branch 'origin/main' into feat/unified-ast-anal…
carlos-alm Mar 9, 2026
a47eb47
fix: remove function nodes from nestingNodeTypes and eliminate O(n²) …
carlos-alm Mar 9, 2026
77b4516
fix: remove function nesting inflation in computeAllMetrics and pass …
carlos-alm Mar 9, 2026
a84b7e4
fix: guard runAnalyses call, fix nested function nesting, rename _eng…
carlos-alm Mar 9, 2026
6d0f34e
fix: remove redundant processed Set and fix multi-line destructuring …
carlos-alm Mar 9, 2026
784bdf7
refactor: migrate raw SQL into repository pattern (Phase 3.3)
carlos-alm Mar 10, 2026
3db3861
fix: address Greptile review — deduplicate relatedTests, hoist prepar…
carlos-alm Mar 10, 2026
d9085b9
Merge remote-tracking branch 'origin/main' into feat/unified-ast-anal…
carlos-alm Mar 11, 2026
a812465
Merge branch 'main' into feat/unified-ast-analysis-framework
carlos-alm Mar 11, 2026
72c20a9
perf: cache prepared statements in hot-path repository functions
carlos-alm Mar 11, 2026
3a8f68b
fix: guard pre-push hook against sh -e failure when diff-impact unava…
carlos-alm Mar 11, 2026
1c43b08
Merge branch 'main' into feat/unified-ast-analysis-framework
carlos-alm Mar 11, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 41 additions & 0 deletions .claude/hooks/check-dead-exports.sh
Original file line number Diff line number Diff line change
Expand Up @@ -62,19 +62,60 @@ if [ -z "$FILES_TO_CHECK" ]; then
fi

# Single Node.js invocation: check all files in one process
# Excludes exports that are re-exported from index.js (public API) or consumed
# via dynamic import() — codegraph's static graph doesn't track those edges.
DEAD_EXPORTS=$(node -e "
const fs = require('fs');
const path = require('path');
const root = process.argv[1];
const files = process.argv[2].split('\n').filter(Boolean);

const { exportsData } = require(path.join(root, 'src/queries.js'));

// Build set of names exported from index.js (public API surface)
const indexSrc = fs.readFileSync(path.join(root, 'src/index.js'), 'utf8');
const publicAPI = new Set();
// Match: export { foo, bar as baz } from '...'
for (const m of indexSrc.matchAll(/export\s*\{([^}]+)\}/g)) {
for (const part of m[1].split(',')) {
const name = part.trim().split(/\s+as\s+/).pop().trim();
if (name) publicAPI.add(name);
}
}
// Match: export default ...
if (/export\s+default\b/.test(indexSrc)) publicAPI.add('default');

// Scan all src/ files for dynamic import() consumers
const srcDir = path.join(root, 'src');
function scanDynamic(dir) {
for (const ent of fs.readdirSync(dir, { withFileTypes: true })) {
if (ent.isDirectory()) { scanDynamic(path.join(dir, ent.name)); continue; }
if (!ent.name.endsWith('.js')) continue;
try {
const src = fs.readFileSync(path.join(dir, ent.name), 'utf8');
// Multi-line-safe: match const { ... } = [await] import('...')
for (const m of src.matchAll(/const\s*\{([^}]+)\}\s*=\s*(?:await\s+)?import\s*\(['"]/gs)) {
for (const part of m[1].split(',')) {
const name = part.trim().split(/\s+as\s+/).pop().trim().split('\n').pop().trim();
if (name && /^\w+$/.test(name)) publicAPI.add(name);
}
}
// Also match single-binding: const X = [await] import('...') (default import)
for (const m of src.matchAll(/const\s+(\w+)\s*=\s*(?:await\s+)?import\s*\(['"]/g)) {
publicAPI.add(m[1]);
}
} catch {}
}
}
scanDynamic(srcDir);

const dead = [];
for (const file of files) {
try {
const data = exportsData(file, undefined, { noTests: true, unused: true });
if (data && data.results) {
for (const r of data.results) {
if (publicAPI.has(r.name)) continue; // public API or dynamic import consumer
dead.push(r.name + ' (' + data.file + ':' + r.line + ')');
}
}
Expand Down
20 changes: 19 additions & 1 deletion .claude/hooks/check-readme.sh
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
#!/bin/bash
# Hook: block git commit if README.md, CLAUDE.md, or ROADMAP.md might need updating but aren't staged.
# Runs as a PreToolUse hook on Bash tool calls.
#
# Policy:
# - If NO docs are staged but source files changed → deny (docs weren't considered)
# - If SOME docs are staged → allow (developer reviewed and chose which to update)
# - If commit message contains "docs check acknowledged" → allow (explicit bypass)

INPUT=$(cat)
COMMAND=$(echo "$INPUT" | node -e "
Expand All @@ -17,11 +22,16 @@ if ! echo "$COMMAND" | grep -qE '^\s*git\s+commit'; then
exit 0
fi

# Allow explicit bypass via commit message
if echo "$COMMAND" | grep -q 'docs check acknowledged'; then
exit 0
fi

# Check which docs are staged
STAGED_FILES=$(git diff --cached --name-only 2>/dev/null)
README_STAGED=$(echo "$STAGED_FILES" | grep -c '^README.md$' || true)
CLAUDE_STAGED=$(echo "$STAGED_FILES" | grep -c '^CLAUDE.md$' || true)
ROADMAP_STAGED=$(echo "$STAGED_FILES" | grep -c '^ROADMAP.md$' || true)
ROADMAP_STAGED=$(echo "$STAGED_FILES" | grep -c 'ROADMAP.md$' || true)

# If all three are staged, all good
if [ "$README_STAGED" -gt 0 ] && [ "$CLAUDE_STAGED" -gt 0 ] && [ "$ROADMAP_STAGED" -gt 0 ]; then
Expand All @@ -32,6 +42,14 @@ fi
NEEDS_CHECK=$(echo "$STAGED_FILES" | grep -cE '(src/|cli\.js|constants\.js|parser\.js|package\.json|grammars/)' || true)

if [ "$NEEDS_CHECK" -gt 0 ]; then
DOCS_STAGED=$((README_STAGED + CLAUDE_STAGED + ROADMAP_STAGED))

# If at least one doc is staged, developer considered docs — allow with info
if [ "$DOCS_STAGED" -gt 0 ]; then
exit 0
fi

# No docs staged at all — block
MISSING=""
[ "$README_STAGED" -eq 0 ] && MISSING="README.md"
[ "$CLAUDE_STAGED" -eq 0 ] && MISSING="${MISSING:+$MISSING, }CLAUDE.md"
Expand Down
1 change: 1 addition & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ JS source is plain JavaScript (ES modules) in `src/`. No transpilation step. The
| `native.js` | Native napi-rs addon loader with WASM fallback |
| `registry.js` | Global repo registry (`~/.codegraph/registry.json`) for multi-repo MCP |
| `resolve.js` | Import resolution (supports native batch mode) |
| `ast-analysis/` | Unified AST analysis framework: shared DFS walker (`visitor.js`), engine orchestrator (`engine.js`), extracted metrics (`metrics.js`), and pluggable visitors for complexity, dataflow, and AST-store |
| `complexity.js` | Cognitive, cyclomatic, Halstead, MI computation from AST; `complexity` CLI command |
| `communities.js` | Louvain community detection, drift analysis |
| `manifesto.js` | Configurable rule engine with warn/fail thresholds; CI gate |
Expand Down
112 changes: 58 additions & 54 deletions docs/roadmap/ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -562,97 +562,101 @@ Plus updated enums on existing tools (edge_kinds, symbol kinds).

**Context:** Phases 2.5 and 2.7 added 38 modules and grew the codebase from 5K to 26,277 lines without introducing shared abstractions. The dual-function anti-pattern was replicated across 19 modules. Three independent AST analysis engines (complexity, CFG, dataflow) totaling 4,801 lines share the same fundamental pattern but no infrastructure. Raw SQL is scattered across 25+ modules touching 13 tables. The priority ordering has been revised based on actual growth patterns -- the new #1 priority is the unified AST analysis framework.

### 3.1 -- Unified AST Analysis Framework ★ Critical (New)
### 3.1 -- Unified AST Analysis Framework ★ Critical

Unify the three independent AST analysis engines (complexity, CFG, dataflow) plus AST node storage into a shared visitor framework. These four modules total 5,193 lines and independently implement the same pattern: per-language rules map → AST walk → collect data → write to DB → query → format.
Unify the independent AST analysis engines (complexity, CFG, dataflow) plus AST node storage into a shared visitor framework. These four modules independently implement the same pattern: per-language rules map → AST walk → collect data → write to DB → query → format.

| Module | Lines | Languages | Pattern |
|--------|-------|-----------|---------|
| `complexity.js` | 2,163 | 8 | Per-language rules → AST walk → collect metrics |
| `cfg.js` | 1,451 | 9 | Per-language rules → AST walk → build basic blocks |
| `dataflow.js` | 1,187 | 1 (JS/TS) | Scope stack → AST walk → collect flows |
| `ast.js` | 392 | 1 (JS/TS) | AST walk → extract stored nodes |

The extractors refactoring (Phase 2.7.6) proved the pattern: split per-language rules into files, share the engine. Apply it to all four AST analysis passes.
**Completed:** All 4 analyses (complexity, CFG, dataflow, AST-store) now run in a single DFS walk via `walkWithVisitors`. The CFG visitor rewrite ([#392](https://github.com/optave/codegraph/pull/392)) eliminated the Mode A/B split, replaced the 813-line `buildFunctionCFG` with a node-level visitor, and derives cyclomatic complexity directly from CFG structure (`E - N + 2`). `cfg.js` reduced from 1,242 → 518 lines.

```
src/
ast-analysis/
visitor.js # Shared AST visitor with hook points
engine.js # Single-pass or multi-pass orchestrator
metrics.js # Halstead, MI, LOC/SLOC (language-agnostic)
cfg-builder.js # Basic-block + edge construction
rules/
complexity/{lang}.js # Cognitive/cyclomatic rules per language
cfg/{lang}.js # Basic-block rules per language
dataflow/{lang}.js # Define-use chain rules per language
ast-store/{lang}.js # Node extraction rules per language
visitor.js # Shared DFS walker with pluggable visitor hooks
engine.js # Orchestrates all analyses in one coordinated pass
metrics.js # Halstead, MI, LOC/SLOC (extracted from complexity.js)
visitor-utils.js # Shared helpers (functionName, extractParams, etc.)
visitors/
complexity-visitor.js # Cognitive/cyclomatic/nesting + Halstead
cfg-visitor.js # Basic-block + edge construction via DFS hooks
ast-store-visitor.js # new/throw/await/string/regex extraction
dataflow-visitor.js # Scope stack + define-use chains
shared.js # findFunctionNode, rule factories, ext mapping
rules/ # Per-language rule files (unchanged)
```

A single AST walk with pluggable visitors eliminates 3 redundant tree traversals per function, shares language-specific node type mappings, and allows new analyses to plug in without creating another 1K+ line module.

**Affected files:** `src/complexity.js`, `src/cfg.js`, `src/dataflow.js`, `src/ast.js` -> split into `src/ast-analysis/`

### 3.2 -- Command/Query Separation ★ Critical 🔄

> **v3.1.1 progress:** CLI display wrappers for all query commands extracted to `queries-cli.js` (866 lines, 15 functions). Shared `result-formatter.js` (`outputResult()` for JSON/NDJSON) and `test-filter.js` created. `queries.js` reduced from 3,395 → 2,490 lines — all `*Data()` functions remain, CLI formatting fully separated ([#373](https://github.com/optave/codegraph/pull/373)).
- ✅ Shared DFS walker with `enterNode`/`exitNode`/`enterFunction`/`exitFunction` hooks, `skipChildren` per-visitor, nesting/scope tracking
- ✅ Complexity visitor (cognitive, cyclomatic, max nesting, Halstead) — file-level and function-level modes
- ✅ AST-store visitor (new/throw/await/string/regex extraction)
- ✅ Dataflow visitor (define-use chains, arg flows, mutations, scope stack)
- ✅ Engine orchestrator: unified pre-walk stores results as pre-computed data on `symbols`, then delegates to existing `buildXxx` for DB writes
- ✅ `builder.js` → single `runAnalyses` call replaces 4 sequential blocks + WASM pre-parse
- ✅ Extracted pure computations to `metrics.js` (Halstead derived math, LOC, MI)
- ✅ Extracted shared helpers to `visitor-utils.js` (from dataflow.js)
- ✅ CFG visitor rewrite — node-level DFS visitor replaces statement-level `buildFunctionCFG`, Mode A/B split eliminated ([#392](https://github.com/optave/codegraph/pull/392))
- ✅ Cyclomatic complexity derived from CFG (`E - N + 2`) — single source of truth for control flow metrics ([#392](https://github.com/optave/codegraph/pull/392))

- ✅ `queries.js` CLI wrappers → `queries-cli.js` (15 functions)
- ✅ Shared `result-formatter.js` (`outputResult` for JSON/NDJSON dispatch)
- ✅ Shared `test-filter.js` (`isTestFile` predicate)
- 🔲 Extract CLI wrappers from remaining modules (audit, batch, check, cochange, communities, complexity, cfg, dataflow, flow, manifesto, owners, structure, triage, branch-compare)
- 🔲 Introduce `CommandRunner` shared lifecycle
- 🔲 Per-command `src/commands/` directory structure
**Affected files:** `src/complexity.js`, `src/cfg.js`, `src/dataflow.js`, `src/ast.js` → split into `src/ast-analysis/`

Eliminate the `*Data()` / `*()` dual-function pattern replicated across 19 modules. Every analysis module (queries, audit, batch, check, cochange, communities, complexity, cfg, dataflow, ast, flow, manifesto, owners, structure, triage, branch-compare, viewer) currently implements both data extraction AND CLI formatting.
### 3.2 -- Command/Query Separation ★ Critical ✅

Introduce a shared `CommandRunner` that handles the open-DB -> validate -> execute -> format -> paginate -> output lifecycle. Each command only implements unique query + analysis logic. Formatting is always separate and pluggable (CLI text, JSON, NDJSON, Mermaid, DOT).
CLI display wrappers extracted from all 19 analysis modules into dedicated `src/commands/` files. Shared infrastructure (`result-formatter.js`, `test-filter.js`) moved to `src/infrastructure/`. `*Data()` functions remain in original modules — MCP dynamic imports unchanged. ~1,059 lines of CLI formatting code separated from analysis logic ([#373](https://github.com/optave/codegraph/pull/373), [#393](https://github.com/optave/codegraph/pull/393)).

```
src/
commands/ # One file per command
query.js # { execute(args, ctx) -> data, format(data, opts) -> string }
impact.js
audit.js
check.js
cfg.js
dataflow.js
...
commands/ # One file per command (16 files)
audit.js, batch.js, cfg.js, check.js, cochange.js, communities.js,
complexity.js, dataflow.js, flow.js, branch-compare.js, manifesto.js,
owners.js, sequence.js, structure.js, triage.js, query.js (barrel re-export)

infrastructure/
command-runner.js # Shared lifecycle
result-formatter.js # Shared formatting: table, JSON, NDJSON, Mermaid, DOT
result-formatter.js # Shared formatting: JSON, NDJSON dispatch
test-filter.js # Shared --no-tests / isTestFile logic
```

- ✅ `queries.js` CLI wrappers → `queries-cli.js` (15 functions)
- ✅ Shared `result-formatter.js` (`outputResult` for JSON/NDJSON dispatch)
- ✅ Shared `test-filter.js` (`isTestFile` predicate)
- ✅ CLI wrappers extracted from remaining 15 modules into `src/commands/` ([#393](https://github.com/optave/codegraph/pull/393))
- ✅ Per-command `src/commands/` directory structure ([#393](https://github.com/optave/codegraph/pull/393))
- ✅ `src/infrastructure/` directory for shared utilities ([#393](https://github.com/optave/codegraph/pull/393))
- ⏭️ `CommandRunner` shared lifecycle — deferred (command files vary too much for a single pattern today)

**Affected files:** All 19 modules with dual-function pattern, `src/cli.js`, `src/mcp.js`

### 3.3 -- Repository Pattern for Data Access ★ Critical 🔄
### 3.3 -- Repository Pattern for Data Access ★ Critical

> **v3.1.1 progress:** `src/db/` directory created with `repository.js` (134 lines), `query-builder.js` (280 lines), and `migrations.js` (312 lines). All db usage across the codebase wrapped in try/finally for reliable `db.close()` ([#371](https://github.com/optave/codegraph/pull/371), [#384](https://github.com/optave/codegraph/pull/384), [#383](https://github.com/optave/codegraph/pull/383)).
>
> **v3.1.2 progress:** `repository.js` split into `src/db/repository/` directory with 10 domain files (nodes, edges, build-stmts, complexity, cfg, dataflow, cochange, embeddings, graph-read, barrel). Raw SQL migrated from 14 src/ modules into repository layer. `connection.js` already complete (89 lines handling open/close/WAL/pragma/locks/readonly).

- ✅ `src/db/` directory structure created
- ✅ `repository.js` — initial Repository class
- ✅ `repository/` — domain-split repository (nodes, edges, build-stmts, complexity, cfg, dataflow, cochange, embeddings, graph-read)
- ✅ `query-builder.js` — lightweight SQL builder (280 lines)
- ✅ `migrations.js` — schema migrations extracted (312 lines)
- ✅ `connection.js` — connection setup (open, WAL mode, pragma tuning, readonly, locks)
- ✅ All db usage wrapped in try/finally for reliable `db.close()`
- 🔲 Migrate remaining raw SQL from 25+ modules into Repository
- 🔲 `connection.js` — extract connection setup (open, WAL mode, pragma tuning)

Consolidate all SQL into a single `Repository` class. Currently SQL is scattered across 25+ modules that each independently open the DB and write raw SQL inline across 13 tables.
- ✅ Migrate remaining raw SQL from 14 modules into Repository

```
src/
db/
connection.js # Open, WAL mode, pragma tuning
migrations.js # Schema versions (currently 13 migrations)
repository.js # ALL data access methods across all 13 tables
query-builder.js # Lightweight SQL builder for common filtered queries
repository/
index.js # Barrel re-export
nodes.js # Node lookups: getNodeId, findFileNodes, bulkNodeIdsByFile, etc.
edges.js # Edge queries: findCallees, findCallers, import/hierarchy edges
build-stmts.js # Cascade purge: purgeFileData, purgeFilesData
complexity.js # function_complexity table reads
cfg.js # cfg_blocks/cfg_edges reads + deletes
dataflow.js # dataflow table checks
cochange.js # co_changes/co_change_meta reads
embeddings.js # embeddings/embedding_meta reads
graph-read.js # Cross-table reads for export/communities
```

Add a query builder for the common pattern "find nodes WHERE kind IN (...) AND file NOT LIKE '%test%' ORDER BY ... LIMIT ? OFFSET ?". Not an ORM -- a thin SQL builder that eliminates string construction across 25 modules.

**Affected files:** `src/db.js` -> split into `src/db/`, SQL extracted from all modules
**Affected files:** `src/db.js` barrel updated, raw SQL extracted from `queries.js`, `builder.js`, `watcher.js`, `structure.js`, `complexity.js`, `cfg.js`, `dataflow.js`, `ast.js`, `ast-analysis/engine.js`, `embedder.js`, `sequence.js`, `communities.js`

### 3.4 -- Decompose queries.js (3,395 Lines) 🔄

Expand Down
Loading
Loading