Skip to content

Commit 77aba34

Browse files
authored
Merge pull request #153 from Wolfvin/feat/issue-59-graphml-export
feat(formatters): GraphML XML export for graph-producing commands (closes #59 phase-3)
2 parents ceabe7c + 937c02e commit 77aba34

7 files changed

Lines changed: 1306 additions & 36 deletions

File tree

README.md

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ CodeLens is an AI-native code intelligence platform that gives AI agents **full
77
## Features
88

99
- **75 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
10-
- **MCP Server (73 Tools)** — Native AI agent integration via Model Context Protocol (JSON-RPC over stdio), 56 statically-defined tools + 17 dynamically discovered, every tool accepts a `format` parameter (`json`/`markdown`/`ai`/`sarif`/`compact`)
10+
- **MCP Server (73 Tools)** — Native AI agent integration via Model Context Protocol (JSON-RPC over stdio), 56 statically-defined tools + 17 dynamically discovered, every tool accepts a `format` parameter (`json`/`markdown`/`ai`/`sarif`/`compact`/`graphml`)
1111
- **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)
1212
- **AST Taint Engine** — Tree-sitter based taint analysis with return-value propagation, scope hierarchy, and branch condition refinement
1313
- **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)
@@ -284,7 +284,7 @@ codelens/
284284
│ ├── pre_commit_hook.py # Git pre-commit hook integration
285285
│ ├── utils.py # Shared utilities (version, helpers)
286286
│ ├── commands/ # One file per CLI command (auto-registered, 64 commands)
287-
│ ├── formatters/ # Output formatters (markdown, sarif, compact)
287+
│ ├── formatters/ # Output formatters (markdown, sarif, compact, graphml)
288288
│ ├── parsers/ # Tree-sitter + fallback parsers
289289
│ │ ├── html_parser.py, css_parser.py, js_frontend_parser.py, js_backend_parser.py
290290
│ │ ├── rust_parser.py, python_parser.py, tsx_parser.py, ts_backend_parser.py
@@ -338,8 +338,9 @@ CodeLens ships with a native MCP server (55 tools) for direct AI agent integrati
338338
python3 scripts/codelens.py serve
339339
```
340340

341-
Every MCP tool accepts a `format` parameter with the enum `[json, markdown, ai, sarif, compact]`.
342-
For high-volume agent workflows, pass `format: "compact"` to cut token usage ~50%. Example:
341+
Every MCP tool accepts a `format` parameter with the enum `[json, markdown, ai, sarif, compact, graphml]`.
342+
For high-volume agent workflows, pass `format: "compact"` to cut token usage ~50%.
343+
For graph-producing commands (`scan`, `trace`, `impact`, `circular`), pass `format: "graphml"` to emit a GraphML 1.0 XML document that opens directly in Gephi/Cytoscape/yEd/Neo4j (issue #59 Phase 3). Example:
343344

344345
```json
345346
// tools/call with format=compact
@@ -371,6 +372,12 @@ python3 scripts/codelens.py check /path/to/workspace --severity high --max-findi
371372

