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
87 changes: 87 additions & 0 deletions docs/design/0254-symbols-overview.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
# Design Doc: Token-efficient symbols overview fast-path

> **Status:** Accepted
> **Date:** 2026-07-13
> **Author:** Claude (direct implementation — user directive)
> **Related issues:** #254
> **Related PRs:** (this PR)

---

## Problem

Gap-analysis vs Serena MCP: Serena's `get_symbols_overview` gives "a hierarchical
map of top-level symbols in a file, allowing agents to understand structure WITHOUT
reading every line" — token-efficient onboarding.

CodeLens has `context --check outline` but: (a) flat per-file output, no workspace-wide
1-call overview; (b) outline re-reads `outline.json` (cached JSON), not the live
`graph_nodes` SQLite table; (c) paginated at 20 files/call — an agent needing "what
lives in each file across 200 files" requires 10 round trips.

## Goal

One call returns compact per-file symbol map (name + kind + line) for a workspace or
specific file — no re-parse, no LSP, data already in `graph_nodes`.

## Implementation

### New Files
- `scripts/commands/symbols_overview.py` — queries `graph_nodes` via sqlite3 directly.
Groups by file, filters to meaningful kinds (function/method/class/module/route/type/
interface/struct/enum/trait), sorts by line. No parser import at runtime.

### Modified Files
- `scripts/commands/context.py` — registered `overview` in `_CHECKS`, added
`_build_namespace` branch (file filter + max_files), updated epilog + examples,
added `--max-files` argument.
- `tests/test_command_registry.py` — `symbols_overview` added to implementation-module
allowlist.

### Not Changed
- `graph_nodes` schema — read-only; no migration needed.
- `outline_engine.py` / `outline.py` — unchanged; overview is a separate fast-path,
not a replacement.

## Output Shape

```json
{
"status": "ok",
"stats": {"total_files": 45, "total_symbols": 312, "truncated": false},
"overview": {
"scripts/commands/audit.py": [
{"name": "add_args", "kind": "function", "line": 83},
{"name": "execute", "kind": "function", "line": 201}
]
}
}
```

## Token Efficiency

Per-file: overview ~1100 chars vs outline ~1600 chars (31% smaller). Workspace-wide:
200 files in 1 call vs outline --all paginating 20 files/call. The key gain is
call-count reduction, not per-symbol byte savings.

## Why a new `_CHECKS` entry, not a flag on outline

- Outline reads `outline.json` (per-file cached) via `outline_engine`; overview reads
`graph_nodes` directly. Different data source = different module.
- Avoids coupling the outline code path to DB-presence logic.
- Follows the pattern of `diagnostics` (#253) and `css` (#251) — thin wrapper per sub-check.

## Alternatives Considered

- **`--detail minimal` on outline.** Rejected — outline's minimal still reads the per-file
cache, doesn't support workspace-wide single-call, and can't naturally express "no body,
just name+kind+line".
- **Compact string format** (`"42:fn:handleAuth"`). Considered — would further reduce tokens
but breaks JSON consumers that iterate `name`/`kind`/`line` keys. The `{"name","kind","line"}`
dict is a small cost for API stability.

## Testing

6 pytest unit tests: no-registry graceful degradation, symbol grouping, file filter,
noise-kind exclusion, max_files truncation, no-reparse invariant (asserts tree-sitter
never imported). All pass.
12 changes: 12 additions & 0 deletions scripts/commands/context.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,10 @@
"module": "commands.diagnostics",
"help": "LSP lint/errors/warnings for a file (issue #253, needs --file)",
},
"overview": {
"module": "commands.symbols_overview",
"help": "Token-efficient hierarchical symbols map from registry (issue #254)",
},
}

ALL_CHECKS = list(_CHECKS.keys())
Expand All @@ -69,13 +73,16 @@
" trace Deep call chain from a symbol\n"
" orient 10-second codebase orientation brief\n"
" diagnostics LSP lint/errors/warnings for a file (needs --file, issue #253)\n"
" overview Token-efficient hierarchical symbols map (issue #254)\n"
"\n"
"Examples:\n"
" codelens context . # orient (default)\n"
" codelens context . --check outline --file src/app.ts\n"
" codelens context . --check trace --name handleAuth\n"
" codelens context . --check context --name handleAuth\n"
" codelens context . --check diagnostics --file src/app.ts\n"
" codelens context . --check overview # workspace symbol map\n"
" codelens context . --check overview --file src/auth.ts\n"
)
parser.add_argument("workspace", nargs="?", default=None,
help="Path to workspace root (auto-detected if omitted)")
Expand Down Expand Up @@ -104,6 +111,8 @@
help="trace/outline: pagination offset")
parser.add_argument("--timeout", type=float, default=None,
help="diagnostics: seconds to wait for LSP to push diagnostics (default 3.0)")
parser.add_argument("--max-files", type=int, default=None, dest="max_files",
help="overview: max files in workspace-wide mode (default: 200)")


def _parse_checks(check_arg: str) -> List[str]:
Expand All @@ -121,7 +130,7 @@
return parts or ["orient"]


def _build_namespace(base_args, check_name: str) -> argparse.Namespace:

Check failure on line 133 in scripts/commands/context.py

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

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

