Skip to content

Latest commit

 

History

History
297 lines (270 loc) · 19.5 KB

File metadata and controls

297 lines (270 loc) · 19.5 KB

Codegraph — CLAUDE.md

Codegraph extracts dependencies between code entities (classes, functions, modules) from multi-language corpora — including non-compilable legacy — into a trait-based metamodel (FamixNG-style), and analyzes the result in TypeScript: dependency graphs, coupling, architecture, and eventually a 3D "code city" visualization (Three.js) of the code structure.

Pipeline: per-language extractors (Java/Spoon first) → versioned model.jsonl interchange files → TypeScript analyzer → reports and the city renderer.

The full implementation plan, milestones, and locked design decisions live in PLAN.md; the conceptual reference for every concept, its attributes, and its relations is METAMODEL.md — read them before structural changes.

Architecture

extractors/java/     Maven project (Spoon, noClasspath) → emits model.jsonl. JVM code
                     only. Also builds a GraalVM native binary per platform, which
                     embeds the Java API distilled from ct.sym (an image has no JVM
                     class library for ECJ to borrow).
extractors/csharp/   .NET 10 project (Roslyn, NO MSBuild: one compilation over
                     every *.cs, BCL reference pack embedded) → emits
                     model.jsonl. Published as one self-contained binary per OS.
