Skip to content
Merged
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
17 changes: 14 additions & 3 deletions .github/workflows/architecture-analysis-reusable.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,12 @@ 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, multi).
required: false
type: string
default: ""
languages:
description: Optional comma-separated polyglot languages (e.g. java,kotlin).
required: false
type: string
default: ""
Expand Down Expand Up @@ -142,12 +147,15 @@ jobs:
env:
SOURCE_PATH: ${{ inputs.source-path }}
LANGUAGE: ${{ inputs.language }}
LANGUAGES: ${{ inputs.languages }}
REPO_NAME: ${{ inputs.repo-name }}
PRIMARY_ALGORITHM: ${{ inputs.primary-algorithm }}
FILTER_NON_ARCHITECTURAL_HELPERS: ${{ inputs.filter-non-architectural-helpers }}
run: |
ARGS=(--source "target-repo/${SOURCE_PATH}")
if [ -n "${LANGUAGE}" ]; then
if [ -n "${LANGUAGES}" ]; then
ARGS+=(--languages "${LANGUAGES}")
elif [ -n "${LANGUAGE}" ]; then
ARGS+=(--language "${LANGUAGE}")
fi
if [ -n "${REPO_NAME}" ]; then
Expand All @@ -167,12 +175,15 @@ jobs:
env:
SOURCE_PATH: ${{ inputs.source-path }}
LANGUAGE: ${{ inputs.language }}
LANGUAGES: ${{ inputs.languages }}
REPO_NAME: ${{ inputs.repo-name }}
WCA_NUM_CLUSTERS: ${{ inputs.wca-num-clusters }}
FILTER_NON_ARCHITECTURAL_HELPERS: ${{ inputs.filter-non-architectural-helpers }}
run: |
ARGS=(--source "target-repo/${SOURCE_PATH}")
if [ -n "${LANGUAGE}" ]; then
if [ -n "${LANGUAGES}" ]; then
ARGS+=(--languages "${LANGUAGES}")
elif [ -n "${LANGUAGE}" ]; then
ARGS+=(--language "${LANGUAGE}")
fi
if [ -n "${REPO_NAME}" ]; then
Expand Down
36 changes: 33 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@ Provides composable tools for parsing source code, recovering architecture, dete
```bash
pip install -e ".[dev]"

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

## Quick Start
Expand Down Expand Up @@ -206,6 +206,36 @@ Agent: call get_full_result(session_id="a1b2c3", max_tokens=2000)
→ full graph data, truncated to fit token budget
```

For a polyglot repository, let `ingest` preserve its file selection and pass
the returned session directly to `parse`:

```
Agent: call ingest(source="/path/to/project", languages=["java", "kotlin"])
→ {session_id: "p1q2r3", languages: ["java", "kotlin"], ...}

Agent: call parse(source_path="p1q2r3")
→ {session_id: "a1b2c3", num_entities: 420, num_edges: 960, ...}
```

Use `language="multi"` instead when every detected supported language should
be parsed automatically.

**Supported polyglot pairs (MVP).** Every requested language is parsed, but
cross-language edges are only created *within a language family*:

| Family | Languages | Cross-language relinking |
|--------|-----------|--------------------------|
| `jvm` | `java`, `kotlin` | Yes — validated pair (shared FQN space, extends/implements across languages) |
| every other language | `python`, `go`, `typescript`, `c`, ... | No — parsed and merged into one graph, but never linked to another family |

The relink heuristics are dotted-name tuned (packages, unique leaf names,
`import a.b.C`) and only the JVM pair is covered by tests, so a Python
`com.auth.service` module and a Java `com.auth.service` class are never
confused for each other. When such an FQN coincidence happens across families
both entities are kept — the later one re-keyed as `<fqn>#<language>` — and the
counts appear in the graph's `metadata` (`fqn_collisions`,
`fqn_collisions_cross_family`, …), so nothing is silently dropped.

## LLM-Powered Analysis

Pass `--use-llm` to enable Claude-powered concern detection. Requires the `claude` CLI installed and authenticated.
Expand Down Expand Up @@ -318,7 +348,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 (structural); polyglot merge+relink via `languages=[...]` / `language="multi"` (cross-language edges within the JVM family only) |
| 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
6 changes: 3 additions & 3 deletions ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ Handle real-world polyglot monorepos.
- [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] **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.
- [x] **18. Cross-language dependency tracking** — MVP: multi-language ingest/parse (`languages=[...]` / `language="multi"`) merges per-language graphs and relinks import/extends/implements across FQN space. Relinking is **family-scoped**: the `jvm` family (Java↔Kotlin) is the supported and validated pair; every other language is its own family and is merged without cross-language edges. Extending relinking to further families (and broader RPC/IDL bridges — gRPC stubs, OpenAPI) remains follow-up.

## Phase 6 — Agent Protocol Integration