372373
# SARIF output for GitHub Advanced Security / VS Code
373374
python3 scripts/codelens.py check /path/to/workspace --format sarif > codelens.sarif
375+
376+
# GraphML export — opens in Gephi/Cytoscape/yEd/Neo4j (issue #59 Phase 3)
377+
python3 scripts/codelens.py scan /path/to/workspace --format graphml > codelens.graphml
378+
python3 scripts/codelens.py trace main /path/to/workspace --format graphml > trace.graphml
379+
python3 scripts/codelens.py impact my_function /path/to/workspace --format graphml > impact.graphml
380+
python3 scripts/codelens.py circular /path/to/workspace --format graphml > cycles.graphml
374381
```
375382

376383
### Plugin System

SKILL-QUICK.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ $CLI list --limit 5 --offset 10 --format compact # → paginated + co
2929
| `--format ai` | Normalized: `{stats, items[], truncated, recommendations}` |
3030
| `--format compact` | Token-efficient: single-char keys + abbreviated types (issue #17). ~50% smaller than `json`. Best for high-volume MCP tool calls |
3131
| `--format sarif` | SARIF v2.1.0 output for GitHub Advanced Security / VS Code |
32+
| `--format graphml` | GraphML 1.0 XML for graph-producing commands (`scan`, `trace`, `impact`, `circular`). Opens in Gephi/Cytoscape/yEd/Neo4j. Other commands emit a single-node placeholder (issue #59 Phase 3) |
3233
| `--limit N` / `--offset N` | Pagination on list-type commands (`list`, `search`, `trace`, `symbols`, `outline`). Default limit=20 (issue #17). `--top N` is an alias for `--limit N --offset 0` |
3334
| `--deep` | Enable LSP-enhanced deep analysis (requires language server; check with `lsp-status`) |
3435
| `--db-path PATH` | Custom SQLite database path (default: `.codelens/codelens.db`) |
@@ -160,7 +161,7 @@ python3 scripts/codelens.py serve
160161
Exposes 73 tools as `codelens_<command>` (e.g., `codelens_query`, `codelens_taint`, `codelens_graph_schema`, `codelens_architecture`, `codelens_resolve_types`, `codelens_git_status`):
161162
- 50 statically-defined tools (full JSON schemas in `mcp_server.py`)
162163
- 17 dynamically-discovered tools (auto-discovered from `COMMAND_REGISTRY`; long-running `watch` and `serve` are excluded)
163-
- Every tool accepts a `format` parameter (`json`/`markdown`/`ai`/`sarif`/`compact`). Use `format: "compact"` for token-efficient responses (~50% smaller than `json`).
164+
- Every tool accepts a `format` parameter (`json`/`markdown`/`ai`/`sarif`/`compact`/`graphml`). Use `format: "compact"` for token-efficient responses (~50% smaller than `json`). Use `format: "graphml"` for graph-producing commands (`scan`/`trace`/`impact`/`circular`) — emits GraphML 1.0 XML that opens in Gephi/Cytoscape/yEd/Neo4j (issue #59 Phase 3).
164165
- `watch` and `serve` itself are excluded (long-running)
165166

166167
See `mcp_config.json` for Claude Desktop, Cursor, VS Code Copilot, Continue.dev, and Cline configuration templates.

scripts/codelens.py

Lines changed: 10 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -885,16 +885,12 @@ def main():
885885
# ``["text", "json"]`` because its default human-readable output is
886886
# a text table, not JSON. Skipping the global add here lets that
887887
# command-specific format work without an argparse conflict.
888+
# Issue #59 Phase 3: ``graphml`` emits a GraphML 1.0 XML document for
889+
# graph-producing commands (scan/trace/impact/circular); other commands
890+
# produce a single-node placeholder so the format is always valid.
888891
if "format" not in existing_dests:
889-
# Issue #62 Phase 1: ``affected`` command uses ``-f`` for
890-
# ``--filter``. Avoid the ``-f`` shortcut clash by only adding
891-
# the global ``-f`` shortcut when the command doesn't already
892-
# claim it. The long form ``--format`` is always safe.
893-
format_args = ["--format"]
894-
if "-f" not in existing_option_strings:
895-
format_args.append("-f")
896-
sub.add_argument(*format_args, choices=["json", "markdown", "ai", "sarif", "compact"], default=None,
897-
help="Output format: json, markdown, ai (normalized schema), sarif (GitHub/VS Code), or compact (token-efficient single-char keys)")
892+
sub.add_argument("--format", "-f", choices=["json", "markdown", "ai", "sarif", "compact", "graphml"], default=None,
893+
help="Output format: json, markdown, ai (normalized schema), sarif (GitHub/VS Code), compact (token-efficient single-char keys), or graphml (GraphML 1.0 XML for graph-producing commands)")
898894

899895
# Add AI-optimized flags to subparser ONLY if the command doesn't already have them
900896
if "top" not in existing_dests:
@@ -927,8 +923,8 @@ def main():
927923
# Global format option (works before subcommand)
928924
# Default: "ai" if CODELENS_AI_MODE is set (for AI consumers), else "json"
929925
_default_format = "ai" if os.environ.get("CODELENS_AI_MODE", "").lower() in ("1", "true", "yes") else "json"
930-
parser.add_argument("--format", "-f", choices=["json", "markdown", "ai", "sarif", "compact"], default=_default_format,
931-
help=f"Output format (default: {_default_format}. Set CODELENS_AI_MODE=1 for ai default. compact = token-efficient single-char keys)")
926+
parser.add_argument("--format", "-f", choices=["json", "markdown", "ai", "sarif", "compact", "graphml"], default=_default_format,
927+
help=f"Output format (default: {_default_format}. Set CODELENS_AI_MODE=1 for ai default. compact = token-efficient single-char keys. graphml = GraphML XML for graph-producing commands)")
932928
parser.add_argument("--db-path", default=None,
933929
help="Custom path for SQLite database (default: .codelens/codelens.db)")
934930
# Issue #157: --diff-base <ref> restricts analysis to files changed
@@ -975,11 +971,11 @@ def main():
975971
arg = sys.argv[i]
976972
if arg in ('-f', '--format') and i + 1 < len(sys.argv):
977973
next_arg = sys.argv[i + 1]
978-
if next_arg in ('json', 'markdown', 'ai', 'sarif', 'compact'):
974+
if next_arg in ('json', 'markdown', 'ai', 'sarif', 'compact', 'graphml'):
979975
global_format = next_arg
980-
elif arg.startswith('-f=') and arg[3:] in ('json', 'markdown', 'ai', 'sarif', 'compact'):
976+
elif arg.startswith('-f=') and arg[3:] in ('json', 'markdown', 'ai', 'sarif', 'compact', 'graphml'):
981977
global_format = arg[3:]
982-
elif arg.startswith('--format=') and arg[9:] in ('json', 'markdown', 'ai', 'sarif', 'compact'):
978+
elif arg.startswith('--format=') and arg[9:] in ('json', 'markdown', 'ai', 'sarif', 'compact', 'graphml'):
983979
global_format = arg[9:]
984980
elif arg == '--top' and i + 1 < len(sys.argv):
985981
try:

scripts/formatters/__init__.py

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -189,7 +189,13 @@ def _normalize_to_ai(data: Any, command: str = "") -> Dict[str, Any]:
189189

190190
def format_output(data: Any, format_type: str = "json", command: str = "",
191191
workspace: str = "") -> str:
192-
"""Format output data as JSON, Markdown, AI (normalized schema), SARIF, or Compact."""
192+
"""Format output data as JSON, Markdown, AI (normalized schema), SARIF, Compact, or GraphML.
193+
194+
GraphML (issue #59 Phase 3) emits a GraphML 1.0 XML document for
195+
graph-producing commands (scan, trace, impact, circular). Non-graph
196+
commands produce a single-node placeholder graph so the format is
197+
always valid XML.
198+
"""
193199
if format_type == "ai":
194200
normalized = _normalize_to_ai(data, command)
195201
return json.dumps(normalized, indent=2, ensure_ascii=False)
@@ -201,12 +207,11 @@ def format_output(data: Any, format_type: str = "json", command: str = "",
201207
if format_type == "compact":
202208
from formatters.compact import format_compact
203209
return format_compact(data, command, workspace)
204-
# Default: JSON — stamp schema_version so raw JSON consumers also get the
205-
# version signal (issue #5). Mutates a copy so the caller's dict is not
206-
# modified in place (raw JSON should reflect what the engine returned,
207-
# plus the version stamp).
208-
if isinstance(data, dict):
209-
stamped = dict(data)
210-
stamp_schema_version(stamped)
211-
return json.dumps(stamped, indent=2, ensure_ascii=False)
210+
if format_type == "graphml":
211+
# Issue #59 Phase 3 — GraphML export for graph-producing commands
212+
# (scan/trace/impact/circular). Non-graph commands emit a single-node
213+
# placeholder so the format is always valid, never raises.
214+
from formatters.graphml import format_graphml
215+
return format_graphml(data, command, workspace)
216+
# Default: JSON
212217
return json.dumps(data, indent=2, ensure_ascii=False)

0 commit comments

Comments
 (0)