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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down
10 changes: 5 additions & 5 deletions SKILL-QUICK.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -146,19 +146,19 @@ $CLI list --limit 5 --offset 10 --format compact # → paginated + co
### Tooling (1)
`plugin <install|list|search|update|info|validate>`

**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:

```bash
python3 scripts/codelens.py serve
```

Exposes 63 tools as `codelens_<command>` (e.g., `codelens_query`, `codelens_taint`, `codelens_graph_schema`, `codelens_architecture`, `codelens_resolve_types`, `codelens_git_status`):
Exposes 64 tools as `codelens_<command>` (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)

Expand Down
4 changes: 2 additions & 2 deletions SKILL.md
Original file line number Diff line number Diff line change
@@ -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.
---
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
236 changes: 236 additions & 0 deletions scripts/commands/arch_metrics.py
Original file line number Diff line number Diff line change
@@ -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(

Check failure on line 63 in scripts/commands/arch_metrics.py

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Refactor this function to reduce its Cognitive Complexity from 19 to the 15 allowed.

See more on https://sonarcloud.io/project/issues?id=Wolfvin_CodeLens&issues=AZ8Za4zq1AEKnFeKO4gy&open=AZ8Za4zq1AEKnFeKO4gy&pullRequest=109
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,
)
2 changes: 1 addition & 1 deletion scripts/graph_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
2 changes: 1 addition & 1 deletion skill.json
Original file line number Diff line number Diff line change
@@ -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": [
Expand Down
2 changes: 1 addition & 1 deletion tests/test_integration.py
Original file line number Diff line number Diff line change
@@ -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)
Expand Down
Loading