Expand All @@ -60,7 +60,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, 17, 18 (MVP) | Phases 1–2 + TS/JS & Go & Kotlin parsers, incremental parsing (Python), `diff_impact`, `context_for_task`, `api_surface`, polyglot merge+relink |
| **Now** | 10 | Architectural changelog |
| **Next** | 11, 16b | Component ownership, Rust parser |
| **Then** | 18–22 | Cross-language tracking, ecosystem breadth |
| **Then** | 19–22 | Ecosystem breadth (OpenAI / LangChain / Claude SDK / IDE) |
20 changes: 17 additions & 3 deletions actions/analyze/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,15 @@ 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, or multi
for every detected language with cross-language edge relinking).
required: false
default: ""
languages:
description: >
Optional comma-separated polyglot languages (e.g. java,kotlin). Mutually
exclusive with language when both would be set by the caller.
required: false
default: ""
repo-name:
Expand Down Expand Up @@ -135,12 +143,15 @@ runs:
env:
SOURCE_PATH: ${{ inputs.source-path }}
LANGUAGE: ${{ inputs.language }}
LANGUAGES: ${{ inputs.languages }}
REPO_NAME: ${{ inputs.repo-name }}
PRIMARY_ALGORITHM: ${{ inputs.primary-algorithm }}
FILTER_NON_ARCHITECTURAL_HELPERS: ${{ inputs.filter-non-architectural-helpers }}
run: |
ARGS=(--source "${SOURCE_PATH}")
if [ -n "${LANGUAGE}" ]; then
if [ -n "${LANGUAGES}" ]; then
ARGS+=(--languages "${LANGUAGES}")
elif [ -n "${LANGUAGE}" ]; then
ARGS+=(--language "${LANGUAGE}")
fi
if [ -n "${REPO_NAME}" ]; then
Expand All @@ -161,12 +172,15 @@ runs:
env:
SOURCE_PATH: ${{ inputs.source-path }}
LANGUAGE: ${{ inputs.language }}
LANGUAGES: ${{ inputs.languages }}
REPO_NAME: ${{ inputs.repo-name }}
WCA_NUM_CLUSTERS: ${{ inputs.wca-num-clusters }}
FILTER_NON_ARCHITECTURAL_HELPERS: ${{ inputs.filter-non-architectural-helpers }}
run: |
ARGS=(--source "${SOURCE_PATH}")
if [ -n "${LANGUAGE}" ]; then
if [ -n "${LANGUAGES}" ]; then
ARGS+=(--languages "${LANGUAGES}")
elif [ -n "${LANGUAGE}" ]; then
ARGS+=(--language "${LANGUAGE}")
fi
if [ -n "${REPO_NAME}" ]; then
Expand Down
37 changes: 29 additions & 8 deletions src/arcade_agent/ci/run_self_analysis.py
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,15 @@ 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, "
"or multi for every detected language)"
),
)
parser.add_argument(
"--languages",
default="",
help="Comma-separated polyglot languages (e.g. java,kotlin)",
)
parser.add_argument(
"--repo-name",
Expand Down Expand Up @@ -140,21 +148,33 @@ def main() -> None:

source = str(Path(args.source).resolve())
language = args.language or None
languages = [part.strip() for part in args.languages.split(",") if part.strip()] or None

print(f"[1/5] Ingesting {source}...")
repo = ingest(source, language=language)
print(f" Found {len(repo.source_files)} source files")
repo = ingest(source, language=language, languages=languages)
print(
f" Found {len(repo.source_files)} source files "
f"(languages={repo.languages or [repo.language]})"
)

if not repo.source_files:
print(" No source files found. Exiting.")
sys.exit(1)

print("[2/5] Parsing dependencies...")
raw_graph = parse(
str(repo.path),
language=repo.language or language or "python",
files=[str(f) for f in repo.source_files],
)
parse_files = [str(f) for f in repo.source_files]
if len(repo.languages) > 1:
raw_graph = parse(
str(repo.path),
languages=repo.languages,
files=parse_files,
)
else:
raw_graph = parse(
str(repo.path),
language=repo.language or language or "python",
files=parse_files,
)
graph = (
_filter_non_architectural_entities(raw_graph)
if args.filter_non_architectural_helpers
Expand Down Expand Up @@ -182,6 +202,7 @@ def main() -> None:
"timestamp": datetime.now(timezone.utc).isoformat(),
"repo_name": args.repo_name or repo.name,
"language": repo.language or language,
"languages": repo.languages,
"commit_sha": os.environ.get("GITHUB_SHA", "local"),
"ref": os.environ.get("GITHUB_REF", "local"),
"algorithm": args.algorithm,
Expand Down
12 changes: 11 additions & 1 deletion src/arcade_agent/parsers/graph.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"""Dependency graph data models."""

from dataclasses import dataclass, field
from typing import Any


@dataclass
Expand Down Expand Up @@ -35,6 +36,9 @@ class DependencyGraph:
entities: dict[str, Entity] = field(default_factory=dict)
edges: list[Edge] = field(default_factory=list)
packages: dict[str, list[str]] = field(default_factory=dict)
# Parse-time notes for consumers (e.g. cross-language FQN collision counts
# from multilang.merge_and_relink). Empty for single-language parses.
metadata: dict[str, Any] = field(default_factory=dict)

@property
def num_entities(self) -> int:
Expand Down Expand Up @@ -65,4 +69,10 @@ def merge(self, other: "DependencyGraph") -> "DependencyGraph":
packages.setdefault(pkg, []).extend(fqns)
for pkg, fqns in other.packages.items():
packages.setdefault(pkg, []).extend(fqns)
return DependencyGraph(entities=entities, edges=edges, packages=packages)
packages = {pkg: list(dict.fromkeys(fqns)) for pkg, fqns in packages.items()}
return DependencyGraph(
entities=entities,
edges=edges,
packages=packages,
metadata={**self.metadata, **other.metadata},
)
Loading
Loading