Skip to content

feat(parser): add Rust support#18

Open
tuannx wants to merge 4 commits into
mainfrom
codex/rust-parser
Open

feat(parser): add Rust support#18
tuannx wants to merge 4 commits into
mainfrom
codex/rust-parser

Conversation

@tuannx

@tuannx tuannx commented Jul 20, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • add a tree-sitter Rust parser for structs, enums, unions, traits, type aliases, functions, methods, imports, qualified references, trait inheritance, and implementations
  • model Rust module conventions and Cargo workspaces, including member-crate prefixes and single-crate src roots
  • defer impl-owner resolution so sibling-module aliases resolve correctly while external and blanket impls do not manufacture local entities
  • wire Rust through ingest/autodetection, caching, CLI/MCP/action help, optional language dependencies, and docs

Addresses the Rust parser roadmap item tracked in #13. The status-board issue should remain open.

Accuracy review

The regression suite covers raw identifiers, unions, async/unsafe/extern functions, inline modules, super glob imports, aliases, generic owners, cfg-alternative impls, external qualified owners, blanket generic impls, Cargo workspaces, source-root detection, and cache invalidation.

Two false-positive cases found during review are explicitly guarded:

  • std::io::Error cannot resolve to an unrelated local Error
  • impl<T> Trait for &mut T cannot attach methods to an unrelated local struct T

Known static-analysis boundaries: macro-generated items are not expanded, cfg alternatives are structurally merged, and external dependency entities are intentionally not added.

Validation

  • .venv/bin/python -m pytest -q — 261 passed
  • .venv/bin/python -m ruff check src/ tests/ — passed
  • focused mypy review — no errors in parsers/rust.py (remaining findings are pre-existing in five imported modules)
  • fresh end-to-end analysis on BurntSushi/ripgrep at 227381db0ee83dfa4341f1e27ff9617c0f5ad992 (66k+ stars):
    • 86 production Rust files
    • 0 tree-sitter ERROR nodes and 0 missing nodes
    • 3,074 entities, 3,487 deduplicated edges, 12 recovered components
    • 0 missing method owners, duplicate package memberships, or dangling edges
  • wheel build verified arcade_agent/parsers/rust.py is packaged

@github-actions

github-actions Bot commented Jul 20, 2026

Copy link
Copy Markdown
Contributor

Architecture Drift Report

Algorithm: PKG | Entities: 719 | Components: 7

Drift from Baseline

Metric Baseline Current Delta
Components 7 7 +0
Similarity 0.98
BalancedArchitectureScore 0.68
PrincipleAlignmentScore 0.84
RCI 0.99
TurboMQ 0.24
BasicMQ 0.24
IntraConnectivity 0.00
InterConnectivity 0.01
TwoWayPairRatio 0.00
DependencyHealth 0.99
ComponentBalance 0.22
HubBalance 0.67
BoundaryClarity 1.00
DependencyDistribution 0.57
SmellDiscipline 0.91

Changes

  • 49 entity movement(s) between components

Smells (1)

  • Concern Overload: Tests

Generated by arcade-agent

@github-actions

github-actions Bot commented Jul 20, 2026

Copy link
Copy Markdown
Contributor

🤖 Architecture Analysis Summary

Powered by arcade-agent — automatic architectural self-analysis


📈 Metric Evolution

Baseline commit: 613a1d9

Legend: 🟢 better · 🔴 worse · 🟡 low impact · ⚪ no change

Metric Baseline Current Change
BalancedArchitectureScore 0.6193 0.6848 🟢 ↑ (+0.0655)
📦 Components 9 9 → (no change)
🧩 Entities 115 120 🟡 ↑ (+5.0000)
🔗 Edges 115 120 🟡 ↑ (+5.0000)
🏷️ Classes 22 27 🟡 ↑ (+5.0000)
ƒ Functions 271 285 🟡 ↑ (+14.0000)
🔧 Methods 34 39 🟡 ↑ (+5.0000)
RCI 0.5652 0.7417 🟢 ↑ (+0.1765)
TurboMQ 0.2493 0.3174 🟢 ↑ (+0.0681)
BasicMQ 0.2493 0.3174 🟢 ↑ (+0.0681)
IntraConnectivity 0.0182 0.0168 🟡 ↓ (-0.0014)
InterConnectivity 0.0323 0.0340 🔴 ↑ (+0.0017)
TwoWayPairRatio 0.0000 0.0000 → (no change)
DependencyHealth 0.9822 0.9813 🔴 ↓ (-0.0009)
ComponentBalance 0.5304 0.5204 🔴 ↓ (-0.0100)
HubBalance 0.5000 0.5000 → (no change)
BoundaryClarity 0.9444 0.9444 → (no change)
DependencyDistribution 0.6263 0.6049 🔴 ↓ (-0.0214)
SmellDiscipline 0.9333 1.0000 🟢 ↑ (+0.0667)
PrincipleAlignmentScore 0.8553 0.8625 🟢 ↑ (+0.0072)

