Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 43 additions & 0 deletions .github/skills/harden-tree-sitter-parsers/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
---
name: harden-tree-sitter-parsers
description: Harden new or changed tree-sitter parsers against adversarial nesting, malformed files, partial-state leaks, and stale non-source cache inputs.
reliability: validated-2x
---

# Harden Tree-sitter Parsers

Use this skill for new parser implementations, parser reviews, recursion failures,
or changes to AST traversal and linking.

## Required workflow

1. Inventory every AST traversal helper and classify it as iterative or recursive.
2. Replace source-depth recursion with an explicit stack or queue. Preserve traversal
order deliberately and avoid repeated tuple/list copying where practical.
3. Extract each file into isolated temporary state. Merge entities, edges, imports,
packages, and pending links only after the file succeeds.
4. Log skipped files with the failure class; do not silently discard valid siblings.
5. Add adversarial fixtures deeper than `sys.getrecursionlimit()` for every distinct
traversal shape. Each fixture must be parsed beside a valid sibling file.
6. Test cache invalidation for manifests or configuration that changes graph identity.
7. Run, in order:
- focused parser and cache tests;
- Ruff and the full test suite;
- a large real repository for the target language;
- arcade-agent self-analysis before/after, reporting metric and smell deltas.
8. Record any newly discovered reusable failure class in `docs/BUG_CATALOG.md`.

## Acceptance invariants

- No `RecursionError` for valid tree-sitter AST depth within the configured file limit.
- One malformed or adversarial file cannot erase healthy sibling entities.
- No partial entities from a failed file enter the final graph.
- No dangling edges, missing method owners, or duplicate package membership.
- Relevant non-source inputs invalidate cached graphs.
- Correctness and explicit failure behavior take precedence over cosmetic metric gains.

## Evidence

- Kotlin follow-up `b7effc5`: iterative deep-expression traversal and sibling survival.
- Rust PR #18: iterative path/use/module/type traversal, transactional file extraction,
Cargo-aware cache invalidation, and adversarial regression matrix.
4 changes: 2 additions & 2 deletions .github/workflows/architecture-analysis-reusable.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ on:
description: Released arcade-agent package version to install, or "source" to install the checked-out repository (self-dogfooding).
required: false
type: string
default: "0.1.1"
default: "0.2.0"
install-extras:
description: Extras string appended to the arcade-agent package install.
required: false
Expand All @@ -24,7 +24,7 @@ on:
type: string
default: "."
language:
description: Optional language override (java, python, typescript, c, go, kotlin).
description: Optional language override (java, python, typescript, c, go, kotlin, rust).
required: false
type: string
default: ""
Expand Down
26 changes: 18 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,10 @@ Provides composable tools for parsing source code, recovering architecture, dete
## Install

```bash
pip install -e ".[dev]"
pip install -e ".[dev,languages]"

# With MCP server support (for AI agent integration)
pip install -e ".[mcp,dev]"
pip install -e ".[mcp,dev,languages]"
```

## Quick Start
Expand Down Expand Up @@ -130,6 +130,16 @@ smell burden, or another architectural pressure.
- TypeScript/JavaScript (full support)
- Go (full support)
- Kotlin (structural support via optional `[languages]` extra; import + inheritance graph)
- Rust (structural support for structs, enums, unions, traits, type aliases, functions, methods,
imports, qualified references, trait inheritance/implementations, and Cargo
workspaces). Rust unit tests live inline, so with `exclude_tests=True` (the
default) `#[cfg(test)]` modules are left out of the graph as well; pass
`exclude_tests=False` to `ingest`/`parse`/`analyze` to keep them.

Tree-sitter parser changes follow the
[adversarial hardening workflow](.github/skills/harden-tree-sitter-parsers/SKILL.md).
Recurring parser failure classes are recorded in the
[living bug catalog](docs/BUG_CATALOG.md).

## Example: ARCADE Core