extractors/typescript/ pnpm workspace package `codegraph-typescript` (the
                     compiler API: ONE program over every *.ts under the roots,
                     a tsconfig read for resolution options only, no build, no
                     node_modules needed) → emits model.jsonl. Runtime dependency:
                     `typescript` alone — never @codegraph/* (a boundary test).
extractors/elixir/   Mix project `codegraph_elixir` (the compiler's PARSER as a
                     library: `Code.string_to_quoted/2` + a lexical resolver + an
                     OTP module/export table baked at compile time; no compile, no
                     deps/) → emits model.jsonl. Shipped as an escript (Elixir
                     embedded, Erlang/OTP on the machine). `mix codegraph.trace`
                     writes the compiler trace `--trace` merges as `generated`
                     edges. No runtime dependency.
schemas/             Generated per-record JSON Schemas + the container contract
                     (README.md) — THE cross-language contract, committed.
packages/core/       @codegraph/core — traits, edges, language profiles (data),
                     Zod validation, JSON Schema export. Pure data + validation.
packages/analyzer/   @codegraph/analyzer — graph construction, derived indexes,
                     queries, metrics (coupling, cycles), exports. Pure computation.
packages/city/       @codegraph/city — the city MODEL: modules → districts,
                     types → buildings (dimensions from configurable metrics),
                     dependencies → roof-to-roof arrows. No placement, no
                     rendering. Pure computation.
packages/navigator/  @codegraph/navigator — the navigator MODEL: the browsable
                     tree (modules → types → operations/attributes) plus one
                     classified dependency row per base edge, carrying member,
                     provenance and anchor included. Pure computation.
packages/insights/   @codegraph/insights — the bottom-up EXPLANATION walk: units
                     (operations → types → modules), SCC-condensed walk order,
                     context packs, prompts, Merkle fingerprints, plan/run, the
                     insights STORE (a port + a SQLite adapter handed an open
                     database) and its export, the `.insights.jsonl` side-car
                     (Specy domain vocabulary). Pure computation: source, model
                     calls and the database are injected.
packages/llm/        @codegraph/llm — LlmClient + OpenRouter (SDK) + Cloudflare
                     AI Gateway (REST) implementations + a deterministic fake.
                     The ONLY package that may import @openrouter/sdk.
packages/cli/        @codegraph/cli — `codegraph` command.
packages/viz/        @codegraph/viz — Three.js code city: renders a laid-out
                     city.json artifact (its ONLY input; guard-enforced). The
                     ONLY package that may import three. Exports ONE thing,
                     `mountCityView(host)`, which navigator-ui embeds; its own
                     Vite app is the standalone page (replay/history).
packages/navigator-ui/ @codegraph/navigator-ui — React model navigator, five
                     tabs: Navigate (virtualized tree + fan-in/fan-out
                     evidence), City (the viz view, full-width, a building's
                     panel opens it in Navigate), Graph (Cytoscape+fcose
                     dependency graph, module/type modes), Cycles (the
                     artifact's precomputed tangle report), Coupling (ranked
                     metrics). Reads navigator.json + city.json (guard-enforced),
                     plus ONE lookup, `insight.json?id=…`: the selected type's or
                     module's LLM explanation, shown as an inference, never a fact.
                     It imports @codegraph/insights not even for types.
                     The ONLY package that may import react and cytoscape; the
                     city comes through @codegraph/viz, never through three.
                     Vite app, no library.
apps/desktop/        The desktop app (Tauri 2): a Cargo workspace — `discovery/`
                     (the extractor catalogue looked up on PATH/Homebrew, written as
                     the daemon's registry; pure Rust, tested anywhere) and
                     `src-tauri/` (window, menu, dialog, drag-and-drop, the
                     single-executable `codegraph` as its ONE sidecar; needs the
                     platform's WebKit to compile). No model logic in Rust; the
                     page it shows is the daemon's, with no Tauri IPC.
fixtures/            Reference corpora + expected model.jsonl snapshots.

Hard boundaries (convenience does not override architecture)

  • core, analyzer, city and navigator never import Three.js or React — they must run in Node with no DOM. viz reads the city model and navigator-ui the navigator model; neither mutates its model and neither re-derives graph facts the analyzer already computes.
  • A frontend imports its model package for TYPES ONLY. A value import drags the whole Node pipeline (→ analyzer → core, zod, node:sqlite) into the browser bundle and breaks the build. Constants a frontend needs — the artifact kind, the dependency-role vocabulary — are restated as literals and pinned equal to the package's own by a test.
  • Extractors contain no metamodel intelligence. They emit JSON conforming to schemas/ — the per-record schemas AND the container contract — and nothing else. All trait/profile/validation logic lives once, in core. An extractor in any language (Java, Go, .NET…) must be able to conform using only the published schema.
  • The interchange is line-based and closed. One JSON record per line, sections in order, identity as (m, s, d), every reference a file-scoped surrogate — so a dangling reference is unwritable, not merely reportable. A producer that cannot close a reference drops it and says so.
  • core owns the vocabulary. Trait names (TNamed, TInvocable, TAttachedTo…), edge kinds, and provenance values are canonical — never rename or alias them locally.
  • Only packages/llm talks to a model provider. @openrouter/sdk is imported in exactly one file there (a boundary test scans the workspace); insights, core, analyzer and the CLI never open a socket, and every test in the workspace runs against the fake client or a fake transport. Explanations live in <model>.insights.db and its export, the side-car <model>.insights.jsonl — never in model.jsonl/model.db.
  • The insights store is not a cache. model.db is rebuilt on any doubt because the model says everything it holds; <model>.insights.db holds what was PAID for, so it is migrated (never regenerated), a newer or foreign file is refused with its bytes untouched, and there is no silent fallback to the JSONL. decode → import → export of a side-car is the identity — a tested property; any change to the store must keep it.

Stack

  • TypeScript, strict mode, "module": "NodeNext"; Node ≥ 22; pnpm workspace
  • Zod v4 — one definition per trait yields the TS type, the runtime validator, and the JSON Schema (z.toJSONSchema())
  • Vitest + fast-check (property-based tests); tsup for builds
  • Java 17+ / Maven / Spoon / Jackson for extractors/java
  • (future) Three.js + Vite for packages/viz — Three.js must never leak into other packages' dependency graphs

Commands

pnpm install
pnpm -r build                 # build all TS packages
pnpm -r test                  # all tests (unit + property)
pnpm run gen:schemas          # regenerate schemas/*.schema.json from core (commit the result)

cd extractors/java && ./mvnw package    # Maven Wrapper — `mvn` is NOT installed
java -jar target/codegraph-java.jar --src <dir> --out model.jsonl
# the jar needs a JDK on PATH; non-interactive shells do not source sdkman:
#   export JAVA_HOME="$HOME/.sdkman/candidates/java/25.0.4-tem"
./build.sh --java --native             # GraalVM binary for THIS platform only —
                                       # native-image cannot cross-compile, so CI
                                       # builds one RID per runner
extractors/java/dist/<rid>/codegraph-java --src <dir> --out model.jsonl
#   needs nothing installed: it carries the Java API distilled from ct.sym

cd extractors/csharp && dotnet test -c Release   # needs the .NET 10 SDK (see extractors/csharp/README.md)
./build.sh --csharp                              # publish the host binary → extractors/csharp/dist/<rid>/codegraph-csharp
./build.sh --csharp --publish-all                # linux-x64/arm64, osx-x64/arm64, win-x64 — all from this host
./test.sh --csharp                               # dotnet test + the published binary must reproduce the snapshot
extractors/csharp/dist/linux-x64/codegraph-csharp --src <dir> --out model.jsonl
#   runtime-only or SDK forms (dotnet <dll>, dotnet run): docs/csharp-extractor.md

./build.sh --ts --sea                  # ONE Node single-executable of the CLI + daemon + both frontends
packages/cli/dist-sea/<rid>/codegraph --version           # for THIS platform only (the image is this Node)
node packages/cli/scripts/sea-smoke.mjs packages/cli/dist-sea/<rid>/codegraph   # its three gates (test.sh runs it)

pnpm --filter @codegraph/desktop tauri:dev     # the desktop shell (after --sea): copies the image as the
#   sidecar, then `tauri dev` — needs Rust + the platform's WebKit (apps/desktop/README.md)
cd apps/desktop && cargo test -p codegraph-discovery   # the discovery crate alone, no WebKit needed
node scripts/homebrew/render.mjs --version X.Y.Z --sums SHA256SUMS [--npm-sha256 HEX] --out tap
#   the Homebrew cask + formulae the release job pushes to defsquare/homebrew-tap (docs/distribution.md);
#   `node --test scripts/homebrew` pins the templates to the release's asset names

./bin/codegraph-typescript --src <dir> --out model.jsonl   # after `pnpm -r build`; no other toolchain
./bin/codegraph-typescript --src packages --src extractors/typescript --out codegraph.jsonl   # self-hosting
#   docs/typescript-extractor.md; `test.sh --ts` checks the built bundle against fixtures/typescript

./build.sh --elixir                                 # mix escript.build → extractors/elixir/dist/codegraph-elixir
./bin/codegraph-elixir --src <dir> --out model.jsonl   # needs Erlang/OTP 27+ on PATH (or ~/.local/share/beam)
./bin/codegraph-elixir --src <dir> --deps <dir>/deps --trace <dir>/codegraph-trace.jsonl --out model.jsonl
#   docs/elixir-extractor.md; `test.sh --elixir` = mix test + the escript must reproduce fixtures/elixir
#   the trace, inside the project: ERL_FLAGS="-pa <repo>/extractors/elixir/_build/dev/lib/codegraph_elixir/ebin" mix codegraph.trace

./bin/codegraph analyze model.jsonl --report deps  # after `pnpm -r build`
./bin/codegraph serve model.jsonl                  # navigator + City tab at http://localhost:4177
#   binds EVERY interface by default; --host 127.0.0.1 keeps it local
./bin/codegraph serve --app --data-dir DIR --extractors registry.json   # the desktop app's daemon (M14a)
#   loopback, ephemeral port, ONE stdout line {"port","token"}; every route under /<token>/;
#   POST /jobs {src} runs a registry entry (or opens a model.jsonl) and builds the page;
#   exits when stdin closes. registry.json = [{ name, path, extensions: [".java"], launch?, env? }]
./bin/codegraph city model.jsonl --layout --out city.json        # the artifacts alone
./bin/codegraph navigator model.jsonl --out navigator.json
./bin/codegraph explain model.jsonl --src DIR --dry-run   # the walk plan, no call
./bin/codegraph explain model.jsonl --src DIR --estimate --price-in 0.10 --price-out 0.60   # tokens in/out + cost, no call
OPENROUTER_API_KEY=… ./bin/codegraph explain model.jsonl --src DIR [--max-calls N] [--yes]
#   [--model SLUG] (default openai/gpt-5.6-luna) [--rollup-model SLUG] (types+modules; defaults to --model)
#   prints the estimate on stderr and asks [y/N] first; --yes (required without a TTY) skips it
#   or CLOUDFLARE_API_TOKEN=… CLOUDFLARE_ACCOUNT_ID=… [--provider cloudflare]
#   → model.insights.db (the working copy: one commit per finished unit) and its export
#     model.insights.jsonl, both beside the model; re-runs redo only what changed
./bin/codegraph explain model.jsonl --export               # the side-car from the store (after a killed run); no model read
./bin/codegraph explain model.jsonl --import FILE [--yes]  # REPLACE the store's records with a side-car's; asks first
./bin/codegraph insights model.jsonl                       # the store's summary: records, concepts, what is owed, spend
./bin/codegraph insights model.jsonl --concept aggregate   # a list (also --level, --min/--max-confidence, --limit)
./bin/codegraph insights model.jsonl --id ID               # one explanation whole; --failures, --runs, --json
#   reads <model>.insights.db and NOTHING else (not the model, no provider); writes nothing
./bin/codegraph serve model.jsonl --insights FILE          # the explanation panel's store, if not beside the model

Metamodel invariants (violating these is a bug, not a style choice)

  1. Traits, not hierarchy. An entity is {id, kind, traits[], ...trait keys}. Never introduce an entity class hierarchy; capabilities compose as traits. The canonical test: a Clojure fn-var is TNamed + TStructural + TInvocable.
  2. Provenance on every edge, always one of declared | derived | dynamic-candidate | generated. Never mix facts and inferences — an analysis that needs facts only filters on declared.
  3. Evidence everywhere: entities and edges carry anchor {file, span}.
  4. Outgoing edges only. Inverse indexes — incoming invocations, subtypes, importers, and children (the inverse of the stored parent) — are derived in memory by the analyzer, never serialized. v1 files still carry children; M6 drops the key while TWithChildren stays a declared trait.
  5. Containment ≠ attachment. TChildOf/TWithChildren = where it is written; TAttachedTo = what it semantically belongs to. Distinct, both kept.
  6. Stub discipline: external types are degraded isStub nodes; their edges are kept. Internal-only view = filter stubs. Membership is decided by a whitelist of corpus-declared ids — never by package/name prefix (Spoon noClasspath invents plausible FQNs).
  7. Identity is the natural key (lang, module, symbol, disambiguator?). A rendered id (lang:module/symbol#disambiguator, from core's renderId) is a display projection and is never parsed — v1 files carry only the rendered form, so the analyzer compares those strings as opaque tokens; from M6 the key is carried structurally and compared component-wise. / and # are reserved in the key's components so rendering stays injective (two keys can never collide into one id); encoding-level surrogates are not identity and never leave the file that assigns them.
  8. Profiles are data. A language profile must be specifiable without being implemented. Validation: required ⊆ traits ⊆ required ∪ optional per kind.
  9. Import graph is the first-class layer — the only one comparable across all languages. Cross-language analyses use the intersection of the profiles involved.
  10. Graph closure is a tested property: no edge, parent, or child may point to an unknown id (stubs count as known). Enforced by the property suite, not by convention.

Code city visualization rules (when packages/viz exists)

  • Meaning controls appearance. Every visual channel (height, footprint, color, glow) maps to a documented metric (LOC, fan-in, kind, provenance…). Color is semantic — never reused for aesthetics alone; no decorative effects.
  • The city renders the model, honestly. Derived/dynamic-candidate edges must be visually distinguishable from declared facts. Never draw a relationship the model does not contain.
  • Per-frame paths allocate nothing. Reuse vectors, colors, materials; instanced meshes for buildings. Visual richness does not permit GC pressure in the render loop.
  • Judge visible work at user-facing camera angles — review screenshots of the actual render, not coordinates in code.

Engineering rules

Red-Green TDD is mandatory, properties as contract

  • Every bug fix starts with the smallest deterministic failing test.
  • The property suite (closure, no self-reference from !== to, provenance set, profile validity, deterministic sorted output) runs against every extractor output — it is the acceptance gate for any new extractor.
  • Extractor cross-validation: when two extractors cover one language, the richer one (Spoon) is the oracle; the other's edge set must be a subset. Any gap is a missed resolution case, not noise.
  • Never move a knob to keep an assertion green — when a test breaks because the model improved, restate it as the durable property it guards.

Verify the deliverable

  • Before handing off: pnpm -r test, pnpm -r build, typecheck green; for the Java extractor, ./mvnw package + schema validation of its output.
  • Verify new code is actually imported, constructed, and called — an unwired subsystem is not delivered.
  • If schemas/ changed, it was regenerated (pnpm run gen:schemas) and committed together with the core change that caused it.
  • For visual changes: exercise them in the browser and read the resulting screenshot before claiming they work.

Style

  • Strict typing throughout; make state ownership visible.
  • Comments explain constraints and non-obvious invariants, not narration. Keep per-function comments to 1–3 lines.
  • Language-specific extraction quirks (e.g. "Spoon invents FQNs in noClasspath") belong as documented notes in the language profile, not as tribal knowledge in comments.

Git

  • Conventional Commits (feat:, fix:, docs:, refactor:, chore:, test:), subject < 50 chars, present tense; scope by package when useful (feat(core): …, feat(extractor-java): …).
  • Describe what the commit actually did — inspect the diff before naming it.
  • One logical change per commit/PR. Never amend or force-push without an explicit request.