🏛️ Current Architecture

Metric Value
📦 Components 9
🧩 Entities 120
🔗 Edges 120
🏷️ Classes 27
ƒ Functions 285
🔧 Methods 39
Balanced Score 🟡 0.6848 (Fair)
Principle Alignment 0.8625
RCI 0.7417
TurboMQ 0.3174
BasicMQ 0.3174
IntraConnectivity 0.0168
InterConnectivity 0.0340
TwoWayPairRatio 0.0000
DependencyHealth 0.9813
ComponentBalance 0.5204
HubBalance 0.5000
BoundaryClarity 0.9444
DependencyDistribution 0.6049
SmellDiscipline 1.0000

🧭 Principle Signals

Signal Value
AcyclicDependencies 1.0000
LayeringHealth 0.9860
ResponsibilityFocus 1.0000
InterfaceSegregation 1.0000
ComponentBalance 0.5204
HubBalance 0.5000
BoundaryClarity 0.9444
DependencyDistribution 0.6049
SmellDiscipline 1.0000

🎯 Score Drivers

Biggest risks

  • HubBalance: gap=0.5000 (signal=0.5000)
  • ComponentBalance: gap=0.4796 (signal=0.5204)
  • DependencyDistribution: gap=0.3951 (signal=0.6049)

Strongest areas

  • AcyclicDependencies: gap=0.0000 (signal=1.0000)
  • InterfaceSegregation: gap=0.0000 (signal=1.0000)
  • ResponsibilityFocus: gap=0.0000 (signal=1.0000)

🕸️ High-Level Design

graph TD
    Algorithms["Algorithms\n43 entities\n5 classes / 3 methods"]
    Budget["Budget\n3 entities\n0 classes / 0 methods"]
    Cache["Cache\n4 entities\n0 classes / 0 methods"]
    Ci["Ci\n5 entities\n0 classes / 0 methods"]
    Exporters["Exporters\n15 entities\n1 classes / 0 methods"]
    Incremental["Incremental\n2 entities\n1 classes / 2 methods"]
    Parsers["Parsers\n22 entities\n16 classes / 32 methods"]
    Serialization["Serialization\n8 entities\n0 classes / 0 methods"]
    Tools["Tools\n18 entities\n4 classes / 2 methods"]
    Cache --> Serialization
    Ci --> Algorithms
    Ci --> Exporters
    Ci --> Serialization
    Ci --> Tools
    Serialization --> Algorithms
    Serialization --> Parsers
    Tools --> Cache
    Tools --> Parsers
Loading
🏗️ Components breakdown
Component Entities Classes Methods
Algorithms 43 5 3
Parsers 22 16 32
Tools 18 4 2
Exporters 15 1 0
Serialization 8 0 0
Ci 5 0 0
Cache 4 0 0
Budget 3 0 0
Incremental 2 1 2

🚨 Architectural Smells

✅ No architectural smells detected.

📈 Evolution vs Baseline

Baseline commit: 613a1d9

Architecture-to-Architecture (A2A) Comparison

Metric Value
A2A Similarity 0.9328
Matched Components 9
Components Added 0
Components Removed 0
Component matching details

Matched:

Baseline Current Similarity
Budget Budget 1.0000
Cache Cache 1.0000
Ci Ci 1.0000
Incremental Incremental 1.0000
Serialization Serialization 1.0000
Exporters Exporters 0.9333
Algorithms Algorithms 0.9070
Tools Tools 0.7826
Parsers Parsers 0.7727
High-level component statistics
Status Baseline Current Similarity Entities Classes Methods
matched Algorithms Algorithms 0.9070 39 → 43 (+4) 5 → 5 (0) 3 → 3 (0)
matched Budget Budget 1.0000 3 → 3 (0) 0 → 0 (0) 0 → 0 (0)
matched Cache Cache 1.0000 4 → 4 (0) 0 → 0 (0) 0 → 0 (0)
matched Ci Ci 1.0000 5 → 5 (0) 0 → 0 (0) 0 → 0 (0)
matched Exporters Exporters 0.9333 14 → 15 (+1) 1 → 1 (0) 0 → 0 (0)
matched Incremental Incremental 1.0000 2 → 2 (0) 1 → 1 (0) 2 → 2 (0)
matched Parsers Parsers 0.7727 17 → 22 (+5) 11 → 16 (+5) 27 → 32 (+5)
matched Serialization Serialization 1.0000 8 → 8 (0) 0 → 0 (0) 0 → 0 (0)
matched Tools Tools 0.7826 23 → 18 (-5) 4 → 4 (0) 2 → 2 (0)
Before/After Mermaid diagrams

Baseline

graph TD
    Algorithms["Algorithms\n39 entities\n5 classes / 3 methods"]
    Budget["Budget\n3 entities\n0 classes / 0 methods"]
    Cache["Cache\n4 entities\n0 classes / 0 methods"]
    Ci["Ci\n5 entities\n0 classes / 0 methods"]
    Exporters["Exporters\n14 entities\n1 classes / 0 methods"]
    Incremental["Incremental\n2 entities\n1 classes / 2 methods"]
    Parsers["Parsers\n17 entities\n11 classes / 27 methods"]
    Serialization["Serialization\n8 entities\n0 classes / 0 methods"]
    Tools["Tools\n23 entities\n4 classes / 2 methods"]
    Cache --> Serialization
    Ci --> Algorithms
    Ci --> Exporters
    Ci --> Serialization
    Ci --> Tools
    Serialization --> Algorithms
    Serialization --> Parsers
    Tools --> Algorithms
    Tools --> Cache
    Tools --> Exporters
    Tools --> Parsers
Loading

Current

graph TD
    Algorithms["Algorithms\n43 entities\n5 classes / 3 methods"]
    Budget["Budget\n3 entities\n0 classes / 0 methods"]
    Cache["Cache\n4 entities\n0 classes / 0 methods"]
    Ci["Ci\n5 entities\n0 classes / 0 methods"]
    Exporters["Exporters\n15 entities\n1 classes / 0 methods"]
    Incremental["Incremental\n2 entities\n1 classes / 2 methods"]
    Parsers["Parsers\n22 entities\n16 classes / 32 methods"]
    Serialization["Serialization\n8 entities\n0 classes / 0 methods"]
    Tools["Tools\n18 entities\n4 classes / 2 methods"]
    Cache --> Serialization
    Ci --> Algorithms
    Ci --> Exporters
    Ci --> Serialization
    Ci --> Tools
    Serialization --> Algorithms
    Serialization --> Parsers
    Tools --> Cache
    Tools --> Parsers
Loading
Component dependency delta
Status Source Target
removed Tools Algorithms
removed Tools Exporters

Smell changes:

  • ✅ Resolved: Concern Overload

💡 CI/CD Insights

  • Quality Score: 🟡 Fair (BalancedArchitectureScore=0.6848)
  • Principle Alignment: 0.8625 (higher means cleaner layering, focus, and boundaries)
  • Top Risk Driver: HubBalance (signal=0.5000)
  • Trend: 📈 Improving modularity
  • Architecture Stability: 🟢 High (A2A=0.9328)
  • Smells: ✅ Clean — no architectural smells

📄 View HTML reports and artifacts


This comment is auto-generated by the self-dogfooding CI job. It updates on every push to this PR.

@tuannx
tuannx force-pushed the codex/rust-parser branch from 75ae540 to 9281b4e Compare July 20, 2026 04:53
@tuannx
tuannx marked this pull request as ready for review July 20, 2026 04:54
Copilot AI review requested due to automatic review settings July 20, 2026 04:54

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 first-class Rust parsing support to arcade-agent, integrating a new tree-sitter-based Rust parser into ingest/parse flows, caching, and project documentation so Rust projects (including Cargo workspaces) can be analyzed like existing languages.

Changes:

  • Introduces RustParser with module-convention handling, entity extraction (types/traits/functions/methods), and a second-pass linker for imports/references/trait relationships.
  • Extends language detection/ingest, parse caching, and CI/MCP/action help text to include rust.
  • Updates docs/roadmap and adds a Rust-focused regression test suite (including workspace + cache invalidation checks).

