Skip to content

Phase 5: Go, Rust, Python, JavaScript and Java - #31

Merged
ARCoder181105 merged 15 commits into
mainfrom
phase-5/polyglot-extraction
Aug 22, 2026
Merged

Phase 5: Go, Rust, Python, JavaScript and Java#31
ARCoder181105 merged 15 commits into
mainfrom
phase-5/polyglot-extraction

Conversation

@ARCoder181105

@ARCoder181105 ARCoder181105 commented Aug 22, 2026

Copy link
Copy Markdown
Owner

Extraction and same-file resolution for five more languages, and a language boundary the resolver cannot cross by accident.

What changed

internal/ts becomes internal/extract. The per-file loop was already language-agnostic — walk, hash, split lines, three capture passes, number overloads — so the four things that were not moved into a Spec: the grammar, the .scm, how a member call's receiver is read, and what an import binds. Adding a language is now a Spec, a .scm, and a fixture.

files.language is the spec's name rather than a constant, so .tsx reports tsx — the value now says which grammar actually read the file, which is precisely what the .tsx bug was invisible for.

Extensions files.language Resolution group Cross-file exact?
.ts / .tsx / .js / .jsx / .mjs / .cjs typescript / tsx / javascript / jsx ecmascript yes
.go go go no
.rs rust rust no
.py python python no
.java java java no

JavaScript came along because the TypeScript spec already knew how to read it and .js/.jsx were being skipped entirely. Java came along because it is the first language here with genuine overloads, which is what overload_index was always for.

The boundary

byName and byPkgName were repo-wide and keyed on name alone, so a call in main.go would have matched a same-named function in main.py. Both are now keyed by resolution group, package_path included — a polyglot repository puts main.go and main.py in the same directory. Partitioned at index-build time rather than filtered at lookup, so there is no code path that can reach a foreign-language candidate and forget to check.

Imports are recorded for every language but dropped at index time for the ones whose specifiers name no file here. Go, Rust, Python and Java resolve through package clauses, crate paths, sys.path and the classpath; entering the import rule for them would answer unresolved for a module it cannot reach, throwing away the name_match the later rules still have.

Limits found by dumping the tree, not by assuming

Each is pinned by an assertion, because a parser that quietly produces less reads as one that worked.

  • GoMap[int](xs), one type argument, parses as type_conversion_expression: the same shape as int(x). Not captured. Two or more type arguments are unambiguous and are.
  • Rust — a macro body is a token_tree. Nothing inside println!("{}", helper()) is parsed as an expression.
  • Java — choosing between sync() and sync(int) needs argument types, so a call to an overloaded method resolves unresolved.
  • Python — nothing structural. Decorators, f-string interpolations, comprehension clauses and await are all parsed, and decorated_definition wraps the definition rather than replacing it.

Two bugs the exit test found in itself

The polyglot fixture is one directory with a file per language, each defining and calling helper.

helper alone proves nothing — it exists in seven files, so ambiguity answers unresolved whether the partition works or not. main.go therefore calls python_only and main.py calls go_only: names unambiguous everywhere, so the only thing stopping them is the partition.

And the assertions first compared ResolutionGroup(caller) with ResolutionGroup(callee) — which passed with ResolutionGroup returning a constant, because both sides moved together. They compare language names from a literal set now. Verified by breaking the function on purpose and watching the tests fail. Recorded as R36.

Also in here

  • R34 closed. Nothing can be checked when zustand/persist rehydrates, so dropMissingRoots runs when the open file's function list arrives — the first moment there is anything to check against.
  • A callee whose language differs from the file being read wears a badge. Every node saying "typescript" on a TypeScript map is noise on a canvas whose three edge styles are already only a dash apart; the boundary is the part worth seeing, and it is also where the resolver stops being able to say anything exact.
  • ModuleCandidates stripped .js to find the .ts behind it and never restored it, so a specifier naming a real .js file matched nothing. Latent until .js files existed.
  • make go-run REPO=... resolved its path against services/parser rather than the repo root, so the usage documented in CLAUDE.md failed with lstat.

Verification

make test        127 api / 146 web / Go, with Postgres up
make lint
make typecheck
make go-vet
make go-run REPO=./services/parser/testdata/polyglot

Written through to Postgres as well, since the IR passing is not the same as the write path handling it: seven files.language values, and the only cross-language edge is tsx -> typescript.

Not checked here: a real polyglot public repository charted end to end in a browser. That needs a GitHub OAuth sign-in, which is yours to do — the tree's language labels, the per-node badge and Shiki highlighting for Go/Rust/Python/Java are worth a look before this is called finished.

