Skip to content

feat(graph): whole-repo LSP map (directory mode) beyond the call walk#14

Open
burrows99 wants to merge 4 commits into
masterfrom
feat/graph-repo-map
Open

feat(graph): whole-repo LSP map (directory mode) beyond the call walk#14
burrows99 wants to merge 4 commits into
masterfrom
feat/graph-repo-map

Conversation

@burrows99

Copy link
Copy Markdown
Owner

What

trace graph could only walk calls out from one --entry function. This adds a repo/directory mode that maps everything the language server reports.

Invocation Mode
trace graph (no --entry) repo map of the auto-detected project root
trace graph --entry src/domain repo map of that directory
trace graph --entry foo.ts@login / foo.ts:42 rooted call walk (unchanged)

How the map is built (everything the LSP supports)

  • Discovery (sourceFiles.ts): resolve a directory — root auto-detected from cwd/--root (nearest tsconfig/package.json/.git) — and walk it for source files, skipping node_modules/dist/.git/hidden + .d.ts, bounded by --max-files.
  • Build (LspCodeGraphProvider.repoGraph): per file documentSymbol → containment (file → class → method/property/…) + the full node-kind set; per callable callHierarchy/outgoingCallscalls edges (incl. super()); per class/interface typeHierarchy/supertypesextends/implements. Each pass is capability-guarded.
  • One unified CodeGraphmode: "rooted" | "repo", new edge kinds (contains/extends/implements), per-kind stats — so the JSON schema and the HTML force-view are reused, and the text view branches to a per-file outline (GraphView.repoMap).

New flags: --max-files, --include-external, --no-inheritance. --entry is now optional.

Honest degradation

The bundled typescript-language-server advertises no typeHierarchyProvider, so extends/implements can't be derived from the LSP on TypeScript. Rather than silently drop it, it surfaces as a visible diagnostic:

GRAPH_DEGRADED (warn): inheritance edges (extends/implements) unavailable — the 'lsp' server has no type-hierarchy support; containment + calls are still mapped

Servers that do support type hierarchy (gopls, rust-analyzer, clangd) get inheritance edges automatically. A references pass is scaffolded in the options but left off (heavy) — the first deliberate "out of bounds for now" candidate.

Sample (trace graph --entry src/domain)

repo map — domain  via lsp
  9 files · 161 symbols · 163 edges  (contains:152 · calls:11)

Target.ts
├─ class Target
│  ├─ method toReference   → calls TargetReference, trigger
├─ class NodeTarget
│  ├─ constructor constructor   → calls Target
└─ class ChromeTarget
   └─ constructor constructor   → calls Target

Verification

  • tsc --noEmit clean; full build (CLI + UI) green
  • 79/80 tests pass (1 Postgres round-trip skipped — needs a live DB), incl. new repo-map build, discovery, and GraphCommand repo-mode tests
  • manual runs over test/fixtures/codegraph and src/domain — text outline, HTML force view, and JSON envelope

Notes

🤖 Generated with Claude Code

…l walk

`trace graph` could only walk calls out from one --entry function. Add a
repo/directory mode that maps everything the language server reports:

- Discovery (`sourceFiles.ts`): resolve a directory (root auto-detected
  from cwd/--root — nearest tsconfig/package.json/.git) and walk it for
  source files, skipping node_modules/dist/.git/hidden + .d.ts, bounded
  by --max-files.
- Build (`LspCodeGraphProvider.repoGraph`): per file, `documentSymbol`
  for containment (file → class → method/property/…) and the full node
  kind set; per callable, `callHierarchy/outgoingCalls` for `calls`
  edges; per class/interface, `typeHierarchy/supertypes` for
  `extends`/`implements`. Each pass is capability-guarded.
- One unified CodeGraph (`mode: "rooted" | "repo"`, new edge kinds,
  per-kind stats), so the schema + HTML force view are reused and the
  text view branches to a per-file outline (GraphView.repoMap).

UX: `trace graph` (no --entry) or `--entry <dir>` → repo map; `--entry
file:line`/`file@symbol` → the unchanged rooted call walk. New flags:
--max-files, --include-external, --no-inheritance.

Honest degradation: the bundled typescript-language-server advertises no
typeHierarchyProvider, so extends/implements can't be derived on TS. That
is surfaced as a GRAPH_DEGRADED warn diagnostic (not silently dropped);
servers that do support type hierarchy (gopls/rust-analyzer/clangd) get
the inheritance edges. A `references` pass is scaffolded but left off
(heavy) — the first "out of bounds for now" candidate.