Reviewed changes

Copilot reviewed 13 out of 13 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
tests/test_parsers/test_rust.py Adds Rust parser regression coverage across module conventions, impl ownership, imports, and Cargo workspace behaviors.
tests/test_cache.py Ensures cache key changes when Rust source files change.
src/arcade_agent/tools/ingest.py Adds Rust file extensions and Cargo workspace-aware source-root detection.
src/arcade_agent/tools/adapters/mcp.py Updates MCP adapter docstrings/help to list Rust as a supported language override.
src/arcade_agent/parsers/rust.py New Rust tree-sitter parser + linker implementation with Cargo workspace/module modeling.
src/arcade_agent/parsers/init.py Registers Rust parser behind an optional dependency import guard.
src/arcade_agent/ci/run_self_analysis.py Updates CLI help to include Rust language override.
src/arcade_agent/cache.py Includes .rs in cache-key file hashing.
ROADMAP.md Marks the Rust parser roadmap item as shipped and describes scope.
README.md Documents Rust support and updates installation extras to include [languages].
pyproject.toml Adds tree-sitter-rust to the optional languages extra.
actions/analyze/action.yml Extends action input docs to include Rust as a language override.
.github/workflows/architecture-analysis-reusable.yml Extends reusable workflow input docs to include Rust as a language override.

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

Comment thread src/arcade_agent/tools/ingest.py
Comment thread src/arcade_agent/parsers/rust.py
Comment thread src/arcade_agent/cache.py Outdated

@lemduc lemduc left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Strong parser overall — and I want to lead with what you got right: defect classes 2–4 from the Kotlin review (function-local hoisting, cross-package leaf-fallback edges, lost inheritance/shared props) are all clean here, with regression tests. resolve() returning None for unresolved multi-segment paths, the blanket-generic-impl skip, and the cfg-alternative dedupe are exactly the right instincts. Registration surfaces are complete, and the ripgrep validation numbers in the PR body held up under independent re-run.

One blocking finding, and it's an ironic one:

1. [Blocking] The recursion defect class from the Kotlin review is back — in four places. _references is iterative (so the original deep-parens killer passes — proof you know the pattern), but these four still recurse unboundedly, and extraction runs outside the per-file try (rust.py:475-481 wraps only stat/read/parse, catching only (OSError, ValueError)):

Crash input (one ~5KB poisoned file) Site Depth
a::a::…::T path, 991 segments _path_segments (~L131) 991
use a::{a::{…}}, 991 levels _flatten_use (~L160) 991
mod m { mod m { … }}, 993 levels visit_container (~L456, mod_item) 993
impl T for &&&…&R / ((((R)))), 993 wrappers _base_type_path (~L224) 993

Each reproduces RecursionError out of RustParser().parse(), killing the entire analysis and losing the good sibling files' entities. Fix pattern is already on main from the Kotlin follow-up (b7effc5): explicit stacks. Please also widen the per-file except to Exception like kotlin.py:374 as a backstop.

2. [Decide before merge] #[cfg(test)] mod tests contents become production entities. Rust unit tests live inline in production files, and exclude_tests=True only filters test paths — on a fresh ripgrep clone, 548 of 3,184 entities (17%) and 37 phantom packages (e.g. grep_cli.escape.tests.backslash) are test items, which will surface as bogus components in recovery and skew metrics. Suggested: when exclude_tests, skip mod_items whose preceding sibling attribute is #[cfg(test)] — or at minimum document the behavior. Maintainer call since it changes graph shape.