Summary by CodeRabbit

  • New Features

    • Added code parsing support for JavaScript, JSX, Go, Rust, Python, and Java alongside TypeScript.
    • Added language badges to graph nodes and function cards for cross-language calls.
    • Added Java syntax highlighting.
    • Improved JavaScript and JSX module resolution.
  • Bug Fixes

    • Prevented call-graph links from incorrectly crossing language boundaries.
    • Removed stale canvas branches after files are reloaded or reparsed.
    • Expanded default exclusions for dependency, cache, and build directories.
  • Documentation

    • Updated supported-language, parsing, resolution, and project-phase documentation.

internal/ts becomes internal/extract. The per-file loop was already
language-agnostic -- walk, hash, split lines, three capture passes, number
overloads -- so the four things that were not move into a Spec: the grammar,
the .scm, how a member call's receiver is read, and what an import binds.

ScopeSegment replaces the node-kind switch that qualifiedName and enclosingDecl
each kept a copy of. A language now states its scope rules once, and both walks
go through it.

files.language is the spec's name rather than a constant, so .tsx reports "tsx"
-- the value says which grammar actually read the file, which is the failure
the .tsx bug was invisible for.

utils.IsSourceFile had no callers and is gone; the registry decides what is a
source file, by exact extension rather than a suffix scan.
byName and byPkgName were repo-wide and keyed on name alone, so a call in
main.go would have matched a same-named function in main.py. Both are now keyed
by resolution group, and package_path is scoped to it too -- a polyglot
repository puts main.go and main.py in the same directory.

Partitioning at index-build time rather than filtering at lookup: there is then
no code path that can reach a foreign-language candidate and forget to check.

.ts and .tsx share the one group with more than one language in it. Everything
else is its own, so the boundary holds by default for a language nobody has
added yet.

Imports are dropped at index time for languages whose specifiers name no file
here. Go, Rust, Python and Java resolve through package clauses, crate paths,
sys.path and the classpath, none of which this package models; entering rule 2
would answer unresolved for a module it cannot reach, throwing away the
name_match rule 3 still has.
tree-sitter-javascript, one grammar for both -- unlike .ts and .tsx it parses
JSX in any file. Scope rules, receivers and ESM imports are TypeScript's
unchanged: same grammar family, same node kinds.

require() is the addition, and it is a call rather than an import statement.
The .scm cannot say "only require" without a predicate the Go binding does not
evaluate, so it over-captures every single-string call argument and jsImports
returns nil for the ones that are not. The driver treats a nil symbol list as
"not an import" and drops the match.

.js and .jsx join the ECMAScript resolution group, so a .js file importing a
.ts file still resolves exact. That exposed a latent bug in ModuleCandidates:
it stripped .js to find the .ts behind it and never restored it, so a specifier
naming a real .js file matched nothing. Both are candidates now, stem first.

TestExtract_OnlyTypeScriptExtensions is replaced by a registry-driven one, so
the next language cannot leave it asserting last month's set.
Methods are named after their receiver type, so Repo.Sync never collides with a
package-level Sync -- and because ScopeSegment now gets first say on a
declaration's own name, the definition and the call site agree on it.

The .scm deliberately does not capture a generic call with one type argument.
Map[int](xs) parses as a type_conversion_expression, the same shape as int(x);
capturing it would invent a call for every conversion in the repository. Two or
more type arguments are unambiguous and do parse as a call. The fixture pins
both halves, because a limit nobody wrote down is a limit that changes by
accident.

Imports bind a qualifier, never the symbols behind it. Recorded for a later Go
resolver; the current one does not follow them, since a Go path names a module
rather than a file here.
Methods are named after the type their impl block targets, so Repo.sync never
collides with a free sync.

Calls inside a macro are not captured, and the fixture pins that rather than
wishing otherwise. println!("{}", describe(&label)) has a token_tree body:
tree-sitter does not parse expressions in it, so describe is a bare identifier
beside a token_tree. Matching identifiers there would invent a call for every
name mentioned in every macro in the repository.

Rust has no quoted import specifier -- a `use` is one nested path expression --
so Spec.Imports now takes the captured node and returns both the module and the
symbols. The driver had been reading a quoted string out of the capture and
guessing at the statement above it, which only ever worked because every
language so far quoted its specifier.
A decorated definition wraps the function_definition rather than replacing it,
so the captured name's parent is still the definition and the recorded source
is the function's rather than the decorator's. async def is an ordinary
function_definition. Both are pinned by the fixture.

f-string interpolation and comprehension clauses are parsed, so calls in them
are real calls -- unlike Rust's macro bodies. The fixture asserts each one,
since these are exactly the places a query stops matching without saying so.

