Skip to content

feat(parser): add Rust support (roadmap #16b) — reland of #18 + #29 - #39

Merged
lemduc merged 3 commits into
mainfrom
feat/rust-parser-reland
Aug 10, 2026
Merged

feat(parser): add Rust support (roadmap #16b) — reland of #18 + #29#39
lemduc merged 3 commits into
mainfrom
feat/rust-parser-reland

Conversation

@lemduc

@lemduc lemduc commented Aug 10, 2026

Copy link
Copy Markdown
Owner

Relands the Rust parser from #18 and the cfg(test) hardening from #29 on top of
current main, with the reviewed defects fixed. Rebuilt on a fresh branch rather
than merged: #18's tools/parse.py diff predates the polyglot rewrite in #19 and
would revert it.

Original contribution by @tuannx (Tony Nguyen), carried as a co-author trailer
on every commit.

What landed

The parsersrc/arcade_agent/parsers/rust.py and its registration, the
tree-sitter-rust dependency in the [languages] extra, and 36 tests. All AST
traversal is iterative and each file is extracted transactionally, so one
adversarial file cannot erase healthy siblings.

The O(n²) fix (blocker). add_entity guarded the per-package entity list with
if fqn not in package_entities — a linear scan of a list that grows to tens of
thousands of entries — and the cross-file merge repeated the same test in a
generator expression. A companion set per package makes both O(1). The set is
reset alongside the per-file packages dict, so no state leaks between files.

5.2 MB generated single-package .rs before after
wall clock 70.2 s 3.0 s (23.4×)
peak RSS 339 MB 344 MB
entities 79,500 79,500
edges 63,600 63,600

Counts are identical — this is a pure complexity fix.

Complete #[cfg(test)] exclusion. Matching only the literal cfg(test) text
on an outer attribute of an item with a body left four shapes leaking. A probe
crate with 2 production structs and 10 test-only entities:

before after
entities 10 2
test entities leaked 8 0
Prod.imports ['std::collections::HashMap', 'mockall::predicate::Eq'] ['std::collections::HashMap']

The four newly-closed shapes, each with a test:

  • compound predicates — the cfg predicate tree is now evaluated instead of
    string-compared, so cfg(all(test, ...)) and cfg(any(test, ...)) match.
    cfg(not(test)) correctly stays production (covered by its own test).
  • inner #![cfg(test)] on a file or module body — previously invisible
    because only attribute_item was inspected. Such a file now yields zero
    entities.
  • out-of-line #[cfg(test)] mod helpers;helpers.rs was later parsed as
    an independent production file. Files are now visited in
    declaring-module-first order so the module's backing path can be excluded.
  • #[cfg(test)] use ... — gave every production entity in the file a
    phantom import of mockall/proptest/rstest and inflated its fan-out.

Wiring that main's restructure needs. All three gaps were confirmed real:

  • exclude_tests never reached a parser — it only drove path filtering, so
    _parse_one left every parser on its default and inline exclusion was dead on
    arrival. analyze now forwards its flag to parse too.
  • Both cache_key call sites now pass exclude_tests, which changes the graph
    for an identical file list. .rs joins the tracked suffixes and Cargo
    manifests are hashed for Rust parses (crate names and module layout come from
    Cargo.toml, which no .rs mtime reflects). The manifest probe matches the
    composite keys source.parse actually passes, e.g. rust|<exclusions>.
  • The Cargo [workspace] probe now runs before _LANG_PREFERRED_ROOTS in
    _detect_source_root. On the new three-crate fixture
    (tests/fixtures/rust_workspace/: root + crates/alpha + crates/beta),
    main ingested 1 of 3 .rs files; it now ingests 3 of 3.

Docs and process files. Rust added to the language lists in the reusable
workflow, the analyze action, the MCP docstrings, the self-analysis CLI help, and
README/ROADMAP prose. Version defaults are main's — no 0.1.10.2.0 bump.

Deliberately left out

The recover.py scope creep (commit 86fb591 on codex/rust-parser, which
changes _refine_facade_groups and imports CONCERN_OVERLOAD_ENTITY_THRESHOLD
from algorithms/concern.py). Its blast radius is 9/11,419 entities (0.08%)
across six non-Rust corpora, and its only real effect is silencing arcade-agent's
own concern-overload smell by relocating five tool entry points, while shifting
this repo's own RCI +9.9% and TurboMQ +10.9%. Rust does not depend on it. It
remains unlanded on codex/rust-parser for a separate decision.

No input size cap. #18's commit message claimed "no other parser has a file
size cap" — that is false: parsers/go.py:20 and parsers/typescript.py:26 both
define _MAX_FILE_BYTES = 1_000_000. Rust ships without one because it no longer
needs one, not because the others lack one. docs/BUG_CATALOG.md carried the same
wrong claim and is corrected here; it now says a cap is a legitimate performance
tool for genuinely non-human-authored input but never a substitute for fixing the
algorithm. Two new reusable failure classes are recorded (quadratic membership
tests; annotation-gated test exclusion leaking through its rarer shapes).

Verification

  • pytest517 passed. ruff check src/ tests/ — clean. No
    tests/test_mcp_e2e.py errors (branching off main picks up the mcp[cli]<2 cap).
  • SKILL.md step 8 mandates a large real repository. Ripgrep: 92 files, 0.2 s,
    2,623 entities. Tokio (a Cargo workspace): 505 files, 0.8 s, 4,345 entities.
    Against the old parser those were 2,662 and 4,516 — the 39 and 171 differences
    are the cfg(test) entities that used to leak.
  • arcade-arch-diff — similarity 0.98, "No architectural changes since the
    baseline", RCI +0.02 and TurboMQ +0.01. The Tools concern-overload smell
    stays, as expected given recover.py was excluded. The baseline is refreshed
    automatically by arch-drift.yml on push to main.

Closes #29. #18 left open for the maintainer to close with a pointer here.

🤖 Generated with Claude Code

lemduc and others added 3 commits August 10, 2026 23:12
Tree-sitter parser for Rust modules, types, traits, functions, methods,
imports, qualified references, trait inheritance/implementations, and Cargo
workspaces. All AST traversal is iterative and each file is extracted
transactionally, so one adversarial file cannot erase healthy siblings.

Two defects found while relanding the original contribution are fixed here.

Linear entity indexing. `add_entity` guarded the per-package entity list with
`if fqn not in package_entities` — a linear scan of a list that grows to tens
of thousands of entries — and the cross-file merge repeated the same test in a
generator expression. A companion `set` per package makes both O(1). On a
5.2 MB generated single-package file this is 70.2s -> 3.0s (23x) with
identical entity (79,500) and edge (63,600) counts. The membership set is
reset alongside the per-file `packages` dict so no state leaks between files.

Complete `#[cfg(test)]` exclusion. Rust unit tests live inline, so path-based
exclusion never sees them, and matching only the literal `cfg(test)` text on
an outer attribute of an item with a body left four shapes leaking. A probe
crate with 2 production structs and 10 test-only entities yielded 10 entities
(8 test-only) before and yields exactly the 2 production structs now:

  * compound predicates — the cfg predicate tree is evaluated rather than
    string-compared, so `cfg(all(test, ...))` and `cfg(any(test, ...))` match
    while `cfg(not(test))` correctly stays production;
  * inner `#![cfg(test)]` on a file or module body, previously invisible
    because only `attribute_item` was inspected;
  * out-of-line `#[cfg(test)] mod helpers;`, whose backing `helpers.rs` was
    later parsed as an independent production file — files are now visited
    in declaring-module-first order so the module path can be excluded;
  * `#[cfg(test)] use ...`, which gave every production entity in the file a
    phantom import of mockall/proptest/rstest and inflated its fan-out.

No input size cap is added. `go.py` and `typescript.py` both keep a 1 MB
`_MAX_FILE_BYTES` for minified and vendored bundles; Rust does not need one
now that the quadratic index is gone.

Co-Authored-By: Tony Nguyen <tuannx87@gmail.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…rkspaces

Three wiring gaps kept the Rust parser's inline test exclusion from ever
taking effect through the public tools.

`exclude_tests` never reached a parser. It only drove *path* filtering during
discovery, so `_parse_one` left every parser on its default and inline
`#[cfg(test)]` exclusion was dead on arrival. `get_parser` returns a fresh
instance per call, so setting the flag there cannot leak between calls.
`analyze` now forwards its own `exclude_tests` to `parse` as well.

`cache_key` ignored `exclude_tests`. Inline exclusion changes the graph for an
*identical* file list, so the explicit `files=` path could return a cached
graph of the wrong shape. The flag now takes part in the key, `.rs` joins the
tracked suffixes, and Cargo manifests are hashed for Rust parses — crate names
and module layout come from `Cargo.toml`, which no `.rs` mtime reflects. The
manifest probe matches composite language keys such as `rust|<exclusions>`,
which is what `source.parse` actually passes.

`_detect_source_root` narrowed Cargo workspaces to the root crate's `src`.
On the new three-crate fixture that ingested 1 of 3 files; the `[workspace]`
probe now runs before the generic source-root candidates and returns the
workspace root, ingesting all 3.

Co-Authored-By: Tony Nguyen <tuannx87@gmail.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Adds Rust to the supported-language lists across the README, ROADMAP, the
reusable analysis workflow, the analyze action, the MCP tool docstrings, and
the self-analysis CLI help.

Ports the contributor process files from the original Rust contribution, with
one claim corrected: `docs/BUG_CATALOG.md` asserted that per-parser input caps
"diverge from the other parsers", but `parsers/go.py` and
`parsers/typescript.py` both define `_MAX_FILE_BYTES = 1_000_000`. The rule now
says what it means — a cap is a legitimate performance tool for input that is
not human-authored, but never a substitute for fixing the underlying
algorithm.

Records two new reusable failure classes: quadratic membership tests on
de-duplicated ordered collections, and annotation-gated test exclusion leaking
through its less common syntactic shapes.

Co-Authored-By: Tony Nguyen <tuannx87@gmail.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

🤖 Architecture Analysis Summary

Powered by arcade-agent — automatic architectural self-analysis


📈 Metric Evolution

Baseline commit: 6b0dd9a

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

Metric Baseline Current Change
BalancedArchitectureScore 0.6126 0.6156 🟢 ↑ (+0.0030)
📦 Components 11 11 → (no change)
🧩 Entities 138 143 🟡 ↑ (+5.0000)
🔗 Edges 136 141 🟡 ↑ (+5.0000)
🏷️ Classes 27 32 🟡 ↑ (+5.0000)
ƒ Functions 311 330 🟡 ↑ (+19.0000)
🔧 Methods 34 39 🟡 ↑ (+5.0000)
RCI 0.5441 0.5603 🟢 ↑ (+0.0162)
TurboMQ 2.6232 2.6327 🟢 ↑ (+0.0095)
BasicMQ 0.2385 0.2393 🟢 ↑ (+0.0008)
IntraConnectivity 0.0175 0.0154 🟡 ↓ (-0.0021)
InterConnectivity 0.0519 0.0512 🟢 ↓ (-0.0007)
TwoWayPairRatio 0.0000 0.0000 → (no change)
DependencyHealth 0.9715 0.9718 🟢 ↑ (+0.0003)
ComponentBalance 0.4769 0.4723 🔴 ↓ (-0.0046)
HubBalance 0.5000 0.5000 → (no change)
BoundaryClarity 0.9545 0.9545 → (no change)
DependencyDistribution 0.6294 0.6294 → (no change)
SmellDiscipline 0.9455 0.9455 → (no change)
PrincipleAlignmentScore 0.8517 0.8513 🔴 ↓ (-0.0004)

🏛️ Current Architecture

Metric Value
📦 Components 11
🧩 Entities 143
🔗 Edges 141
🏷️ Classes 32
ƒ Functions 330
🔧 Methods 39
Balanced Score 🟡 0.6156 (Fair)
Principle Alignment 0.8513
RCI 0.5603
TurboMQ 2.6327
BasicMQ 0.2393
IntraConnectivity 0.0154
InterConnectivity 0.0512
TwoWayPairRatio 0.0000
DependencyHealth 0.9718
ComponentBalance 0.4723
HubBalance 0.5000
BoundaryClarity 0.9545
DependencyDistribution 0.6294
SmellDiscipline 0.9455

🧭 Principle Signals

Signal Value
AcyclicDependencies 1.0000
LayeringHealth 0.9789
ResponsibilityFocus 0.9455
InterfaceSegregation 1.0000
ComponentBalance 0.4723
HubBalance 0.5000
BoundaryClarity 0.9545
DependencyDistribution 0.6294
SmellDiscipline 0.9455

🎯 Score Drivers

Biggest risks

  • ComponentBalance: gap=0.5277 (signal=0.4723)
  • HubBalance: gap=0.5000 (signal=0.5000)
  • DependencyDistribution: gap=0.3706 (signal=0.6294)

Strongest areas

  • AcyclicDependencies: gap=0.0000 (signal=1.0000)
  • InterfaceSegregation: gap=0.0000 (signal=1.0000)
  • LayeringHealth: gap=0.0211 (signal=0.9789)

🕸️ High-Level Design

graph TD
    Algorithms["Algorithms\n47 entities\n10 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"]
    Display["Display\n1 entities\n0 classes / 0 methods"]
    Exporters["Exporters\n17 entities\n1 classes / 0 methods"]
    Incremental["Incremental\n2 entities\n1 classes / 2 methods"]
    Parsers["Parsers\n26 entities\n16 classes / 32 methods"]
    Serialization["Serialization\n8 entities\n0 classes / 0 methods"]
    Source["Source\n7 entities\n1 classes / 1 methods"]
    Tools["Tools\n23 entities\n3 classes / 1 methods"]
    Cache --> Serialization
    Ci --> Algorithms
    Ci --> Display
    Ci --> Exporters
    Ci --> Serialization
    Ci --> Tools
    Serialization --> Algorithms
    Serialization --> Parsers
    Source --> Cache
    Source --> Parsers
    Tools --> Algorithms
    Tools --> Exporters
    Tools --> Source
Loading
🏗️ Components breakdown
Component Entities Classes Methods
Algorithms 47 10 3
Parsers 26 16 32
Tools 23 3 1
Exporters 17 1 0
Serialization 8 0 0
Source 7 1 1
Ci 5 0 0
Cache 4 0 0
Budget 3 0 0
Incremental 2 1 2
Display 1 0 0

🚨 Architectural Smells

Severity Type Affected Components
🟡 medium Concern Overload Tools

📈 Evolution vs Baseline

Baseline commit: 6b0dd9a

Architecture-to-Architecture (A2A) Comparison

Metric Value
A2A Similarity 0.9825
Matched Components 11
Components Added 0
Components Removed 0
Component matching details

Matched:

Baseline Current Similarity
Algorithms Algorithms 1.0000
Budget Budget 1.0000
Cache Cache 1.0000
Ci Ci 1.0000
Display Display 1.0000
Exporters Exporters 1.0000
Incremental Incremental 1.0000
Serialization Serialization 1.0000
Source Source 1.0000
Tools Tools 1.0000
Parsers Parsers 0.8077
High-level component statistics
Status Baseline Current Similarity Entities Classes Methods
matched Algorithms Algorithms 1.0000 47 → 47 (0) 10 → 10 (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 Display Display 1.0000 1 → 1 (0) 0 → 0 (0) 0 → 0 (0)
matched Exporters Exporters 1.0000 17 → 17 (0) 1 → 1 (0) 0 → 0 (0)
matched Incremental Incremental 1.0000 2 → 2 (0) 1 → 1 (0) 2 → 2 (0)
matched Parsers Parsers 0.8077 21 → 26 (+5) 11 → 16 (+5) 27 → 32 (+5)
matched Serialization Serialization 1.0000 8 → 8 (0) 0 → 0 (0) 0 → 0 (0)
matched Source Source 1.0000 7 → 7 (0) 1 → 1 (0) 1 → 1 (0)
matched Tools Tools 1.0000 23 → 23 (0) 3 → 3 (0) 1 → 1 (0)
Before/After Mermaid diagrams

Baseline

graph TD
    Algorithms["Algorithms\n47 entities\n10 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"]
    Display["Display\n1 entities\n0 classes / 0 methods"]
    Exporters["Exporters\n17 entities\n1 classes / 0 methods"]
    Incremental["Incremental\n2 entities\n1 classes / 2 methods"]
    Parsers["Parsers\n21 entities\n11 classes / 27 methods"]
    Serialization["Serialization\n8 entities\n0 classes / 0 methods"]
    Source["Source\n7 entities\n1 classes / 1 methods"]
    Tools["Tools\n23 entities\n3 classes / 1 methods"]
    Cache --> Serialization
    Ci --> Algorithms
    Ci --> Display
    Ci --> Exporters
    Ci --> Serialization
    Ci --> Tools
    Serialization --> Algorithms
    Serialization --> Parsers
    Source --> Cache
    Source --> Parsers
    Tools --> Algorithms
    Tools --> Exporters
    Tools --> Source
Loading

Current

graph TD
    Algorithms["Algorithms\n47 entities\n10 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"]
    Display["Display\n1 entities\n0 classes / 0 methods"]
    Exporters["Exporters\n17 entities\n1 classes / 0 methods"]
    Incremental["Incremental\n2 entities\n1 classes / 2 methods"]
    Parsers["Parsers\n26 entities\n16 classes / 32 methods"]
    Serialization["Serialization\n8 entities\n0 classes / 0 methods"]
    Source["Source\n7 entities\n1 classes / 1 methods"]
    Tools["Tools\n23 entities\n3 classes / 1 methods"]
    Cache --> Serialization
    Ci --> Algorithms
    Ci --> Display
    Ci --> Exporters
    Ci --> Serialization
    Ci --> Tools
    Serialization --> Algorithms
    Serialization --> Parsers
    Source --> Cache
    Source --> Parsers
    Tools --> Algorithms
    Tools --> Exporters
    Tools --> Source
Loading
Component dependency delta
Status Source Target
matched No dependency delta -

💡 CI/CD Insights

  • Quality Score: 🟡 Fair (BalancedArchitectureScore=0.6156)
  • Principle Alignment: 0.8513 (higher means cleaner layering, focus, and boundaries)
  • Top Risk Driver: ComponentBalance (signal=0.4723)
  • Trend: ➡️ Stable architectural quality
  • Architecture Stability: 🟢 High (A2A=0.9825)
  • Smells: ⚠️ 1 smell(s) — review suggested

📄 View HTML reports and artifacts


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

@github-actions

Copy link
Copy Markdown
Contributor

Architecture Drift Report

Algorithm: PKG | Entities: 143 | Components: 11

Drift from Baseline

Metric Baseline Current Delta
Components 11 11 +0
Similarity 0.98
BalancedArchitectureScore 0.61 0.62 ⚪ +0.00
PrincipleAlignmentScore 0.85 0.85 ⚪ -0.00
RCI 0.54 0.56 🟢 +0.02
TurboMQ 2.62 2.63 🟢 +0.01
BasicMQ 0.24 0.24 ⚪ +0.00
IntraConnectivity 0.02 0.02 ⚪ -0.00
InterConnectivity 0.05 0.05 ⚪ -0.00
TwoWayPairRatio 0.00 0.00 ⚪ +0.00
DependencyHealth 0.97 0.97 ⚪ +0.00
ComponentBalance 0.48 0.47 ⚪ -0.00
HubBalance 0.50 0.50 ⚪ +0.00
BoundaryClarity 0.95 0.95 ⚪ +0.00
DependencyDistribution 0.63 0.63 ⚪ +0.00
SmellDiscipline 0.95 0.95 ⚪ +0.00

Architectural changes — baselinecurrent

No architectural changes since the baseline.

Components

Component Entities Responsibility
Algorithms 47 Entities in algorithms
Parsers 26 Entities in parsers
Tools 23 Entities in tools
Exporters 17 Entities in exporters
Serialization 8 Entities in serialization
Source 7 Entities in source
Ci 5 Entities in ci
Cache 4 Entities in cache
Budget 3 Entities in budget
Incremental 2 Entities in incremental
Display 1 Entities in display

Architecture Diagram

graph LR
    Algorithms["Algorithms"]
    Budget["Budget"]
    Cache["Cache"]
    Ci["Ci"]
    Display["Display"]
    Exporters["Exporters"]
    Incremental["Incremental"]
    Parsers["Parsers"]
    Serialization["Serialization"]
    Source["Source"]
    Tools["Tools"]
    Cache --> Serialization
    Ci --> Algorithms
    Ci --> Display
    Ci --> Exporters
    Ci --> Serialization
    Ci --> Tools
    Serialization --> Algorithms
    Serialization --> Parsers
    Source --> Cache
    Source --> Parsers
    Tools --> Algorithms
    Tools --> Exporters
    Tools --> Source
Loading

Smells (1)

  • Concern Overload: Tools
    • 💡 Split the overloaded component into smaller, focused units. Consider extracting sub-packages or introducing an interface layer.

Generated by arcade-agent

@lemduc
lemduc merged commit d2bb44b into main Aug 10, 2026
7 checks passed
@lemduc
lemduc deleted the feat/rust-parser-reland branch August 10, 2026 16:16
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