Non-blocking:

  • _MAX_FILE_BYTES = 1_000_000 silently drops >1MB files from the graph — new behavior no other parser has, undocumented (and it doesn't mitigate the recursion crashes; the 991-segment file is ~5KB).
  • README labels Rust "(full)" while Kotlin is "(structural)" — same tier, inconsistent label.
  • rel_path at ~L483 duplicates the relative_to already computed inside the try.
  • Per-file except tuple differs from kotlin.py's — align while fixing finding 1.

Also a heads-up on land order: #19 (polyglot) structurally rewrites ingest.py, and your touches there are small — expect to rebase this onto #19 rather than the reverse; while doing so, eyeball the interaction between your Cargo-workspace root detection in _detect_source_root and #19's multilang path (which deliberately bypasses it).

Test results on the branch: 290 passed, ruff clean; deep-parens/local-types/qualified-external-leaf/macros/invalid-file adversarial runs all clean except the four recursion inputs above. Fix finding 1, decide finding 2, and this is merge-ready.

@tuannx
tuannx force-pushed the codex/rust-parser branch from 9281b4e to 55d9760 Compare July 21, 2026 12:13
@tuannx
tuannx requested a review from lemduc July 21, 2026 12:34
@tuannx

tuannx commented Jul 21, 2026

Copy link
Copy Markdown
Collaborator Author

Addressed all requested changes in 55d9760 after rebasing onto main@8090d12.

Blocking fixes

  • Replaced all four unbounded recursive walkers (_path_segments, _flatten_use, inline module traversal, and _base_type_path) with iterative stack/worklist implementations.
  • Made extraction transactional per file: a malformed or adversarial file is logged and discarded without leaking partial graph state or losing healthy sibling files.
  • Excluded inline #[cfg(test)] mod tests trees from the production graph while retaining non-test cfg modules.

Inline review fixes

  • Switched Cargo manifest reads to binary tomllib.load() with safe malformed-encoding/TOML handling in both ingest and the Rust parser.
  • Added Cargo.toml to Rust cache keys, including explicit .rs file-list analysis.
  • Added regression coverage for both behaviors; the three outdated inline threads have been replied to and resolved.

Recurrence prevention and dogfooding

  • Added depth-1,200 adversarial tests for all four recursive shapes, plus poisoned-file/healthy-sibling isolation tests.
  • A depth-5,000 qualified-path case improved from ~11.3s to ~0.008s after removing recursive/repeated traversal.
  • Re-ran against ripgrep (86 production Rust files): 2,641 entities / 2,215 edges, zero test-named entities/packages, missing owners, dangling edges, or duplicate package members.
  • Ran Arcade Agent against itself before/after: architecture graph and metrics remained stable (120 entities / 120 edges; Balanced Architecture Score 0.6229).
  • Added docs/BUG_CATALOG.md documenting the repeated Kotlin/Rust recursion defect and .github/skills/harden-tree-sitter-parsers/SKILL.md as the reusable parser-hardening workflow.

Validation

  • 306 passed
  • Ruff clean
  • mypy clean for touched source files
  • wheel contains the Rust parser
  • Python 3.12/3.13 CI and Architecture Drift Detection are green

The PR is currently MERGEABLE / CLEAN and ready for re-review.

@tuannx

tuannx commented Jul 21, 2026

Copy link
Copy Markdown
Collaborator Author

Resolved the remaining Concern Overload: Tools smell in 86fb591.

Root cause: package recovery correctly recognized thin facades by outgoing dependency affinity, but any caller from another component prevented reassignment. This left compare, compute_metrics, detect_smells, recover, and visualize in the broad Tools bucket even though each fronts a single subsystem.

The refinement now treats incoming callers as usage, not responsibility, only for oversized package buckets. Compact boundaries remain package-anchored, so focused components such as Cache are not pulled into their implementation dependencies. Regression tests cover both the oversized-bucket reassignment and compact-boundary guard.

Self-dogfood delta:

Metric Before After
Architectural smells 1 0
Tools entities 23 18
RCI 0.5833 0.7417
TurboMQ 0.2504 0.3174
BalancedArchitectureScore 0.6229 0.6848
Entities / edges / components 120 / 120 / 9 120 / 120 / 9

Validation: 308 passed, Ruff clean, and git diff --check clean. The CI self-analysis should refresh the PR report from this commit.

Addresses the remaining PR #18 review findings after 55d9760.

Finding 2 — `#[cfg(test)] mod tests` contents became production entities.
55d9760 started skipping those modules but did so unconditionally. The skip
is now gated on `exclude_tests`, which is what the reviewer asked for and
what keeps the flag honest: `ingest(exclude_tests=False)` must still yield
test entities. `exclude_tests` is a `LanguageParser` attribute (default
True, ignored by parsers without inline tests), threaded from the `parse`
tool and `analyze`, and folded into the parse cache key so the two graph
shapes cannot collide in the cache.

Non-blocking — removed `_MAX_FILE_BYTES`. It silently dropped >1MB files,
no other parser has it, and it never mitigated the recursion crashes it
appeared to guard against; the per-file `except Exception` boundary is the
real backstop. Recorded the rule in the bug catalog and corrected the
README, which had documented the cap.

Tests: cfg(test) modules kept with exclude_tests=False and dropped (with
their nested items) by default, the parse tool threading the flag, cache
key sensitivity, a >1MB file surviving, and a parenthesized-type case added
to the deep-AST matrix.

Signed-off-by: Duc Minh Le <duclm.bk@gmail.com>

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@lemduc

lemduc commented Jul 22, 2026

Copy link
Copy Markdown
Owner

Review follow-up — per-finding status

Verified each finding against the branch as it stands (three commits landed after the review: ed9c878, 55d9760, 86fb591), then implemented what was left. New commit: cbea67f.

# Finding Status Where
1 [Blocking] Unbounded recursion in _path_segments, _flatten_use, visit_container/mod_item, _base_type_path Already fixed before this pass 55d9760
1b Per-file except (OSError, ValueError)Exception backstop Already fixed 55d9760
2 [Decide before merge] #[cfg(test)] mod tests become production entities Fixed now — and see the behavior note below cbea67f
NB-1 _MAX_FILE_BYTES = 1_000_000 silent >1MB drop Fixed now — removed cbea67f
NB-2 README labels Rust "(full)" vs Kotlin "(structural)" Already fixed (both now "structural") 55d9760
NB-3 rel_path duplicates the relative_to inside the try Already fixed 55d9760
NB-4 Per-file except tuple differs from kotlin.py Already fixed (both except Exception) 55d9760

Finding 1 — verified, not just assumed

All four sites are explicit LIFO worklists now, and extraction moved inside the per-file try. I re-ran the exact crash inputs from the review table against the current code:

Input Result
a::a::…::T, 991 segments parses, 1 entity
use a::{a::{…}}, 991 levels parses, 1 entity
mod m { mod m { … }}, 993 levels parses, 2 entities
impl T for &&&…&R, 993 wrappers parses, 2 entities
impl T for ((((R)))), 993 wrappers parses, 2 entities

No RecursionError. The existing parametrized regression test already covers the first four at depth 1,200; I added the parenthesized-type shape (((((R))))) as a fifth case since it was listed in the table but not covered.

⚠️ Finding 2 — behavior change, please confirm

55d9760 already skipped #[cfg(test)] modules, but unconditionally. That silently overrode exclude_tests=False, so I gated it:

  • LanguageParser gains an exclude_tests attribute (default True), documented as "drop inline test constructs"; parsers for languages without inline tests ignore it.
  • RustParser skips a mod_item only when it carries #[cfg(test)] and self.exclude_tests. The whole subtree goes — nested mods and impl blocks inside it too.
  • The flag is threaded analyzeparse tool → parser, and folded into the parse cache key so the two graph shapes can't collide in .arcade-cache.

This changes graph shape on Rust repos — on ripgrep it removes ~548 of 3,184 entities (17%) and ~37 phantom packages from default runs. That is the intent (they are test items, and Rust unit tests are inline so path-based exclusion can never see them), but it is a visible default-behavior change and you can veto it: reverting is a one-line change to the gate. exclude_tests=False now restores the old output exactly.

Non-blocking NB-1 — chose removal over documentation

Removed _MAX_FILE_BYTES. Reasons: no other parser caps input size, it drops real code silently with only a log line, and it never mitigated the recursion crashes it looked like it was guarding — the per-file except Exception boundary is the actual backstop. The README had documented the cap; that text is corrected. Added a design-checklist bullet to docs/BUG_CATALOG.md so the pattern isn't reintroduced. Regression test asserts a >1MB file still yields its entities.

Verification

  • pytest: 314 passed (baseline before this work: 308 passed — +6 new tests)
  • ruff check src/ tests/: All checks passed
  • mypy src/: 220 errors before and after, byte-identical set in the touched files — pre-existing repo-wide, no regression from this change

Deliberately left undone

lemduc added a commit that referenced this pull request Jul 22, 2026
The merge+relink introduced for roadmap #18 was language-blind: all three
resolution paths (unique-leaf fallback in resolve_name, leaf import fallback
and exact-FQN import match in relink_edges) could bind an entity to one written
in an unrelated language. On a Python+Java fixture this fabricated
`app.service.PaymentHandler -[extends]-> com.example.core.Base` and an import
edge from a Python module to the Java class with the same FQN, which then
propagated into pkg recovery and WCA clustering.

Introduce language families (jvm = java+kotlin; every other language its own
family) and gate every resolution path plus the merge step on family
compatibility. The unique-leaf index is now built per family, so in-family
resolution is unaffected. Cross-family FQN collisions no longer drop an entity
silently: the later one is re-keyed as `<fqn>#<language>` with its edges and
package listings remapped, and collision counts are reported in the new
DependencyGraph.metadata (also surfaced in MCP summaries, since agents never
see log lines).

Single-language output is unchanged byte-for-byte (verified against
origin/main across six fixture/language combinations; metadata is omitted from
serialization when empty).

Also from the review:
- document the supported polyglot pairs (module docstring, parse/MCP
  docstrings, README table, ROADMAP)
- drop the dead leaf-split in resolve_name
- remove the scala entry from _LANG_PREFERRED_ROOTS (no Scala parser)
- filter provided files by language in the single-language branch too, with a
  warning for skipped files
- count only files (not directories) when resolving language="multi"
- sort/deduplicate an explicit languages list so ordering cannot change
  collision winners

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
lemduc pushed a commit that referenced this pull request Jul 22, 2026
Add languages=[] / language="multi" for Maven dual-root discovery and
merge+relink of import/extends/implements across Java↔Kotlin FQN space
(roadmap #18 MVP). Wire MCP and CI analyze inputs; keep first entity on
cross-language FQN collisions.

Co-authored-by: Cursor <cursoragent@cursor.com>
lemduc added a commit that referenced this pull request Jul 22, 2026
The merge+relink introduced for roadmap #18 was language-blind: all three
resolution paths (unique-leaf fallback in resolve_name, leaf import fallback
and exact-FQN import match in relink_edges) could bind an entity to one written
in an unrelated language. On a Python+Java fixture this fabricated
`app.service.PaymentHandler -[extends]-> com.example.core.Base` and an import
edge from a Python module to the Java class with the same FQN, which then
propagated into pkg recovery and WCA clustering.

Introduce language families (jvm = java+kotlin; every other language its own
family) and gate every resolution path plus the merge step on family
compatibility. The unique-leaf index is now built per family, so in-family
resolution is unaffected. Cross-family FQN collisions no longer drop an entity
silently: the later one is re-keyed as `<fqn>#<language>` with its edges and
package listings remapped, and collision counts are reported in the new
DependencyGraph.metadata (also surfaced in MCP summaries, since agents never
see log lines).

Single-language output is unchanged byte-for-byte (verified against
origin/main across six fixture/language combinations; metadata is omitted from
serialization when empty).

Also from the review:
- document the supported polyglot pairs (module docstring, parse/MCP
  docstrings, README table, ROADMAP)
- drop the dead leaf-split in resolve_name
- remove the scala entry from _LANG_PREFERRED_ROOTS (no Scala parser)
- filter provided files by language in the single-language branch too, with a
  warning for skipped files
- count only files (not directories) when resolving language="multi"
- sort/deduplicate an explicit languages list so ordering cannot change
  collision winners

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@tuannx

tuannx commented Jul 23, 2026

Copy link
Copy Markdown
Collaborator Author

Posting a self-review before merge — I verified the branch in an isolated worktree: 314 tests pass, ruff clean, mypy clean on parsers/rust.py. Three things worth resolving first, and I'd like your call on two of them, @lemduc.

1. Confirmed bug: comment between #[cfg(test)] and its module defeats test exclusion

In visit_container, pending_attributes is cleared by any non-attribute node, and tree-sitter emits comments as named children. Reproduced against this branch:

pub struct Production;
#[cfg(test)]
// unit tests for this module
mod tests { struct Fixture; }

tests.Fixture leaks into the graph despite exclude_tests=True. Stacked attributes (#[cfg(test)] + #[allow(...)]) work correctly; only interleaved comments break it.

Fix is one line — skip line_comment/block_comment nodes without clearing pending_attributes — plus a regression test in the adversarial matrix. I'll push this to the branch.

2. recover.py facade-refinement change is out of scope for this PR — your call

The change (external callers no longer pin an entity to an oversized bucket, sharing CONCERN_OVERLOAD_ENTITY_THRESHOLD from concern.py) alters pkg-algorithm recovery output for every language, not just Rust, and the PR description doesn't mention it. It has tests covering both directions and is likely what made the ripgrep components settle at 12 — but it silently shifts baselines for existing users.

Options:

  • a) keep it here but document it prominently in the PR body / changelog, or
  • b) I split it into its own PR so the behavior change gets its own review trail.

I lean (b) for changelog honesty, but happy either way — @lemduc which do you prefer?

3. Release sequencing: docs advertise Rust against 0.2.0, which doesn't contain it

The README now lists Rust support and bumps the pins to actions/analyze@v0.2.0 / arcade-agent-version: "0.2.0" — but released 0.2.0 predates this PR. A CI user following the updated README with language: rust gets KeyError: No parser for 'rust'. Same for the default in actions/analyze/action.yml and the reusable workflow.

Proposal: merge without the version-string bumps, cut 0.2.1 (or 0.3.0 given the recovery change, if it stays) right after, then bump pins in the release PR. @lemduc — OK with that, and which version number?

Minor (non-blocking, noting for the record)

  • _crate_context re-parses each crate's Cargo.toml per file — worth memoizing by directory for large workspaces.
  • cache_key with language=None now does a second full rglob for Cargo.toml on every project regardless of language; collecting manifests during the first walk would avoid the double traversal.
  • Hashing tests:excluded into every key invalidates all existing cached graphs on upgrade — one-time cost, deserves a release-note line.
  • use foo::{self as alias} produces import path ("foo", "self"), so references through the alias don't resolve. Rare; fine as a documented boundary.
  • .github/skills/harden-tree-sitter-parsers/SKILL.md and docs/BUG_CATALOG.md bring my review-process conventions into the repo (README links them). @lemduc if you'd rather keep process files out of the tree, I'll drop them from this PR.

🤖 Review generated with Claude Code

lemduc added a commit that referenced this pull request Jul 24, 2026
* feat: polyglot multilang ingest/parse with cross-language relink

Add languages=[] / language="multi" for Maven dual-root discovery and
merge+relink of import/extends/implements across Java↔Kotlin FQN space
(roadmap #18 MVP). Wire MCP and CI analyze inputs; keep first entity on
cross-language FQN collisions.

Co-authored-by: Cursor <cursoragent@cursor.com>

* test: add Java+Kotlin polyglot E2E coverage

Cover ingest→parse→recover, MCP session pipeline, and CLI
--languages self-analysis against existing mixed fixtures.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix: address Copilot multilang parse/ingest review

Detect multi-language parse from path suffixes without requiring files
on disk, and fail fast on unknown ingest language overrides.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix: complete polyglot MCP pipeline

* fix(multilang): scope cross-language merge and relink by language family

The merge+relink introduced for roadmap #18 was language-blind: all three
resolution paths (unique-leaf fallback in resolve_name, leaf import fallback
and exact-FQN import match in relink_edges) could bind an entity to one written
in an unrelated language. On a Python+Java fixture this fabricated
`app.service.PaymentHandler -[extends]-> com.example.core.Base` and an import
edge from a Python module to the Java class with the same FQN, which then
propagated into pkg recovery and WCA clustering.

Introduce language families (jvm = java+kotlin; every other language its own
family) and gate every resolution path plus the merge step on family
compatibility. The unique-leaf index is now built per family, so in-family
resolution is unaffected. Cross-family FQN collisions no longer drop an entity
silently: the later one is re-keyed as `<fqn>#<language>` with its edges and
package listings remapped, and collision counts are reported in the new
DependencyGraph.metadata (also surfaced in MCP summaries, since agents never
see log lines).

Single-language output is unchanged byte-for-byte (verified against
origin/main across six fixture/language combinations; metadata is omitted from
serialization when empty).

Also from the review:
- document the supported polyglot pairs (module docstring, parse/MCP
  docstrings, README table, ROADMAP)
- drop the dead leaf-split in resolve_name
- remove the scala entry from _LANG_PREFERRED_ROOTS (no Scala parser)
- filter provided files by language in the single-language branch too, with a
  warning for skipped files
- count only files (not directories) when resolving language="multi"
- sort/deduplicate an explicit languages list so ordering cannot change
  collision winners

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Duc Le <duclm.bk@gmail.com>
Co-authored-by: Claude Fable 5 <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.

3 participants