Class nesting is part of the name: Repo.Nested.deep never collides with a
module-level deep.

The whole import statement is captured, because `import a.b` and
`from .m import x as y` share no node to point at. `import a.b` binds a, not
a.b, because a is the only name a call site can then write.
Every enclosing type names a method, and both hidden scopes are named too: a
method inside `new Runnable(){...}` is Repo.task.<anonymous>.run, and a lambda
body is anonymous the same way.

Java is the first language here with real overloads -- sync() and sync(int) in
one class -- so this is where overload_index stops being theoretical.
TypeScript's overload signatures parse as function_signature and were never
captured, so the post-pass had nothing to number. The resolver test asserts the
consequence: a call to sync resolves to nothing, because choosing between two
methods needs argument types this parser does not have.

Also asserts what the phase promises across all four new languages: an exact
edge outside the ECMAScript family is always same-file.
One directory, a file per language, each defining and calling a function named
helper. One directory on purpose: package_path is identical across all of them,
so a symbol table keyed on name and package alone links Go to Python.

helper alone is not enough to prove the boundary. It exists in seven files, so
ambiguity answers unresolved and the assertion passes whether the partition
works or not. main.go therefore calls python_only and main.py calls go_only --
names that are unambiguous everywhere, so the only thing stopping them is the
language partition.

The assertions compare language names from a literal set rather than asking
utils.ResolutionGroup. A test that measures the boundary with the function
under test agrees with itself: collapsing every language into one group makes
main.go resolve into main.py, and the group-based version noticed nothing.
Verified by breaking ResolutionGroup and watching these fail.

Skip-path defaults gain vendor, target, __pycache__, .venv, venv, .gradle,
.mypy_cache and .tox -- the new languages' node_modules.
A badge on every node saying "typescript" on a TypeScript map is noise on a
canvas whose three edge styles are already only a dash apart. What is worth
showing is the boundary -- which is also where the resolver stops being able to
say anything exact, so the badge and the dotted edge explain each other.

Filled in MindMap rather than buildGraph: a language belongs to a file and
buildGraph is given functions, and only the canvas knows which file the reader
opened, which is what "another language" is measured against. The tree already
carries every file's language, so it costs no request.

The card is re-measured when the badge appears. A card that grows a badge it
was not sized for truncates its own name instead, which is the same class of
bug as the "start" badge squeezing cloneRetryOptions to cloneRet...

Sidebar's empty state no longer claims TypeScript is the only language, and
Shiki learns java.
A re-parse reinserts a changed file's functions under new ids while the file
row keeps its own, so the ids zustand/persist restores can point at rows that
are gone -- and since Phase 4 a webhook does that with nobody touching the
browser.

Nothing can be checked at rehydrate, because nothing is loaded yet. The check
runs when the open file's function list arrives, which is the first moment
there is anything to check against, and that list is authoritative for branch
roots because a root is only ever opened from its card.

An expanded id deeper in the map, in another file, is left to its own query: it
404s and that branch is not drawn. Judging it here would mean fetching every
file's functions to answer a question the query already answers.
CLAUDE.md gains the language list, the Spec layout, and the two conventions
that bite: the resolver partitions rather than filters, and a test must not
decide what to allow by calling the function it is testing.

PARSING_STRATEGY closes the cross-language limitation it recorded as
unreachable, and gains a table of what each language does not capture and why.
Every row was found by dumping the tree and is pinned by a fixture assertion --
they are limits, not bugs, and the failure mode of an unrecorded one is a
parser that quietly produces less.