See more on https://sonarcloud.io/project/issues?id=Wolfvin_CodeLens&issues=AZ9bGeCqxi9ITghehk4R&open=AZ9bGeCqxi9ITghehk4R&pullRequest=264
ns = argparse.Namespace()
for attr in ("format", "top", "max_tokens", "lite", "deep", "db_path",
"diff_base", "diff_scope", "disable_suppression",
Expand Down Expand Up @@ -160,6 +169,9 @@
elif check_name == "diagnostics":
ns.file = getattr(base_args, "file", None)
ns.timeout = getattr(base_args, "timeout", None) or 3.0
elif check_name == "overview":
ns.file = getattr(base_args, "file", None)
ns.max_files = getattr(base_args, "max_files", None) or 200
return ns


Expand Down
124 changes: 124 additions & 0 deletions scripts/commands/symbols_overview.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
# @WHO: scripts/commands/symbols_overview.py
# @WHAT: Token-efficient hierarchical symbols map from graph_nodes (issue #254)
# @PART: commands
# @ENTRY: execute()
"""symbols_overview — hierarchical top-level symbols fast-path (issue #254).

Queries ``graph_nodes`` in the already-built SQLite registry (no re-parse,
no LSP) and returns a compact per-file map of top-level symbols:
name + kind + line

Intended use: agent onboarding — understand "what lives in each file" without
reading every line. Token cost is <<1% of outline-full.

Registered as ``context --check overview``.
"""

import os
import sqlite3
from collections import defaultdict
from typing import Any, Dict, List, Optional

from utils import default_db_path


# Symbol kinds to include in overview. Omit synthetic / noise kinds.
_INCLUDE_KINDS = frozenset({
"function", "method", "class", "module", "route",
"type", "interface", "struct", "enum", "trait",
})

# Max files to include when no --file filter is given.
_DEFAULT_MAX_FILES = 200


def _query_overview(
db_path: str,
file_filter: Optional[str] = None,
max_files: int = _DEFAULT_MAX_FILES,
) -> Dict[str, Any]:
"""Query graph_nodes and return compact per-file symbol map."""
if not os.path.exists(db_path):
return {
"status": "no_registry",
"note": "Run 'codelens scan <workspace>' first to build the registry.",
}

conn = sqlite3.connect(db_path)
conn.row_factory = sqlite3.Row
try:
if file_filter:
# Normalize separator for cross-platform matching
norm = file_filter.replace("\\", "/")
rows = conn.execute(
"SELECT name, node_type, file, line FROM graph_nodes "
"WHERE REPLACE(file,'\\\\','/') LIKE ? "
"ORDER BY file, line",
(f"%{norm}%",),
).fetchall()
else:
rows = conn.execute(
"SELECT name, node_type, file, line FROM graph_nodes "
"ORDER BY file, line"
).fetchall()
finally:
conn.close()

by_file: Dict[str, List[Dict]] = defaultdict(list)
for row in rows:
kind = row["node_type"] or "function"
if kind not in _INCLUDE_KINDS:
continue
# Normalize file separator
f = (row["file"] or "").replace("\\", "/")
by_file[f].append({
"name": row["name"],
"kind": kind,
"line": row["line"],
})

# Apply max_files cap (workspace-wide mode only)
files_sorted = sorted(by_file.keys())
truncated = False
if not file_filter and len(files_sorted) > max_files:
files_sorted = files_sorted[:max_files]
truncated = True

overview = {f: by_file[f] for f in files_sorted}
total_symbols = sum(len(v) for v in overview.values())

return {
"status": "ok",
"stats": {
"total_files": len(overview),
"total_symbols": total_symbols,
"truncated": truncated,
},
"overview": overview,
}


def add_args(parser):
parser.add_argument("workspace", nargs="?", default=None,
help="Path to workspace root (auto-detected if omitted)")
parser.add_argument("--file", default=None,
help="Filter to a specific file (substring match)")
parser.add_argument("--max-files", type=int, default=_DEFAULT_MAX_FILES,
dest="max_files",
help=f"Max files in workspace-wide mode (default: {_DEFAULT_MAX_FILES})")


def execute(args, workspace):
"""Return token-efficient hierarchical symbols map.

@FLOW: SYMBOLS_OVERVIEW
@CALLS: _query_overview() -> dict
@MUTATES: nothing (read-only DB query)
"""
db_path = getattr(args, "db_path", None) or default_db_path(workspace)
file_filter = getattr(args, "file", None)
max_files = getattr(args, "max_files", None) or _DEFAULT_MAX_FILES
return _query_overview(db_path, file_filter=file_filter, max_files=max_files)

# Issue #254: registered as the `overview` sub-check of the `context` umbrella
# (see commands/context.py), NOT a standalone command — count stays 12.
2 changes: 1 addition & 1 deletion tests/test_command_registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ def test_every_command_module_registers():
"export_snapshot", "git_status", "graph_schema", "import_snapshot",
"init", "lsp_status", "orient", "outline", "ownership", "perf_hint",
"query_graph", "regex_audit", "secrets", "side_effect", "smell",
"staleness", "taint", "trace", "vuln_scan",
"staleness", "symbols_overview", "taint", "trace", "vuln_scan",
}
_UTILITY_MODULES |= _DEPRECATED_ALIAS_MODULES
missing = []
Expand Down
Loading
Loading