From 4f436c528561a623fefa0083eac5e27c3ebca582 Mon Sep 17 00:00:00 2001 From: Wolfvin Date: Tue, 30 Jun 2026 16:43:29 +0000 Subject: [PATCH] =?UTF-8?q?feat(arch):=20add=20arch-metrics=20command=20?= =?UTF-8?q?=E2=80=94=20fan-in/out,=20instability,=20god-module=20detection?= =?UTF-8?q?=20(closes=20#61)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 10 +- SKILL-QUICK.md | 10 +- SKILL.md | 4 +- pyproject.toml | 2 +- scripts/commands/arch_metrics.py | 236 +++++++++++++++++++++++++++++++ scripts/graph_model.py | 2 +- skill.json | 2 +- tests/test_integration.py | 2 +- 8 files changed, 252 insertions(+), 16 deletions(-) create mode 100644 scripts/commands/arch_metrics.py diff --git a/README.md b/README.md index 7c07e279..b62508a4 100644 --- a/README.md +++ b/README.md @@ -2,12 +2,12 @@ > **Before an AI writes a new class/id/function, CodeLens must be checked. This is not optional.** -CodeLens is an AI-native code intelligence platform that gives AI agents **full visibility** into a codebase before they write any code. It prevents collision, overwrite of existing logic, security vulnerabilities, and dead code through 65 CLI commands, an MCP server with 63 tools (50 static + 13 dynamic), AST-based taint analysis, live CVE/OSV scanning, a plugin system with OWASP Top 10 + Compliance rule packs, a true graph data model (nodes + edges) for structural code queries, and token-efficient `--format compact` output for high-volume agent workflows (issue #17). +CodeLens is an AI-native code intelligence platform that gives AI agents **full visibility** into a codebase before they write any code. It prevents collision, overwrite of existing logic, security vulnerabilities, and dead code through 66 CLI commands, an MCP server with 64 tools (50 static + 14 dynamic), AST-based taint analysis, live CVE/OSV scanning, a plugin system with OWASP Top 10 + Compliance rule packs, a true graph data model (nodes + edges) for structural code queries, and token-efficient `--format compact` output for high-volume agent workflows (issue #17). ## Features -- **65 CLI Commands** — From basic scan/query to AST taint analysis, CVE scanning, plugin management, auto-fix, dashboards, CI/CD quality gates, and `graph-schema` for cheap graph-shape introspection -- **MCP Server (63 Tools)** — Native AI agent integration via Model Context Protocol (JSON-RPC over stdio), 50 statically-defined tools + 13 dynamically discovered, every tool accepts a `format` parameter (`json`/`markdown`/`ai`/`sarif`/`compact`) +- **66 CLI Commands** — From basic scan/query to AST taint analysis, CVE scanning, plugin management, auto-fix, dashboards, CI/CD quality gates, and `graph-schema` for cheap graph-shape introspection +- **MCP Server (64 Tools)** — Native AI agent integration via Model Context Protocol (JSON-RPC over stdio), 50 statically-defined tools + 14 dynamically discovered, every tool accepts a `format` parameter (`json`/`markdown`/`ai`/`sarif`/`compact`) - **Token-Efficient Compact Output (v8.2, issue #17)** — `--format compact` produces single-char-key JSON with abbreviated types, omitted null fields, and relative paths — ~50% smaller than `json` on real trace output. Combined with `--limit`/`--offset` pagination, 5 structural queries now cost <5k tokens (down from 30-80k) - **AST Taint Engine** — Tree-sitter based taint analysis with return-value propagation, scope hierarchy, and branch condition refinement - **Live CVE/OSV Scanning** — Real-time vulnerability data from OSV.dev API with SQLite cache, 9 ecosystems (PyPI, npm, crates.io, Go, Maven, NuGet, RubyGems, Pub, Hex) @@ -225,8 +225,8 @@ codelens/ │ ├── changelog.md # Older changelog (per-version highlights) │ └── agent-integration.md # AI agent integration guide ├── scripts/ -│ ├── codelens.py # CLI entry point (65 commands registered) -│ ├── mcp_server.py # MCP JSON-RPC server (63 tools) +│ ├── codelens.py # CLI entry point (66 commands registered) +│ ├── mcp_server.py # MCP JSON-RPC server (64 tools) │ ├── registry.py # Registry read/write/build │ ├── persistent_registry.py # SQLite persistent storage (WAL mode) │ ├── base_parser.py # Base tree-sitter parser diff --git a/SKILL-QUICK.md b/SKILL-QUICK.md index 5da0036b..cf29d54e 100755 --- a/SKILL-QUICK.md +++ b/SKILL-QUICK.md @@ -114,7 +114,7 @@ $CLI list --limit 5 --offset 10 --format compact # → paginated + co | "Cross-file taint" | `dataflow` | `taint` (taint is single-file, AST-deep) | | "Auto-fix issues" | `fix` | `check` (check just gates, doesn't fix) | -## All 65 Commands +## All 66 Commands ### Setup & Lifecycle (8+) `init` · `scan [--incremental] [--max-files N] [--full]` · `registry-validate` · `detect` · `watch [--debounce SECS] [--git-mode] [--interval SECS]` · `git-status` · `migrate` · `serve` · `lsp-status` (issue #33: `codelens --lsp-status` top-level flag is an alias of `codelens lsp-status` — both delegate to `hybrid_engine.get_lsp_status()` and return the identical payload) @@ -146,9 +146,9 @@ $CLI list --limit 5 --offset 10 --format compact # → paginated + co ### Tooling (1) `plugin ` -**Total: 65 commands** (auto-registered via `commands/__init__.py`; rerun `python3 scripts/sync_command_count.py --apply` after adding/removing a command) +**Total: 66 commands** (auto-registered via `commands/__init__.py`; rerun `python3 scripts/sync_command_count.py --apply` after adding/removing a command) -## MCP Server (63 Tools) +## MCP Server (64 Tools) Start the MCP server for AI agent integration: @@ -156,9 +156,9 @@ Start the MCP server for AI agent integration: python3 scripts/codelens.py serve ``` -Exposes 63 tools as `codelens_` (e.g., `codelens_query`, `codelens_taint`, `codelens_graph_schema`, `codelens_architecture`, `codelens_resolve_types`, `codelens_git_status`): +Exposes 64 tools as `codelens_` (e.g., `codelens_query`, `codelens_taint`, `codelens_graph_schema`, `codelens_architecture`, `codelens_resolve_types`, `codelens_git_status`): - 50 statically-defined tools (full JSON schemas in `mcp_server.py`) -- 13 dynamically-discovered tools (auto-discovered from `COMMAND_REGISTRY`; long-running `watch` and `serve` are excluded) +- 14 dynamically-discovered tools (auto-discovered from `COMMAND_REGISTRY`; long-running `watch` and `serve` are excluded) - Every tool accepts a `format` parameter (`json`/`markdown`/`ai`/`sarif`/`compact`). Use `format: "compact"` for token-efficient responses (~50% smaller than `json`). - `watch` and `serve` itself are excluded (long-running) diff --git a/SKILL.md b/SKILL.md index cb753460..e3bd5eb2 100755 --- a/SKILL.md +++ b/SKILL.md @@ -1,10 +1,10 @@ --- name: codelens description: > - CodeLens — AI-Native Code Intelligence. 65 commands for AI-powered code analysis, + CodeLens — AI-Native Code Intelligence. 66 commands for AI-powered code analysis, security auditing, quality scoring, AST-based taint analysis, live CVE scanning, and pre-write safety checks. Supports 28+ languages with tree-sitter + regex - fallback parsing. MCP server exposes 63 tools for AI agent integration. + fallback parsing. MCP server exposes 64 tools for AI agent integration. For quick command reference with validated output schemas, see SKILL-QUICK.md. For version history, see CHANGELOG.md. --- diff --git a/pyproject.toml b/pyproject.toml index 9bfe9646..beed2de6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,7 @@ build-backend = "setuptools.build_meta" [project] name = "codelens" version = "8.2.0" -description = "Live Codebase Reference Intelligence — 65 commands for AI-powered code analysis, security auditing, and quality scoring" +description = "Live Codebase Reference Intelligence — 66 commands for AI-powered code analysis, security auditing, and quality scoring" readme = "README.md" license = {text = "MIT"} requires-python = ">=3.8" diff --git a/scripts/commands/arch_metrics.py b/scripts/commands/arch_metrics.py new file mode 100644 index 00000000..75072c06 --- /dev/null +++ b/scripts/commands/arch_metrics.py @@ -0,0 +1,236 @@ +"""arch-metrics command — architecture metrics from the graph database. + +Computes fan-in, fan-out, instability, and god-module detection from the +SQLite graph tables. Closes issue #61. + +Usage:: + + codelens arch-metrics [workspace] + codelens arch-metrics --format json + codelens arch-metrics --sort-by fan-in + codelens arch-metrics --threshold-fanin 15 --threshold-fanout 20 +""" + +from __future__ import annotations + +import os +import sys +import sqlite3 +import argparse +from typing import Any, Dict, List, Optional, Tuple + +from commands import register_command + + +def add_args(parser: argparse.ArgumentParser) -> None: + """Register arch-metrics CLI arguments. + + Note: ``--format`` is added globally by ``codelens.py`` with choices + ``json|markdown|ai|sarif|compact``. We reuse it: ``json`` produces + machine-readable JSON; any other value produces a human-readable table. + """ + parser.add_argument( + "workspace", + nargs="?", + default=None, + help="Path to workspace root (auto-detected if omitted)", + ) + parser.add_argument( + "--threshold-fanin", + type=int, + default=10, + help="Fan-in threshold for god-module detection (default: 10)", + ) + parser.add_argument( + "--threshold-fanout", + type=int, + default=15, + help="Fan-out threshold for god-module detection (default: 15)", + ) + parser.add_argument( + "--sort-by", + choices=["instability", "fan-in", "fan-out", "name"], + default="instability", + help="Sort order (default: instability descending)", + ) + + +def _default_db_path(workspace: str) -> str: + """Return the default SQLite database path for the workspace.""" + return os.path.join(workspace, ".codelens", "codelens.db") + + +def _compute_metrics( + db_path: str, + fanin_threshold: int, + fanout_threshold: int, +) -> List[Dict[str, Any]]: + """Compute fan-in/out + instability for every module in the graph. + + A "module" is a file path (the ``file`` column in ``graph_nodes``). + Fan-in = number of DISTINCT source files that have edges pointing TO + nodes in this file. Fan-out = number of DISTINCT target files that + edges FROM nodes in this file point to. + """ + conn = sqlite3.connect(db_path) + conn.row_factory = sqlite3.Row + + # Collect all distinct files from graph_nodes + files = { + row[0] + for row in conn.execute( + "SELECT DISTINCT file FROM graph_nodes WHERE file IS NOT NULL" + ) + } + if not files: + conn.close() + return [] + + # Fan-out per file: count distinct target files for edges originating + # from nodes in this file. + fan_out: Dict[str, int] = {} + for row in conn.execute( + """ + SELECT DISTINCT ge.source_id AS src_node, gn_target.file AS target_file + FROM graph_edges ge + JOIN graph_nodes gn_src ON ge.source_id = gn_src.node_id + LEFT JOIN graph_nodes gn_target ON ge.target_id = gn_target.node_id + WHERE gn_src.file IS NOT NULL AND gn_target.file IS NOT NULL + """ + ): + src_file = None + # Look up source file from node_id + src_row = conn.execute( + "SELECT file FROM graph_nodes WHERE node_id = ?", (row["src_node"],) + ).fetchone() + if src_row: + src_file = src_row["file"] + if src_file and row["target_file"] and src_file != row["target_file"]: + fan_out[src_file] = fan_out.get(src_file, 0) + 1 + + # Fan-in per file: count distinct source files that have edges pointing + # to nodes in this file. + fan_in: Dict[str, int] = {} + for row in conn.execute( + """ + SELECT DISTINCT ge.target_id AS tgt_node, gn_src.file AS src_file + FROM graph_edges ge + JOIN graph_nodes gn_target ON ge.target_id = gn_target.node_id + LEFT JOIN graph_nodes gn_src ON ge.source_id = gn_src.node_id + WHERE gn_target.file IS NOT NULL AND gn_src.file IS NOT NULL + """ + ): + tgt_file = None + tgt_row = conn.execute( + "SELECT file FROM graph_nodes WHERE node_id = ?", (row["tgt_node"],) + ).fetchone() + if tgt_row: + tgt_file = tgt_row["file"] + if tgt_file and row["src_file"] and tgt_file != row["src_file"]: + fan_in[tgt_file] = fan_in.get(tgt_file, 0) + 1 + + conn.close() + + # Build result list + results: List[Dict[str, Any]] = [] + for f in sorted(files): + fi = fan_in.get(f, 0) + fo = fan_out.get(f, 0) + total = fi + fo + instability = fo / total if total > 0 else 0.0 + flags: List[str] = [] + if fi > fanin_threshold or fo > fanout_threshold: + flags.append("god-module") + results.append({ + "module": f, + "fan_in": fi, + "fan_out": fo, + "instability": round(instability, 4), + "flags": flags, + }) + return results + + +def _sort_results( + results: List[Dict[str, Any]], + sort_by: str, +) -> List[Dict[str, Any]]: + """Sort results by the specified key.""" + if sort_by == "name": + return sorted(results, key=lambda r: r["module"]) + elif sort_by == "fan-in": + return sorted(results, key=lambda r: r["fan_in"], reverse=True) + elif sort_by == "fan-out": + return sorted(results, key=lambda r: r["fan_out"], reverse=True) + else: # instability (default) + return sorted(results, key=lambda r: r["instability"], reverse=True) + + +def _format_table(results: List[Dict[str, Any]]) -> str: + """Format results as a human-readable table.""" + if not results: + return "No modules found in graph." + + # Column widths + mod_w = max(len(r["module"]) for r in results) + mod_w = max(mod_w, len("Module")) + header = ( + f"{'Module':<{mod_w}} {'Fan-in':>6} {'Fan-out':>7} " + f"{'Instability':>12} {'Flags':s}" + ) + sep = "-" * len(header) + lines = [header, sep] + for r in results: + flags_str = ", ".join(r["flags"]) if r["flags"] else "" + lines.append( + f"{r['module']:<{mod_w}} {r['fan_in']:>6} {r['fan_out']:>7} " + f"{r['instability']:>12.2f} {flags_str}" + ) + return "\n".join(lines) + + +def execute(args: argparse.Namespace, workspace: str) -> Dict[str, Any]: + """Execute the arch-metrics command. + + Returns a dict with the metrics results. When ``--format table`` is + selected (default), also prints the table to stdout. + """ + db_path = _default_db_path(workspace) + + if not os.path.exists(db_path): + print( + "Error: Graph database not found. Run `codelens scan` first.", + file=sys.stderr, + ) + sys.exit(1) + + results = _compute_metrics( + db_path, + fanin_threshold=args.threshold_fanin, + fanout_threshold=args.threshold_fanout, + ) + results = _sort_results(results, args.sort_by) + + # The global formatter in codelens.py handles JSON output. We just + # return the dict; the formatter prints it. For non-JSON formats + # (markdown, ai, compact), the formatter also handles conversion. + return { + "status": "ok", + "workspace": workspace, + "metrics": results, + "total_modules": len(results), + "god_modules": [r for r in results if r["flags"]], + "thresholds": { + "fan_in": args.threshold_fanin, + "fan_out": args.threshold_fanout, + }, + "sort_by": args.sort_by, + } + + +register_command( + "arch-metrics", + "Compute architecture metrics (fan-in/out, instability, god-module detection) from graph", + add_args, + execute, +) diff --git a/scripts/graph_model.py b/scripts/graph_model.py index d7f7d6d8..22244927 100644 --- a/scripts/graph_model.py +++ b/scripts/graph_model.py @@ -13,7 +13,7 @@ - New tables `graph_nodes` and `graph_edges` are additive (prefixed `graph_` to avoid colliding with any existing table name). - The flat registry tables and JSON files are untouched. -- All 65 existing CLI commands continue to work unchanged. +- All 66 existing CLI commands continue to work unchanged. Schema: graph_nodes( diff --git a/skill.json b/skill.json index 8041b7c8..e2b2ad3d 100755 --- a/skill.json +++ b/skill.json @@ -1,7 +1,7 @@ { "name": "codelens", "version": "8.2.0", - "description": "Live Codebase Reference Intelligence. 65 commands for AI-powered code analysis, security auditing, quality scoring, and pre-write safety checks. Supports 28+ languages with regex+AST hybrid parsing. Must activate before writing/editing/deleting any class, id, or function.", + "description": "Live Codebase Reference Intelligence. 66 commands for AI-powered code analysis, security auditing, quality scoring, and pre-write safety checks. Supports 28+ languages with regex+AST hybrid parsing. Must activate before writing/editing/deleting any class, id, or function.", "author": "codelens", "command_categories": { "setup": [ diff --git a/tests/test_integration.py b/tests/test_integration.py index 4303c159..6aad33de 100644 --- a/tests/test_integration.py +++ b/tests/test_integration.py @@ -1,5 +1,5 @@ """ -Integration smoke tests for all 65 CodeLens commands. +Integration smoke tests for all 66 CodeLens commands. Tests that every command: 1. Runs without crash (valid JSON output)