Expand Down Expand Up @@ -251,17 +261,17 @@ jobs:
issues: write
pull-requests: write
steps:
- uses: lemduc/arcade-agent/actions/analyze@v0.1.1
- uses: lemduc/arcade-agent/actions/analyze@v0.2.0
with:
arcade-agent-version: "0.1.1"
arcade-agent-version: "0.2.0"
```

Common optional inputs:

```yaml
- uses: lemduc/arcade-agent/actions/analyze@v0.1.1
- uses: lemduc/arcade-agent/actions/analyze@v0.2.0
with:
arcade-agent-version: "0.1.1"
arcade-agent-version: "0.2.0"
source-path: "."
language: ""
primary-algorithm: pkg
Expand All @@ -270,7 +280,7 @@ Common optional inputs:
```

For reproducible CI, keep `arcade-agent-version` pinned to a released package
version such as `"0.1.1"`. Avoid `latest` in shared CI because a new package
version such as `"0.2.0"`. Avoid `latest` in shared CI because a new package
release can change analyzer behavior without a workflow review.

The action stores the baseline as a GitHub Actions artifact on
Expand Down Expand Up @@ -318,7 +328,7 @@ arcade-agent ports and extends the capabilities of the original [ARCADE](https:/
| 6 quality metrics | Done | RCI, TurboMQ, BasicMQ, IntraConnectivity, InterConnectivity, TwoWayPairRatio |
| Balanced architecture score | Done | Derived reporting score combining core metrics, principle signals, and smell burden |
| A2A architecture comparison | Done | Hungarian algorithm on Jaccard similarity |
| Multi-language parsing | Done | Java, Python, C/C++, TypeScript/JavaScript, Go (full); Kotlin (structural) |
| Multi-language parsing | Done | Java, Python, C/C++, TypeScript/JavaScript, Go (full); Kotlin, Rust (structural) |
| 5 export formats | Done | HTML, DOT, JSON, RSF, Mermaid |
| LLM concern extraction | Done | Claude CLI for semantic BCO/SPF detection |
| MCP server | Done | Expose tools to AI agents via Model Context Protocol with session store |
Expand Down
8 changes: 5 additions & 3 deletions ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,9 @@ Handle real-world polyglot monorepos.
- [x] **15. TypeScript/JS parser** — Shipped in #8 (`parsers/typescript.py`).
- [x] **16a. Go parser** — Shipped alongside TS/JS in #8 (`parsers/go.py`).
- [x] **16a2. Kotlin parser** — Shipped (`parsers/kotlin.py`) for JVM/Kotlin-first repos (e.g. embabel-agent).
- [ ] **16b. Rust parser** — Still open. High-demand language for agent-assisted development.
- [x] **16b. Rust parser** — Tree-sitter parser for Rust modules, types, traits, functions,
methods, imports, qualified references, trait relationships, and Cargo workspaces
(`parsers/rust.py`).
- [x] **17. Incremental parsing** — Content-hash extract cache shipped in #9 (`incremental.py`), wired for the Python parser only; extending to the other two-pass parsers is follow-up.
- [ ] **18. Cross-language dependency tracking** — Java↔Python via gRPC, TS frontend↔Java backend, etc.

Expand All @@ -60,7 +62,7 @@ Work everywhere agents work.

| Priority | Items | Rationale |
|----------|-------|-----------|
| **Done** | 1–9, 12, 13, 14, 15, 16a, 17 | Phases 1–2 + TS/JS & Go parsers, incremental parsing (Python), `diff_impact`, `context_for_task`, `api_surface` |
| **Done** | 1–9, 12, 13, 14, 15, 16a, 16a2, 16b, 17 | Phases 1–2 + TS/JS, Go, Kotlin & Rust parsers, incremental parsing (Python), `diff_impact`, `context_for_task`, `api_surface` |
| **Now** | 10 | Architectural changelog |
| **Next** | 11, 16b | Component ownership, Rust parser |
| **Next** | 11 | Component ownership |
| **Then** | 18–22 | Cross-language tracking, ecosystem breadth |
4 changes: 2 additions & 2 deletions actions/analyze/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ inputs:
arcade-agent-version:
description: Released arcade-agent package version to install.
required: false
default: "0.1.1"
default: "0.2.0"
python-version:
description: Python version used for analysis.
required: false
Expand All @@ -23,7 +23,7 @@ inputs:
required: false
default: "."
language:
description: Optional language override (java, python, typescript, c, go, kotlin).
description: Optional language override (java, python, typescript, c, go, kotlin, rust).
required: false
default: ""
repo-name:
Expand Down
50 changes: 50 additions & 0 deletions docs/BUG_CATALOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# Parser Robustness Bug Catalog

Reliability: `validated-2x`

This is the living catalog for parser failure classes that can abort or distort
whole-repository analysis. Entries use reproducible fixtures and design-time
prevention rules so the same defect is not rediscovered language by language.

## Design-time checklist

- Traverse untrusted AST depth with explicit stacks or queues, never Python recursion.
- Put every file extraction behind a transactional boundary: publish its entities only
after extraction succeeds.
- Test nesting deeper than `sys.getrecursionlimit()` for every traversal shape.
- Pair each poisoned input with a healthy sibling file and assert the sibling survives.
- Track non-source inputs such as manifests when they affect graph identity or cache keys.
- Do not add per-parser input caps (file size, node counts) as a stand-in for robustness:
they drop real code silently, diverge from the other parsers, and never fix the
traversal defect they appear to mitigate. The per-file exception boundary is the backstop.
- Run focused tests, the full suite, a large real repository, and arcade-agent's own
self-analysis before publishing parser changes.

## 1. Kotlin deep-expression traversal aborted repository analysis

- **Symptom:** A machine-generated expression with thousands of nested parentheses
raised `RecursionError`; valid sibling files disappeared because parsing aborted.
- **Root cause:** Recursive AST descent treated source nesting as trusted call-stack depth.
- **Detection:** Parse a deeply nested Kotlin file beside a valid file and assert the
valid entity remains in the graph.
- **Fix:** Replace recursive descent with explicit stacks and isolate failures per file.
- **Prevention:** Apply the parser hardening skill to every new or materially changed
tree-sitter traversal.
- First encountered: Kotlin parser follow-up `b7effc5`.
- **Pattern note:** First confirmed instance of cross-language AST depth fragility.

## 2. Rust path/use/module/type traversals repeated the recursion defect

- **Symptom:** Roughly 1,000 nested path segments, use groups, inline modules, or type
wrappers raised `RecursionError` and killed analysis for healthy sibling files.
- **Root cause:** Four helpers used recursive descent even though `_references` already
demonstrated the safe iterative pattern; extraction also ran outside the file-level
exception boundary.
- **Detection:** Parameterize all four AST shapes above the Python recursion limit and
parse each beside a valid Rust file.
- **Fix:** Use explicit LIFO worklists, publish per-file extraction state transactionally,
and log-and-skip unexpected file-level failures.
- **Prevention:** Require the shared adversarial matrix and self-dogfood before parser PRs.
- First encountered: Rust parser PR #18 review, 2026-07-21.
- **Pattern note:** Second confirmed cross-language instance. Keep the class on the
design checklist; wait for a third instance before naming a broader meta-pattern.
2 changes: 1 addition & 1 deletion examples/workflows/arcade-agent-analysis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ on:
# Copy this file to .github/workflows/arcade-agent-analysis.yml in any repository
# that should run arcade-agent architecture analysis.
env:
ARCADE_AGENT_VERSION: "0.1.1"
ARCADE_AGENT_VERSION: "0.2.0"
PYTHON_VERSION: "3.12"
INSTALL_EXTRAS: "[languages]"
SOURCE_PATH: "."
Expand Down
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ languages = [
"tree-sitter-c>=0.23.0",
"tree-sitter-cpp>=0.23.0",
"tree-sitter-kotlin>=1.1.0",
"tree-sitter-rust>=0.23.0",
]
mcp = [
"mcp[cli]>=1.0",
Expand Down
10 changes: 7 additions & 3 deletions src/arcade_agent/algorithms/concern.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,17 @@

log = logging.getLogger(__name__)

CONCERN_OVERLOAD_ENTITY_THRESHOLD = 20
CONCERN_OVERLOAD_HIGH_ENTITY_THRESHOLD = 40
CONCERN_OVERLOAD_MIN_INTERNAL_EDGES_PER_ENTITY = 0.4


def detect_concern_overload(
architecture: Architecture,
dep_graph: DependencyGraph,
threshold: int = 20,
high_threshold: int = 40,
min_internal_edges_per_entity: float = 0.4,
threshold: int = CONCERN_OVERLOAD_ENTITY_THRESHOLD,
high_threshold: int = CONCERN_OVERLOAD_HIGH_ENTITY_THRESHOLD,
min_internal_edges_per_entity: float = CONCERN_OVERLOAD_MIN_INTERNAL_EDGES_PER_ENTITY,
) -> list[dict]:
"""Detect components with too many responsibilities.

Expand Down
51 changes: 43 additions & 8 deletions src/arcade_agent/cache.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,12 @@ def _cache_dir(project_root: Path) -> Path:
return project_root / _CACHE_DIR


def cache_key(source_path: str, language: str | None, files: list[str] | None) -> str:
def cache_key(
source_path: str,
language: str | None,
files: list[str] | None,
exclude_tests: bool = True,
) -> str:
"""Compute a cache key from source path, language, and file mtimes.

The key is a SHA-256 hash of the sorted file paths and their modification
Expand All @@ -31,6 +36,9 @@ def cache_key(source_path: str, language: str | None, files: list[str] | None) -
source_path: Root directory of the project.
language: Language being parsed (or None for auto-detect).
files: Specific files to parse, or None to discover all.
exclude_tests: Whether inline test code is excluded. Parsers that honor
it (Rust) produce a different graph for the same files, so it must
take part in the key.

Returns:
A hex digest string usable as a cache filename.
Expand All @@ -39,17 +47,44 @@ def cache_key(source_path: str, language: str | None, files: list[str] | None) -
hasher = hashlib.sha256()
hasher.update(str(root).encode())
hasher.update((language or "auto").encode())
hasher.update(b"tests:excluded" if exclude_tests else b"tests:included")

if files:
file_paths = sorted(files)
file_paths = set(files)
else:
# Hash all source-like files under root
file_paths = sorted(str(f) for f in root.rglob("*") if f.is_file() and f.suffix in {
".java", ".py", ".c", ".cpp", ".h", ".hpp", ".ts", ".tsx", ".js", ".jsx",
".go", ".kt", ".kts",
})

for fp in file_paths:
file_paths = {
str(f)
for f in root.rglob("*")
if f.is_file()
and f.suffix
in {
".java",
".py",
".c",
".cpp",
".h",
".hpp",
".ts",
".tsx",
".js",
".jsx",
".go",
".kt",
".kts",
".rs",
}
}

tracks_rust = language in {None, "rust"} or any(
Path(file_path).suffix == ".rs" for file_path in file_paths
)
if tracks_rust:
file_paths.update(
str(manifest) for manifest in root.rglob("Cargo.toml") if manifest.is_file()
)

for fp in sorted(file_paths):
p = Path(fp)
hasher.update(fp.encode())
if p.exists():
Expand Down
2 changes: 1 addition & 1 deletion src/arcade_agent/ci/run_self_analysis.py
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ def main() -> None:
parser.add_argument(
"--language",
default="",
help="Optional language override (java, python, typescript, c, go, kotlin)",
help="Optional language override (java, python, typescript, c, go, kotlin, rust)",
)
parser.add_argument(
"--repo-name",
Expand Down
5 changes: 5 additions & 0 deletions src/arcade_agent/parsers/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,11 @@
except ImportError:
pass

try:
import arcade_agent.parsers.rust # noqa: F401
except ImportError:
pass

from arcade_agent.parsers.base import LanguageParser, get_parser

__all__ = ["LanguageParser", "get_parser"]
12 changes: 12 additions & 0 deletions src/arcade_agent/parsers/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,18 @@
class LanguageParser(ABC):
"""Abstract base class for language-specific parsers."""

def __init__(self, exclude_tests: bool = True) -> None:
"""Create a parser.

Args:
exclude_tests: Whether test code should be kept out of the graph.
File-level exclusion happens in ``ingest``; this flag lets a
parser additionally drop *inline* test constructs that live in
production files (e.g. Rust's ``#[cfg(test)] mod tests``).
Parsers for languages without inline tests ignore it.
"""
self.exclude_tests = exclude_tests

@property
@abstractmethod
def language(self) -> str:
Expand Down
Loading
Loading