RISKS closes R34 and opens R36 (a self-referential test) and R37 (six languages
through one resolver, with only one language's rules modelled).
The recipe cds into services/parser, so the path documented in CLAUDE.md --
written relative to the repo root, like every other path in that file -- was
being looked up one level too deep and failed with lstat. abspath resolves it
against the Makefile's own directory and leaves an absolute path alone.
@coderabbitai

coderabbitai Bot commented Aug 22, 2026

Copy link
Copy Markdown

Review Change Stack

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Free

Run ID: 23888b85-95eb-4e55-8bda-df8190570cd1

📥 Commits

Reviewing files that changed from the base of the PR and between ad505a4 and bb1cb09.

⛔ Files ignored due to path filters (1)
  • services/parser/go.sum is excluded by !**/*.sum
📒 Files selected for processing (11)
  • docs/UI_GUIDE.md
  • services/parser/cmd/parser/main.go
  • services/parser/go.mod
  • services/parser/internal/extract/golang.go
  • services/parser/internal/extract/java.go
  • services/parser/internal/extract/javascript.go
  • services/parser/internal/extract/python.go
  • services/parser/internal/extract/rust.go
  • services/parser/internal/extract/typescript.go
  • services/parser/internal/utils/constants.go
  • services/parser/internal/utils/nodes.go
🚧 Files skipped from review as they are similar to previous changes (6)
  • services/parser/internal/extract/java.go
  • services/parser/internal/extract/rust.go
  • services/parser/internal/extract/typescript.go
  • services/parser/internal/extract/python.go
  • services/parser/internal/utils/nodes.go
  • services/parser/internal/extract/javascript.go

Included review availability: Your plan provides up to 1 included review per hour; 0 remain after this review.


📝 Walkthrough

Walkthrough

The parser now supports specification-driven extraction for TypeScript, JavaScript, Go, Rust, Python, and Java. Resolver indexes use language groups. The web canvas displays foreign-language badges and removes stale persisted roots.

Changes

Polyglot extraction and resolution

Layer / File(s) Summary
Extraction framework and language specifications
services/parser/internal/extract/*, services/parser/internal/utils/*, services/parser/queries/*, services/parser/cmd/parser/main.go
The TypeScript extractor became a language-agnostic extract package. Registered specifications provide grammars, queries, scope names, receivers, and import handling for six language families.
Language fixtures and extraction tests
services/parser/internal/extract/*_test.go, services/parser/testdata/lang/*
Tests and fixtures cover language metadata, calls, scopes, imports, closures, JSX, generics, macros, and Java overloads.
Language-group resolver
services/parser/internal/resolver/*, services/parser/testdata/polyglot/*
Resolver indexes now partition candidates by language group. ECMAScript files share a group. Go, Rust, Python, and Java remain isolated.
Canvas language metadata and stale-root cleanup
apps/web/src/components/*, apps/web/src/lib/*, apps/web/src/store/*
Graph nodes can carry foreign-language metadata. Cards resize for badges. Persisted root branches are removed when their functions no longer exist.
Documentation and workflow alignment
CLAUDE.md, DEVELOPMENT.md, PLAN.md, TASKLIST.md, docs/*, Makefile
Documentation records Phase 5 polyglot extraction, resolution limits, parser layout, fixtures, validation, and updated parser execution behavior.

Estimated code review effort: 4 (Complex) | ~60 minutes

Merge Risk: ⚪ Minimal · up to bb1cb

The PR expands language extraction and same-file resolution with documented verification; no actionable merge-blocking risk remains beyond normal checks and review.

Sequence Diagram(s)

sequenceDiagram
  participant Parser
  participant Extract
  participant Resolver
  participant MindMap
  participant FunctionCard
  Parser->>Extract: parse registered source extensions
  Extract->>Resolver: provide language-tagged functions, calls, and imports
  Resolver->>Resolver: resolve within language groups
  Resolver-->>MindMap: return graph nodes and edges
  MindMap->>FunctionCard: render localized nodes and foreign-language badges
Loading

Note

🎁 Summarized by CodeRabbit Free

Your organization is on the Free plan. CodeRabbit will generate a high-level summary and a walkthrough for each pull request. For a comprehensive line-by-line review, please upgrade your subscription to CodeRabbit Pro by visiting https://app.coderabbit.ai/settings/billing.

Comment @coderabbitai help to get the list of available commands.

go mod tidy had not been re-run since JavaScript went in, so tree-sitter-go,
-rust, -python and -java were still listed as indirect and go.sum still carried
the versions other grammars pull in transitively. CI's tidy check caught it.
Twenty-seven tree-sitter field names were still spelled inline across the seven
language specs -- "name" six times, "alias" four, and so on. They join the node
kinds in internal/utils/constants.go, in their own block: a field is not a
kind, and every grammar names its fields from the same small vocabulary, so
they are shared rather than grouped per language.

Worth having as constants for the same reason the kinds are. ChildByFieldName
returns nil for a field that does not exist, so a grammar that renames one
drops whatever read it and says nothing.

cmd/parser also spelled out the three confidence tiers a second time, next to
the constants that exist so they cannot drift from the database CHECK. It reads
utils.ConfidenceTiers now. Its two --format values become package-local
constants, since nothing outside that command reads them.

No behaviour change: the polyglot summary is identical before and after.
It was named the branch after the 3b gate, in two places. Phases 4 and 5 went
first, so the page is overdue rather than upcoming and the doc was telling the
next reader something that stopped being true two phases ago.
@ARCoder181105
ARCoder181105 merged commit 4e240ed into main Aug 22, 2026
4 checks passed
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.

1 participant