Verified: tsc clean, full build green, 79/80 tests pass (1 Postgres
skip), incl. new repo-map + discovery tests; manual runs over
test/fixtures/codegraph and src/domain (text + HTML + JSON).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings June 19, 2026 13:14

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a new repo/directory mode to trace graph that maps an entire directory/repo via LSP (symbols + containment + calls + optional inheritance), reusing the existing CodeGraph envelope/schema and renderers while introducing new edge kinds and diagnostics for capability-based degradation.

Changes:

  • Introduces repo discovery utilities (discoverSourceFiles, isDirectory, resolveRepoRoot) and wires them into a new LspCodeGraphProvider.repoGraph(...) build path.
  • Extends the CodeGraph model to support mode: "rooted" | "repo" plus new edge kinds and repo-mode stats, and updates text/HTML rendering accordingly.
  • Updates CLI/options + input normalization, adds GRAPH_DEGRADED, and expands integration tests for repo mode.

Reviewed changes

Copilot reviewed 10 out of 10 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
test/codegraph.test.js Adds tests for directory discovery, repoGraph output, and repo-mode rendering.
src/shared/codes.ts Adds GRAPH_DEGRADED diagnostic code for capability-based partial graphs.
src/io/InputManager.ts Makes --entry optional and introduces repo-mode normalization logic.
src/io/descriptors.ts Expands graph command input shape with repo-mode flags.
src/codegraph/sourceFiles.ts New: repo root resolution and deterministic directory walking with caps/filters.
src/codegraph/LspCodeGraphProvider.ts Adds repoGraph() and additional symbol-kind handling for repo maps.
src/codegraph/CodeGraphProvider.ts Updates graph interfaces for repo mode, new edge kinds, and stats.
src/cli/commands/GraphView.ts Adds repoMap() text renderer and adjusts HTML metadata for repo graphs.
src/cli/commands/GraphCommand.ts Orchestrates rooted vs repo builds; emits truncation + degraded diagnostics.
src/cli/Cli.ts Updates trace graph command flags/description for the new repo mode.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/cli/commands/GraphCommand.ts Outdated
Comment on lines +48 to +50
// Repo map: resolve the directory to cover — an explicit --root/dir, else the detected project root of cwd
// (nearest tsconfig/package.json/.git), so a bare `trace graph` maps the project it's run in.
const root = resolveRepoRoot(request.root ?? process.cwd());
Comment thread src/io/InputManager.ts
Comment on lines +82 to +87
maxFiles: raw.maxFiles,
includeExternal: raw.includeExternal,
inheritance: raw.inheritance,
server: raw.server,
args: { ...(entryString ? { entry: entryString } : {}), ...(raw.root ? { root: raw.root } : {}), ...(raw.server ? { server: raw.server } : {}), ...(raw.maxFiles ? { maxFiles: raw.maxFiles } : {}) },
};
Comment thread src/io/InputManager.ts
Comment on lines 94 to 98
maxDepth: raw.depth,
includeExternal: raw.includeExternal,
server: raw.server,
args: { entry: raw.entry, ...(raw.root ? { root: raw.root } : {}), ...(raw.server ? { server: raw.server } : {}), depth: raw.depth },
args: { entry: entryString, ...(raw.root ? { root: raw.root } : {}), ...(raw.server ? { server: raw.server } : {}), depth: raw.depth },
};
Comment on lines +207 to +213
const open = (uri: string): void => {
if (opened.has(uri)) return;
let text: string;
try { text = readFileSync(fileURLToPath(uri), "utf8"); } catch { return; }
client.notify(DidOpenTextDocumentNotification.type, { textDocument: { uri, languageId: languageIdFor(uri), version: 1, text } });
opened.add(uri);
};
Comment thread test/codegraph.test.js
Comment on lines +80 to +83
test("discoverSourceFiles walks a directory (extension-filtered) and resolveRepoRoot detects the project root", () => {
const found = discoverSourceFiles(ROOT, { maxFiles: 100 });
assert.deepEqual(found.files.map((f) => f.split("/").pop()).sort(), ["helper.ts", "sample.ts"]);
assert.equal(found.truncated, false);
burrows99 and others added 2 commits June 19, 2026 14:34
…, test portability

- Repo root: bare `trace graph` (no --root/dir) now detects the nearest
  project root by walking UP from cwd via findProjectRootFrom, instead of
  mapping cwd as-is — so running in a subdir maps the whole project. An
  explicit --root/dir is still honored verbatim.
- meta.args: include the flags that change the map — --include-external
  and --no-inheritance (repo), --include-external (rooted) — so a Trace is
  reproducible from its recorded invocation.
- languageId: map non-TS extensions (go/py/rs/rb/c/cpp/java/…) and fall
  back to the bare extension, not "typescript" — a wrong id can stop a
  non-TS server from parsing files at all.
- test: split discovered paths on [/\\] so the basename assertion is
  Windows-safe.

Verified: tsc clean, 79/80 tests pass (1 Postgres skip).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ne export directory

Generalize `export-skill` into `exports`: a single command that provisions
a project's export directory (.claude/skills/trace) with everything
trace-cli hands off — the bundled `trace` skill AND interactive HTML maps
of that project, built right then:

- graph.html — the whole-repo LSP map (GraphCommand repo mode)
- deps.html  — the module-import graph (DepsCommand / madge)

ExportSkillCommand → ExportCommand (now async; orchestrates the skill copy
plus the two analyses). The skill copy is the must-succeed step; each map
is best-effort — a failed/empty analysis still writes a page and is
reported via `ok`, never aborting the export. CLI prints the skill dest +
each map path.

Verified: tsc clean, 81/82 tests pass (1 Postgres skip), incl. new export
integration tests (skill copied, graph.html + deps.html written as real
HTML pages); manual `trace exports <dir>` end-to-end.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings June 19, 2026 13:49
@burrows99

Copy link
Copy Markdown
Owner Author

Added in a2e0124: a new exports command (generalizes export-skill), per follow-up discussion.

trace exports [dir] provisions a project's export directory (.claude/skills/trace) with everything in one shot:

  • the bundled trace skill (so Claude Code picks it up)
  • graph.html — the whole-repo LSP map of the project (this PR's repo mode)
  • deps.html — the module-import graph (madge)

ExportSkillCommandExportCommand (async; orchestrates the skill copy + both analyses). The skill copy is must-succeed; each map is best-effort — a failed/empty analysis still writes a page and is reported via ok, never aborting the export. It lives in this PR because it depends directly on the repo-map mode added here.

Verified: tsc clean, 81/82 tests pass (1 Postgres skip), incl. new export integration tests + manual trace exports <dir>.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 16 out of 16 changed files in this pull request and generated 6 comments.

Comment on lines +5 to +6
/** Extensions the bundled TypeScript server understands — the default scan set for a repo map. */
export const DEFAULT_SOURCE_EXTENSIONS = [".ts", ".tsx", ".mts", ".cts", ".js", ".jsx", ".mjs", ".cjs"];
Comment on lines +60 to +64
if (!IGNORED_DIRS.has(entry.name)) walk(fullPath);
} else if (entry.isFile() && !entry.name.endsWith(".d.ts") && extensions.has(extname(entry.name).toLowerCase())) {
if (files.length >= options.maxFiles) { truncated = true; return; }
files.push(fullPath);
}
Comment on lines +51 to +59
const root = request.root ? resolveRepoRoot(request.root) : findProjectRootFrom(process.cwd());
const graph = await provider.repoGraph({
root,
maxFiles: request.maxFiles ?? MAX_FILES,
maxNodes: request.maxNodes ?? MAX_NODES,
includeExternal: request.includeExternal ?? false,
inheritance: request.inheritance,
server: request.server,
});
Comment on lines +61 to +63
if (graph.stats.truncated) {
diagnostics.push(Diagnostic.warn(Code.GRAPH_TRUNCATED, `repo map truncated (${graph.stats.files} files, ${graph.stats.nodes} symbols) — narrow with --entry <subdir>, or raise --max-files`));
}
Comment on lines +52 to +55
} catch (error) {
log.warn(`${kind} map failed`, { err: String((error as Error)?.message ?? error).split("\n")[0] });
return { kind, path, ok: false };
}
Comment thread src/index.ts
Comment on lines 23 to 25
export { DoctorCommand } from "./cli/commands/DoctorCommand.js";
export { ExportSkillCommand } from "./cli/commands/ExportSkillCommand.js";
export { ExportCommand } from "./cli/commands/ExportCommand.js";
export { Cli } from "./cli/Cli.js";
…d exports

Running `exports` on this repo surfaced two issues:
- the deps map scanned a git submodule's build/ output (798 modules) —
  default an --exclude for the export's deps map covering
  node_modules/dist/build/out/cache/coverage/vendor, mirroring the repo
  graph's source-discovery ignore set, so deps.html reflects the project.
- the generated export directory (skill copy + graph.html/deps.html) is
  regenerable, not source — gitignore .claude/skills/trace/.

Test extends the export integration test: a build/ module is excluded
from deps.html (and never enters the repo graph).

Verified: tsc clean, 81/82 tests pass (1 Postgres skip).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants