diff --git a/Cargo.lock b/Cargo.lock index 71788fa..84ffc3e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1223,7 +1223,7 @@ checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" [[package]] name = "spectra-context" -version = "0.4.0" +version = "0.4.1" dependencies = [ "atomic-write-file", "base64", @@ -1242,7 +1242,7 @@ dependencies = [ [[package]] name = "spectra-context-core" -version = "0.4.0" +version = "0.4.1" dependencies = [ "atomic-write-file", "ignore", @@ -1276,7 +1276,7 @@ dependencies = [ [[package]] name = "spectra-context-render" -version = "0.4.0" +version = "0.4.1" dependencies = [ "resvg", "spectra-context-core", diff --git a/Cargo.toml b/Cargo.toml index 62ca981..f456f67 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,7 +7,7 @@ members = [ resolver = "2" [workspace.package] -version = "0.4.0" +version = "0.4.1" edition = "2024" license = "MIT" rust-version = "1.88" diff --git a/README.md b/README.md index 1a2e0c5..0484bf5 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ **An adaptive context runtime for local AI coding agents.** [![Rust 1.88+](https://img.shields.io/badge/Rust-1.88%2B-CE412B?logo=rust&logoColor=white)](https://www.rust-lang.org/) -[![Version: 0.4.0](https://img.shields.io/badge/Version-0.4.0-38BDF8.svg)](https://github.com/rankupgames/Spectra/releases/tag/v0.4.0) +[![Version: 0.4.1](https://img.shields.io/badge/Version-0.4.1-38BDF8.svg)](https://github.com/rankupgames/Spectra/releases/tag/v0.4.1) [![License: MIT](https://img.shields.io/badge/License-MIT-22C55E.svg)](LICENSE) [![Status: Prototype](https://img.shields.io/badge/Status-Prototype-F59E0B.svg)](#project-status) @@ -20,7 +20,7 @@ Agent lifecycle ──adapter hooks──▶ immutable ledger ──▶ bounded Instead of dumping source up front, Spectra lets the model see the shape of the system, choose an exact `path:start-end` anchor, and read code once it knows what it is looking for. > [!IMPORTANT] -> Spectra is an early prototype. The v0.4 runtime retains the complete CodeGraph v1.3.0 language and extension surface with 39 adapters. The harness-neutral Ledger is verified for Codex, Claude Code, and Gemini CLI; Cursor support is intentionally partial because it can reinject continuity only at session start. See [Project status](#project-status) before relying on it in production. +> Spectra is an early prototype. The v0.4 runtime supports 39 language and dialect adapters. The harness-neutral Ledger is verified for Codex, Claude Code, and Gemini CLI; Cursor support is intentionally partial because it can reinject continuity only at session start. See [Project status](#project-status) before relying on it in production. The [agent support contract](docs/agent-support.md) tracks topology and Ledger support separately so an MCP integration is never mistaken for lifecycle coverage. @@ -41,7 +41,7 @@ Most code-context tools answer with source and explanation together. That can be | --- | ---: | | Median provider-input reduction | **88.3%** | | Composite recall-proxy retention | **118.0%** | -| Expected-anchor recall | **85.2%** (CodeGraph: 68.5%) | +| Expected-anchor recall | **85.2%** (reference arm: 68.5%) | | Ledger median estimated-token reduction | **93.4%** | | Ledger fact retention | **100%** | | Maximum Ledger projection | **57 tokens** | @@ -52,7 +52,7 @@ These numbers come from nine frozen prompts across pinned ripgrep, Tokio, and ru ### Requirements -- At least one supported local agent: Claude Code, Cursor, Codex, OpenCode, Hermes Agent, Gemini CLI, Antigravity, or Kiro +- At least one supported local agent: Claude Code, Claude Desktop, Cursor, Codex CLI/Desktop, OpenCode, Hermes Agent, Gemini CLI/Code Assist for VS Code, Antigravity, or Kiro - A repository containing at least one supported source language Any MCP client can also run `spectra serve --mcp` manually. @@ -62,13 +62,13 @@ Any MCP client can also run `spectra serve --mcp` manually. macOS and Linux: ```sh -curl --proto '=https' --tlsv1.2 -fsSL https://raw.githubusercontent.com/rankupgames/Spectra/v0.4.0/install.sh | sh +curl --proto '=https' --tlsv1.2 -fsSL https://raw.githubusercontent.com/rankupgames/Spectra/v0.4.1/install.sh | sh ``` Windows PowerShell: ```powershell -irm https://raw.githubusercontent.com/rankupgames/Spectra/v0.4.0/install.ps1 | iex +irm https://raw.githubusercontent.com/rankupgames/Spectra/v0.4.1/install.ps1 | iex ``` Package-manager and source-install alternatives: @@ -77,7 +77,7 @@ Package-manager and source-install alternatives: brew install rankupgames/tap/spectra scoop bucket add rankupgames https://github.com/rankupgames/scoop-bucket scoop install rankupgames/spectra -cargo install --git https://github.com/rankupgames/Spectra.git --tag v0.4.0 --bin spectra --locked +cargo install --git https://github.com/rankupgames/Spectra.git --tag v0.4.1 --bin spectra --locked ``` Cargo installation requires Rust 1.88 or newer. Every release archive is covered by the published `SHA256SUMS`, Sigstore bundle, and build provenance; both direct installers verify the archive checksum before installing it. @@ -100,9 +100,10 @@ Expected output: ```text Claude Code: MCP=current, Ledger hooks=current, Capability=topology+ledger -Codex: MCP=current, Ledger hooks=current, Capability=topology+ledger +Claude Desktop: MCP=current, Ledger=not available, Capability=topology +Codex CLI/Desktop: MCP=current, Ledger hooks=current, Capability=topology+ledger Cursor: MCP=current, Ledger hooks=current, Capability=topology+ledger-partial -Gemini CLI: MCP=current, Ledger hooks=current, Capability=topology+ledger +Gemini CLI/Code Assist (VS Code): MCP=current, Ledger hooks=current, Capability=topology+ledger ``` Your output only lists the agents Spectra detected. Codex, Claude Code, and Gemini CLI have recorded-wire `topology+ledger` coverage. Cursor is visibly labeled `topology+ledger-partial`: it records lifecycle facts but can reinject context only at `sessionStart`, not before every prompt. @@ -194,7 +195,7 @@ spectra hook [--agent codex|claude|gemini|cursor] On filesystems where native recursive watching is unavailable or unreliable, `spectra autosync install` adds marked blocks to the repository's `post-commit`, `post-merge`, and `post-checkout` hooks. Each hook launches `spectra sync --quiet` in the background. Installation is idempotent, honors Git's resolved hooks directory, preserves existing hook bodies, and `spectra autosync remove` deletes only Spectra-owned blocks. -`--agent auto` is the default and configures every detected agent. `--agent all` attempts every adapter; agents that expose configuration only through their own CLI must already be installed. Non-interactive use requires an explicit `--agent` or `--yes`, so it never waits for wizard input. Verified local configuration is available for Codex, Claude Code, Gemini CLI, and Cursor. +`--agent auto` is the default and configures every detected agent. `--agent all` attempts every adapter. `claude-desktop` is a distinct target because Claude Desktop has its own configuration; `codex-desktop` and `gemini-desktop` are accepted aliases for the shared Codex and Gemini Code Assist for VS Code configurations. Codex Desktop can be configured even when the Codex CLI is absent. Non-interactive use requires an explicit `--agent` or `--yes`, so it never waits for wizard input. Verified project-local configuration is available for Codex, Claude Code, Gemini CLI, and Cursor. The installer is idempotent and ownership-aware: it updates stale Spectra registrations, preserves unrelated settings and comments, writes configuration atomically, and refuses to overwrite or remove a foreign MCP entry named `spectra`. @@ -263,9 +264,9 @@ The internal graph kernel is domain-neutral: - adjacency indexes and invariant validation - code-specific `SourceSpan` data kept in a separate sidecar -Every adapter maps parser-backed or structured extraction into the same graph vocabulary. The current v0.2 packs cover the complete CodeGraph v1.3.0 language surface, including structural symbols, imports, calls, inheritance, implementations, configuration references, templates, infrastructure blocks, and legacy execution edges where applicable. Web components additionally parse embedded JavaScript or TypeScript, connect template events to script handlers, resolve rendered components, and model SvelteKit, Nuxt, Astro, Razor, Drupal, and ArkUI routes. Native adapters connect Objective-C interfaces, protocols, implementations, and message sends, and distinguish CUDA and Metal entry-point kernels from ordinary functions. Structured adapters cover Terraform/OpenTofu, Nix, YAML, XML/MyBatis, properties, Twig, CFML queries, and COBOL/CICS. Ambiguous targets remain explicit uncertain boundaries. Rendering condenses cycles, layers nodes, clusters related code, routes typed edges, and emits deterministic SVG and PNG artifacts. +Every adapter maps parser-backed or structured extraction into the same graph vocabulary. The current v0.2 packs cover 39 language and dialect adapters, including structural symbols, imports, calls, inheritance, implementations, configuration references, templates, infrastructure blocks, and legacy execution edges where applicable. Web components additionally parse embedded JavaScript or TypeScript, connect template events to script handlers, resolve rendered components, and model SvelteKit, Nuxt, Astro, Razor, Drupal, and ArkUI routes. Native adapters connect Objective-C interfaces, protocols, implementations, and message sends, and distinguish CUDA and Metal entry-point kernels from ordinary functions. Structured adapters cover Terraform/OpenTofu, Nix, YAML, XML/MyBatis, properties, Twig, CFML queries, and COBOL/CICS. Ambiguous targets remain explicit uncertain boundaries. Rendering condenses cycles, layers nodes, clusters related code, routes typed edges, and emits deterministic SVG and PNG artifacts. -The adapter contract, functional acceptance bar, and CodeGraph parity matrix are tracked in [Code adapters](docs/code-adapters.md). +The adapter contract and functional acceptance bar are tracked in [Code adapters](docs/code-adapters.md). See the [Ledger design and maintenance boundaries](docs/state-machine-ledger.md) for the state-machine contract. @@ -302,15 +303,15 @@ The benchmark protocol, frozen prompts, raw evaluation data, and replay fixtures Implemented: -- adapter-driven topology extraction and incremental indexing across all 39 CodeGraph v1.3.0 language families and dialect adapters -- CodeGraph-parity server framework routes plus React/Next, SwiftUI, React Native, Expo Module, and Fabric client/native bridges +- adapter-driven topology extraction and incremental indexing across all 39 supported language families and dialect adapters +- server framework routes plus React/Next, SwiftUI, React Native, Expo Module, and Fabric client/native bridges - embedded JavaScript/TypeScript bridges, component rendering and event bindings, and conventional SvelteKit, Nuxt, and Astro page routes - query-focused deterministic PNG and SVG rendering - budgeted Context Packet v1 responses with deterministic intent routing, atomic evidence packing, bounded source windows, stale-safe continuation cursors, and explicit-only maps - session-durable evidence deduplication with hashed receipts, bounded LRU storage, concurrent writers, corruption recovery, and fail-open delivery - local privacy-safe efficiency metrics with opt-out, inspection, and explicit reset -- the complete CodeGraph v1.3.0 MCP query capability set, with `spectra_context` as the one-tool default and all twelve v0.3 tools behind compatible allowlists -- automatic MCP installation for Claude Code, Cursor, Codex, OpenCode, Hermes Agent, Gemini CLI, Antigravity, and Kiro +- the complete v0.3 MCP query capability set, with `spectra_context` as the one-tool default and all twelve legacy tools behind compatible allowlists +- automatic MCP installation for Claude Code, Claude Desktop, Cursor, Codex CLI/Desktop, OpenCode, Hermes Agent, Gemini CLI/Code Assist for VS Code, Antigravity, and Kiro - automatic lifecycle-hook installation for Codex, Claude Code, Gemini CLI, and Cursor - append-only, per-session State Machine Ledger with replay, recovery, redaction, concurrency control, cross-harness project facts, and bounded projection - stable harness-neutral `spectra lifecycle ingest` JSON v1 protocol diff --git a/benchmarks/README.md b/benchmarks/README.md index 053953e..7bc37ee 100644 --- a/benchmarks/README.md +++ b/benchmarks/README.md @@ -1,8 +1,8 @@ -# Comparative encoding benchmark: Spectra and CodeGraph +# Comparative encoding benchmark -CodeGraph is used here as a mature, source-rich local graph reference. This benchmark asks whether Spectra's visual-topology encoding can reduce model input while preserving enough architecture and navigation quality to justify further research. It is not intended as a general ranking of the two projects. +A mature, source-rich local graph tool is used as the reference arm. This benchmark asks whether Spectra's visual-topology encoding can reduce model input while preserving enough architecture and navigation quality to justify further research. It is not intended as a general product ranking. -Spectra's internal prototype gate is at least 50% fewer median model input tokens than the `codegraph_explore` reference arm while retaining at least 90% of that arm's architecture/navigation task score. +Spectra's internal prototype gate is at least 50% fewer median model input tokens than the reference arm while retaining at least 90% of that arm's architecture/navigation task score. ## Corpus @@ -12,14 +12,14 @@ Use pinned commits of three Rust repositories representing different scales: 2. tokio 3. rust-analyzer -Record every repository URL and exact commit SHA beside the result. Run `codegraph init` and `spectra init` from the same clean checkout. +Record every repository URL and exact commit SHA beside the result. Initialize both tools from the same clean checkout. ## Procedure For each prompt in `prompts.json`: 1. Start a fresh conversation using the same multimodal model, system prompt, temperature, and maximum output tokens. -2. CodeGraph arm: allow `codegraph_explore` and ordinary targeted reads. +2. Reference arm: allow the reference graph query and ordinary targeted reads. 3. Spectra arm: allow `spectra_map` and ordinary targeted reads. 4. Record provider-reported input tokens, output tokens, image settings, tool calls, wall time, returned payload bytes, and final answer. 5. Grade expected architecture concepts and expected source anchors blind to the arm. @@ -36,13 +36,12 @@ cargo build --release --workspace --manifest benchmarks/prompts.json \ --corpus-root /path/to/pinned/corpus \ --output benchmarks/results/run-name \ - --codegraph-bin /path/to/codegraph \ --spectra-bin /path/to/spectra \ --reindex \ --repeats 3 ``` -The runner verifies commit SHAs, records tool versions and cold indexing times, performs repeated warm queries, saves every raw CodeGraph response and Spectra image, estimates text-only tokens at four characters per token, and computes expected-anchor path recall. The estimate deliberately excludes vision tokens; only provider-reported usage can complete the final comparison. +The runner verifies commit SHAs, records tool versions and cold indexing times, performs repeated warm queries, saves every raw reference response and Spectra image, estimates text-only tokens at four characters per token, and computes expected-anchor path recall. The estimate deliberately excludes vision tokens; only provider-reported usage can complete the final comparison. Use the runner's `--help` output for the internal reference-binary flag name. Generated reports are written under `benchmarks/results/`. That directory is intentionally ignored by Git; publish selected results separately when they have been reviewed for release. @@ -55,11 +54,10 @@ cargo run --release -p spectra-context --bin spectra-adapter-eval -- \ --manifest benchmarks/adapter-repositories.json \ --corpus-root /path/to/adapter-corpus \ --output benchmarks/results/adapter-parity \ - --codegraph-bin /path/to/codegraph \ --reindex ``` -The runner verifies every commit, rebuilds both indexes, compares the complete CodeGraph route-label set with Spectra, requires the pinned minimum Spectra route count, measures resolved Spectra route edges, and exits nonzero on a gap. Additional Spectra routes are reported rather than treated as failures because the reference may detect no routes in a framework it otherwise recognizes. The reviewed v0.2 baseline is recorded in [`v0.2-baseline.md`](v0.2-baseline.md). +The runner verifies every commit, rebuilds both indexes, compares the complete reference route-label set with Spectra, requires the pinned minimum Spectra route count, measures resolved Spectra route edges, and exits nonzero on a gap. Additional Spectra routes are reported rather than treated as failures because the reference may detect no routes in a framework it otherwise recognizes. The reviewed v0.2 baseline is recorded in [`v0.2-baseline.md`](v0.2-baseline.md). Use the runner's `--help` output for its internal reference-binary flag name. ## State Machine Ledger benchmark diff --git a/benchmarks/v0.2-baseline.md b/benchmarks/v0.2-baseline.md index 4740fa2..7a18129 100644 --- a/benchmarks/v0.2-baseline.md +++ b/benchmarks/v0.2-baseline.md @@ -1,10 +1,10 @@ # v0.2 adapter and topology baseline -The reviewed 2026-07-13 v0.2 gate used CodeGraph 1.3.0 and Spectra index v4. +The reviewed 2026-07-13 v0.2 gate used a pinned reference implementation and Spectra index v4. ## Real-repository adapter routes -| Repository | Framework | CodeGraph routes | Spectra routes | Shared | Resolved Spectra routes | +| Repository | Framework | Reference routes | Spectra routes | Shared | Resolved Spectra routes | | --- | --- | ---: | ---: | ---: | ---: | | full-stack-fastapi | FastAPI + React | 0 | 18 | 0 | 16 | | laravel | Laravel | 1 | 1 | 1 | 0 | @@ -12,18 +12,18 @@ The reviewed 2026-07-13 v0.2 gate used CodeGraph 1.3.0 and Spectra index v4. | spring-petclinic | Spring MVC | 17 | 17 | 17 | 17 | | vapor-template | Vapor | 3 | 3 | 3 | 3 | -The parity gate passed with all 22 CodeGraph route labels present in Spectra (100% reference-route recall). Spectra emitted 40 routes in total, including 18 FastAPI routes where CodeGraph 1.3.0 emitted none. Laravel's starter route uses an inline closure, so it has no static handler edge to resolve. +The parity gate passed with all 22 reference route labels present in Spectra (100% reference-route recall). Spectra emitted 40 routes in total, including 18 FastAPI routes where the reference implementation emitted none. Laravel's starter route uses an inline closure, so it has no static handler edge to resolve. ## Existing topology regression corpus -The pinned ripgrep, Tokio, and rust-analyzer corpus completed all nine deterministic prompts on v0.2. Median text-only CodeGraph context was 5,819 estimated tokens; median Spectra semantic metadata was 252 estimated tokens, a 95.7% metadata reduction before vision-token accounting. Mean expected-anchor path recall was 74.1% for CodeGraph and 90.7% for Spectra. Spectra cold indexing took 201 ms, 597 ms, and 1,699 ms respectively, versus 1,372 ms, 5,054 ms, and 21,176 ms for CodeGraph. +The pinned ripgrep, Tokio, and rust-analyzer corpus completed all nine deterministic prompts on v0.2. Median text-only reference context was 5,819 estimated tokens; median Spectra semantic metadata was 252 estimated tokens, a 95.7% metadata reduction before vision-token accounting. Mean expected-anchor path recall was 74.1% for the reference arm and 90.7% for Spectra. Spectra cold indexing took 201 ms, 597 ms, and 1,699 ms respectively, versus 1,372 ms, 5,054 ms, and 21,176 ms for the reference. These deterministic token figures exclude image-token charges. The provider-backed run below supplies the image-inclusive comparison. ## Provider-backed multimodal evaluation -The final complete nine-prompt corpus ran successfully on 2026-07-13 with `grok-4.5`, low reasoning effort, and low image detail. All 18 CodeGraph and Spectra arms completed against freshly rebuilt v4 indexes. Median provider-reported input was 6,718 tokens for CodeGraph and 786 tokens for Spectra, an 88.3% reduction including vision-token accounting. Median total usage was 7,473 tokens for CodeGraph and 1,531 for Spectra, a 79.5% reduction. Aggregate reported cost was $0.104730 for CodeGraph and $0.058710 for Spectra, a 43.9% reduction. +The final complete nine-prompt corpus ran successfully on 2026-07-13 with `grok-4.5`, low reasoning effort, and low image detail. All 18 reference and Spectra arms completed against freshly rebuilt indexes. Median provider-reported input was 6,718 tokens for the reference and 786 tokens for Spectra, an 88.3% reduction including vision-token accounting. Median total usage was 7,473 tokens for the reference and 1,531 for Spectra, a 79.5% reduction. Aggregate reported cost was $0.104730 for the reference and $0.058710 for Spectra, a 43.9% reduction. -Mean lexical concept recall was 44.4% for CodeGraph and 48.1% for Spectra. Mean expected-anchor recall was 68.5% for CodeGraph and 85.2% for Spectra. Their equally weighted combined proxies were 56.5% and 66.7%, so Spectra retained 118.0% of the reference proxy while passing the 50% median-input-reduction gate. Review of the paired answers found no critical Spectra failure; the semantic legend consistently exposed the relevant qualified APIs while retaining explicit uncertainty for truncated graphs. These lexical proxies and internal review remain diagnostics rather than a substitute for an external blind human study. +Mean lexical concept recall was 44.4% for the reference and 48.1% for Spectra. Mean expected-anchor recall was 68.5% for the reference and 85.2% for Spectra. Their equally weighted combined proxies were 56.5% and 66.7%, so Spectra retained 118.0% of the reference proxy while passing the 50% median-input-reduction gate. Review of the paired answers found no critical Spectra failure; the semantic legend consistently exposed the relevant qualified APIs while retaining explicit uncertainty for truncated graphs. These lexical proxies and internal review remain diagnostics rather than a substitute for an external blind human study. The reviewed raw report is stored in the ignored benchmark artifact directory at `benchmarks/results/grok-v0.2-semantic-full-2026-07-13/grok-evaluation.json`. diff --git a/crates/spectra-cli/Cargo.toml b/crates/spectra-cli/Cargo.toml index f8c640b..0a298f5 100644 --- a/crates/spectra-cli/Cargo.toml +++ b/crates/spectra-cli/Cargo.toml @@ -27,8 +27,8 @@ rmcp.workspace = true schemars.workspace = true serde.workspace = true serde_json.workspace = true -spectra-core = { package = "spectra-context-core", version = "0.4.0", path = "../spectra-core" } -spectra-render = { package = "spectra-context-render", version = "0.4.0", path = "../spectra-render" } +spectra-core = { package = "spectra-context-core", version = "0.4.1", path = "../spectra-core" } +spectra-render = { package = "spectra-context-render", version = "0.4.1", path = "../spectra-render" } tokio.workspace = true [[bin]] diff --git a/crates/spectra-cli/src/agents/local.rs b/crates/spectra-cli/src/agents/local.rs index eaf058f..e268380 100644 --- a/crates/spectra-cli/src/agents/local.rs +++ b/crates/spectra-cli/src/agents/local.rs @@ -21,17 +21,43 @@ pub(super) fn install_codex( dry_run: bool, topology_only: bool, ) -> Result { - let config = project.join(".codex/config.toml"); - let hooks_path = project.join(".codex/hooks.json"); + install_codex_at( + project.join(".codex/config.toml"), + project.join(".codex/hooks.json"), + dry_run, + topology_only, + "project", + ) +} + +pub(super) fn install_codex_global(dry_run: bool, topology_only: bool) -> Result { + let root = codex_home()?; + install_codex_at( + root.join("config.toml"), + root.join("hooks.json"), + dry_run, + topology_only, + "user", + ) +} + +fn install_codex_at( + config: std::path::PathBuf, + hooks_path: std::path::PathBuf, + dry_run: bool, + topology_only: bool, + scope: &str, +) -> Result { let executable = spectra_executable()?; let current = if config.exists() { fs::read_to_string(&config)? } else { String::new() }; - if current.contains("[mcp_servers.spectra]") && !current.contains(START) { + let owns_unmarked = owned_unmarked_block(¤t); + if current.contains("[mcp_servers.spectra]") && !current.contains(START) && !owns_unmarked { return Err(format!( - "Codex already has a non-Spectra-owned project MCP entry named 'spectra' in {}", + "Codex already has a non-Spectra-owned {scope} MCP entry named 'spectra' in {}", config.display() ) .into()); @@ -54,7 +80,12 @@ pub(super) fn install_codex( if let Some(target) = &hook_target { hooks::install(target, true)?; } - let updated = replace_block(¤t, &block); + let base = if owns_unmarked { + remove_unmarked_block(¤t) + } else { + current.clone() + }; + let updated = replace_block(&base, &block); let snapshot = (!dry_run) .then(|| ConfigSnapshot::capture(&config)) .transpose()?; @@ -63,11 +94,11 @@ pub(super) fn install_codex( } let mut message = if dry_run { format!( - "Codex: Would configure project MCP in {}.", + "Codex: Would configure {scope} MCP in {}.", config.display() ) } else { - format!("Codex: Project MCP configured in {}.", config.display()) + format!("Codex: {scope} MCP configured in {}.", config.display()) }; if let Some(target) = &hook_target { message.push('\n'); @@ -76,7 +107,7 @@ pub(super) fn install_codex( Err(error) => { if let Some(snapshot) = snapshot { snapshot.restore().map_err(|rollback| { - format!("{error}; additionally failed to roll back Codex project configuration: {rollback}") + format!("{error}; additionally failed to roll back Codex {scope} configuration: {rollback}") })?; } return Err(error); @@ -87,16 +118,39 @@ pub(super) fn install_codex( } pub(super) fn uninstall_codex(project: &Path, dry_run: bool) -> Result { - let config = project.join(".codex/config.toml"); - let hooks_path = project.join(".codex/hooks.json"); + uninstall_codex_at( + project.join(".codex/config.toml"), + project.join(".codex/hooks.json"), + dry_run, + "project", + ) +} + +pub(super) fn uninstall_codex_global(dry_run: bool) -> Result { + let root = codex_home()?; + uninstall_codex_at( + root.join("config.toml"), + root.join("hooks.json"), + dry_run, + "user", + ) +} + +fn uninstall_codex_at( + config: std::path::PathBuf, + hooks_path: std::path::PathBuf, + dry_run: bool, + scope: &str, +) -> Result { let current = if config.exists() { fs::read_to_string(&config)? } else { String::new() }; - if current.contains("[mcp_servers.spectra]") && !current.contains(START) { + let owns_unmarked = owned_unmarked_block(¤t); + if current.contains("[mcp_servers.spectra]") && !current.contains(START) && !owns_unmarked { return Err(format!( - "Codex project MCP entry named 'spectra' in {} is not owned by Spectra", + "Codex {scope} MCP entry named 'spectra' in {} is not owned by Spectra", config.display() ) .into()); @@ -108,11 +162,15 @@ pub(super) fn uninstall_codex(project: &Path, dry_run: bool) -> Result Result { snapshot.restore().map_err(|rollback| { format!( - "{error}; additionally failed to roll back Codex project configuration: {rollback}" + "{error}; additionally failed to roll back Codex {scope} configuration: {rollback}" ) })?; return Err(error); } }; Ok(format!( - "Codex: Removed project MCP configuration.\n{hook_message}" + "Codex: Removed {scope} MCP configuration.\n{hook_message}" )) } +pub(super) fn codex_global_status() -> Result { + let root = codex_home()?; + let config = root.join("config.toml"); + let current = if config.exists() { + fs::read_to_string(&config)? + } else { + String::new() + }; + let executable = spectra_executable()?; + let command = serde_json::to_string( + executable + .to_str() + .ok_or("Spectra executable path is not valid UTF-8")?, + )?; + let expected = format!("command = {command}\nargs = [\"serve\", \"--mcp\"]"); + let mcp = if (current.contains(START) && current.contains(&expected)) + || unmarked_command(¤t).is_some_and(|configured| { + let configured = std::path::PathBuf::from(configured); + configured.canonicalize().unwrap_or(configured) == executable + }) { + "current" + } else if current.contains(START) || owned_unmarked_block(¤t) { + "stale" + } else if current.contains("[mcp_servers.spectra]") { + "foreign conflict" + } else { + "missing" + }; + let hook_target = HookTarget { + label: "Codex", + agent: "codex", + path: root.join("hooks.json"), + schema: HookSchema::Grouped, + events: CODEX_EVENTS, + }; + let hooks = hooks::status(&hook_target)?; + Ok(format!( + "Codex CLI/Desktop: MCP={mcp}, Ledger hooks={hooks}, Capability=topology+ledger" + )) +} + +fn codex_home() -> Result { + if let Some(path) = + std::env::var_os("SPECTRA_CODEX_HOME").or_else(|| std::env::var_os("CODEX_HOME")) + { + return Ok(path.into()); + } + std::env::var_os("SPECTRA_HOME") + .or_else(|| std::env::var_os("HOME")) + .or_else(|| std::env::var_os("USERPROFILE")) + .map(std::path::PathBuf::from) + .map(|path| path.join(".codex")) + .ok_or_else(|| "unable to locate the Codex home directory".into()) +} + fn replace_block(current: &str, block: &str) -> String { let without = remove_block(current); let trimmed = without.trim_end(); @@ -156,3 +269,45 @@ fn remove_block(current: &str) -> String { current.to_owned() } } + +fn unmarked_block_range(current: &str) -> Option> { + let start = current.find("[mcp_servers.spectra]")?; + let remainder = ¤t[start + "[mcp_servers.spectra]".len()..]; + let end = remainder.find("\n[").map_or(current.len(), |offset| { + start + "[mcp_servers.spectra]".len() + offset + 1 + }); + Some(start..end) +} + +fn unmarked_command(current: &str) -> Option { + let range = unmarked_block_range(current)?; + let block = ¤t[range]; + let compact: String = block + .chars() + .filter(|character| !character.is_whitespace()) + .collect(); + if !compact.contains("args=[\"serve\",\"--mcp\"]") { + return None; + } + block.lines().find_map(|line| { + let value = line.trim().strip_prefix("command")?.trim_start(); + let value = value.strip_prefix('=')?.trim(); + serde_json::from_str(value).ok() + }) +} + +fn owned_unmarked_block(current: &str) -> bool { + unmarked_command(current).is_some_and(|command| { + std::path::Path::new(&command) + .file_stem() + .and_then(|stem| stem.to_str()) + .is_some_and(|stem| stem.eq_ignore_ascii_case("spectra")) + }) +} + +fn remove_unmarked_block(current: &str) -> String { + let Some(range) = unmarked_block_range(current) else { + return current.to_owned(); + }; + format!("{}{}", ¤t[..range.start], ¤t[range.end..]) +} diff --git a/crates/spectra-cli/src/agents/mod.rs b/crates/spectra-cli/src/agents/mod.rs index 411af3e..deba904 100644 --- a/crates/spectra-cli/src/agents/mod.rs +++ b/crates/spectra-cli/src/agents/mod.rs @@ -20,8 +20,9 @@ pub(crate) struct Report { pub errors: Vec, } -const TARGETS: [Agent; 8] = [ +const TARGETS: [Agent; 9] = [ Agent::Claude, + Agent::ClaudeDesktop, Agent::Cursor, Agent::Codex, Agent::OpenCode, @@ -38,10 +39,14 @@ pub(crate) enum Agent { /// Configure every supported agent, even when it is not detected. All, Claude, + /// Configure the standalone Claude desktop application. + ClaudeDesktop, Cursor, + #[value(alias = "codex-desktop")] Codex, OpenCode, Hermes, + #[value(alias = "gemini-desktop", alias = "gemini-code-assist")] Gemini, Antigravity, Kiro, @@ -208,10 +213,13 @@ fn install_one( Agent::Codex if location == Location::Local => { local::install_codex(project, dry_run, topology_only) } - Agent::Codex if topology_only => { + Agent::Codex if topology_only && codex_cli_available() => { Ok(crate::install::install_codex_topology_only(dry_run)?.to_string()) } - Agent::Codex => Ok(crate::install::install_codex(dry_run)?.to_string()), + Agent::Codex if codex_cli_available() => { + Ok(crate::install::install_codex(dry_run)?.to_string()) + } + Agent::Codex => local::install_codex_global(dry_run, topology_only), Agent::OpenCode => jsonc::install(dry_run), Agent::Hermes => hermes::install(dry_run), agent => { @@ -290,7 +298,10 @@ fn uninstall_one( } match agent { Agent::Codex if location == Location::Local => local::uninstall_codex(project, dry_run), - Agent::Codex => Ok(crate::install::uninstall_codex(dry_run)?.to_string()), + Agent::Codex if codex_cli_available() => { + Ok(crate::install::uninstall_codex(dry_run)?.to_string()) + } + Agent::Codex => local::uninstall_codex_global(dry_run), Agent::OpenCode => jsonc::uninstall(dry_run), Agent::Hermes => hermes::uninstall(dry_run), agent => { @@ -326,7 +337,8 @@ fn uninstall_one( fn status_one(agent: Agent) -> Result { match agent { - Agent::Codex => crate::install::codex_status(), + Agent::Codex if codex_cli_available() => crate::install::codex_status(), + Agent::Codex => local::codex_global_status(), Agent::OpenCode => jsonc::status(), Agent::Hermes => hermes::status(), agent => { @@ -396,7 +408,7 @@ fn hook_target( events: CLAUDE_HOOK_EVENTS, }, Agent::Gemini => hooks::HookTarget { - label: "Gemini CLI", + label: "Gemini CLI/Code Assist (VS Code)", agent: "gemini", path: if location == Location::Local { project.join(".gemini/settings.json") @@ -439,6 +451,12 @@ fn json_target(agent: Agent, location: Location, project: &Path) -> Result JsonTarget { + label: "Claude Desktop", + root_key: "mcpServers", + path: claude_desktop_path()?, + args: STDIO_ARGS, + }, Agent::Cursor => JsonTarget { label: "Cursor", root_key: "mcpServers", @@ -454,7 +472,7 @@ fn json_target(agent: Agent, location: Location, project: &Path) -> Result JsonTarget { - label: "Gemini CLI", + label: "Gemini CLI/Code Assist (VS Code)", root_key: "mcpServers", path: if location == Location::Local { project.join(".gemini/settings.json") @@ -485,11 +503,12 @@ fn label(agent: Agent) -> &'static str { Agent::Auto => "Automatic detection", Agent::All => "All agents", Agent::Claude => "Claude Code", + Agent::ClaudeDesktop => "Claude Desktop", Agent::Cursor => "Cursor", - Agent::Codex => "Codex", + Agent::Codex => "Codex CLI/Desktop", Agent::OpenCode => "OpenCode", Agent::Hermes => "Hermes Agent", - Agent::Gemini => "Gemini CLI", + Agent::Gemini => "Gemini CLI/Code Assist (VS Code)", Agent::Antigravity => "Antigravity", Agent::Kiro => "Kiro", } @@ -501,12 +520,18 @@ fn detected(agent: Agent) -> bool { env::var_os("SPECTRA_CODEX_BIN").is_some() || command_exists("codex") || home_path(".codex").is_some_and(|path| path.exists()) + || desktop_app_exists("Codex") } Agent::Claude => { command_exists("claude") || claude_path().is_ok_and(|path| path.exists()) || home_path(".claude").is_some_and(|path| path.exists()) } + Agent::ClaudeDesktop => { + env::var_os("SPECTRA_CLAUDE_DESKTOP_CONFIG").is_some() + || claude_desktop_path().is_ok_and(|path| path.exists()) + || desktop_app_exists("Claude") + } Agent::Cursor => { command_exists("cursor") || command_exists("cursor-agent") @@ -517,7 +542,11 @@ fn detected(agent: Agent) -> bool { command_exists("opencode") || jsonc::path().is_ok_and(|path| path.exists()) } Agent::Hermes => command_exists("hermes") || hermes::path().is_ok_and(|path| path.exists()), - Agent::Gemini => command_exists("gemini") || gemini_path().is_ok_and(|path| path.exists()), + Agent::Gemini => { + command_exists("gemini") + || gemini_path().is_ok_and(|path| path.exists()) + || gemini_desktop_detected() + } Agent::Antigravity => { command_exists("agy") || command_exists("antigravity") @@ -600,6 +629,28 @@ fn claude_path() -> Result { configured_path("SPECTRA_CLAUDE_CONFIG", ".claude.json") } +fn claude_desktop_path() -> Result { + if let Some(path) = env::var_os("SPECTRA_CLAUDE_DESKTOP_CONFIG") { + return Ok(PathBuf::from(path)); + } + #[cfg(target_os = "macos")] + return home_path("Library/Application Support/Claude/claude_desktop_config.json") + .ok_or_else(|| "unable to locate the user home directory".into()); + #[cfg(windows)] + return env::var_os("APPDATA") + .map(PathBuf::from) + .map(|path| path.join("Claude/claude_desktop_config.json")) + .ok_or_else(|| "unable to locate the roaming application data directory".into()); + #[cfg(not(any(target_os = "macos", windows)))] + { + let base = env::var_os("XDG_CONFIG_HOME") + .map(PathBuf::from) + .or_else(|| home_path(".config")) + .ok_or("unable to locate the user configuration directory")?; + Ok(base.join("Claude/claude_desktop_config.json")) + } +} + fn cursor_path() -> Result { configured_path("SPECTRA_CURSOR_CONFIG", ".cursor/mcp.json") } @@ -638,3 +689,59 @@ fn command_exists(command: &str) -> bool { false }) } + +fn codex_cli_available() -> bool { + env::var_os("SPECTRA_CODEX_BIN").is_some() || command_exists("codex") +} + +fn desktop_app_exists(name: &str) -> bool { + if env::var_os(format!("SPECTRA_{}_DESKTOP", name.to_ascii_uppercase())).is_some() { + return true; + } + #[cfg(target_os = "macos")] + { + Path::new("/Applications") + .join(format!("{name}.app")) + .exists() + || home_path("Applications") + .is_some_and(|path| path.join(format!("{name}.app")).exists()) + } + #[cfg(not(target_os = "macos"))] + { + #[cfg(windows)] + { + let executable = format!("{name}.exe"); + return env::var_os("LOCALAPPDATA") + .map(PathBuf::from) + .is_some_and(|path| { + path.join("Programs").join(name).join(&executable).exists() + || path.join(name).join(&executable).exists() + }); + } + #[cfg(not(windows))] + { + let desktop_file = format!("{}.desktop", name.to_ascii_lowercase()); + Path::new("/usr/share/applications") + .join(&desktop_file) + .exists() + || home_path(".local/share/applications") + .is_some_and(|path| path.join(desktop_file).exists()) + } + } +} + +fn gemini_desktop_detected() -> bool { + if env::var_os("SPECTRA_GEMINI_DESKTOP").is_some() { + return true; + } + home_path(".vscode/extensions").is_some_and(|path| { + fs::read_dir(path).is_ok_and(|entries| { + entries.filter_map(Result::ok).any(|entry| { + entry + .file_name() + .to_string_lossy() + .starts_with("google.geminicodeassist") + }) + }) + }) +} diff --git a/crates/spectra-cli/src/mcp.rs b/crates/spectra-cli/src/mcp.rs index 7e7ec58..a32096c 100644 --- a/crates/spectra-cli/src/mcp.rs +++ b/crates/spectra-cli/src/mcp.rs @@ -861,7 +861,7 @@ impl SpectraServer { #[tool_handler( name = "spectra", - version = "0.4.0", + version = "0.4.1", instructions = "Use spectra_context for budgeted, text-first code context. Request representation=map explicitly when a visual topology is worth provider image tokens. Legacy focused tools remain available through SPECTRA_MCP_TOOLS." )] impl ServerHandler for SpectraServer { diff --git a/crates/spectra-cli/tests/cli.rs b/crates/spectra-cli/tests/cli.rs index 987bf48..5c9c64b 100644 --- a/crates/spectra-cli/tests/cli.rs +++ b/crates/spectra-cli/tests/cli.rs @@ -460,6 +460,7 @@ fn standard_json_agent_installers_are_owned_idempotent_and_reversible() { let binary = env!("CARGO_BIN_EXE_spectra"); let targets = [ ("claude", "SPECTRA_CLAUDE_CONFIG"), + ("claude-desktop", "SPECTRA_CLAUDE_DESKTOP_CONFIG"), ("cursor", "SPECTRA_CURSOR_CONFIG"), ("gemini", "SPECTRA_GEMINI_CONFIG"), ("antigravity", "SPECTRA_ANTIGRAVITY_CONFIG"), @@ -556,6 +557,116 @@ fn standard_json_agent_installers_are_owned_idempotent_and_reversible() { fs::remove_dir_all(root).unwrap(); } +#[test] +fn codex_desktop_installs_without_the_codex_cli() { + let root = fixture(); + let codex_home = root.join(".codex"); + let output = Command::new(env!("CARGO_BIN_EXE_spectra")) + .args(["install", "--agent", "codex-desktop"]) + .env("PATH", &root) + .env("SPECTRA_CODEX_HOME", &codex_home) + .env("SPECTRA_CODEX_DESKTOP", "1") + .output() + .unwrap(); + assert!( + output.status.success(), + "{}", + String::from_utf8_lossy(&output.stderr) + ); + let config = fs::read_to_string(codex_home.join("config.toml")).unwrap(); + assert!(config.contains("[mcp_servers.spectra]")); + assert!(config.contains("# >>> spectra managed MCP >>>")); + assert!(codex_home.join("hooks.json").exists()); + + let status = Command::new(env!("CARGO_BIN_EXE_spectra")) + .args(["status", "--agent", "codex-desktop"]) + .env("PATH", &root) + .env("SPECTRA_CODEX_HOME", &codex_home) + .output() + .unwrap(); + assert!(status.status.success()); + assert!(String::from_utf8_lossy(&status.stdout).contains("MCP=current")); + + let removed = Command::new(env!("CARGO_BIN_EXE_spectra")) + .args(["uninstall", "--agent", "codex-desktop"]) + .env("PATH", &root) + .env("SPECTRA_CODEX_HOME", &codex_home) + .output() + .unwrap(); + assert!(removed.status.success()); + assert!( + !fs::read_to_string(codex_home.join("config.toml")) + .unwrap() + .contains("[mcp_servers.spectra]") + ); + + fs::write( + codex_home.join("config.toml"), + "[mcp_servers.spectra]\ncommand = \"/old/spectra\"\nargs = [\"serve\", \"--mcp\"]\n\n[unrelated]\nkeep = true\n", + ) + .unwrap(); + let migrated = Command::new(env!("CARGO_BIN_EXE_spectra")) + .args(["install", "--agent", "codex-desktop"]) + .env("PATH", &root) + .env("SPECTRA_CODEX_HOME", &codex_home) + .output() + .unwrap(); + assert!(migrated.status.success()); + let migrated = fs::read_to_string(codex_home.join("config.toml")).unwrap(); + assert!(migrated.contains("# >>> spectra managed MCP >>>")); + assert!(migrated.contains("[unrelated]")); + + fs::remove_dir_all(root).unwrap(); +} + +#[test] +fn gemini_desktop_alias_uses_the_shared_code_assist_config() { + let root = fixture(); + let config = root.join("gemini.json"); + fs::write(&config, "{}\n").unwrap(); + let output = Command::new(env!("CARGO_BIN_EXE_spectra")) + .args(["install", "--agent", "gemini-desktop", "--topology-only"]) + .env("SPECTRA_GEMINI_CONFIG", &config) + .output() + .unwrap(); + assert!(output.status.success()); + let value: serde_json::Value = serde_json::from_slice(&fs::read(&config).unwrap()).unwrap(); + assert!(value["mcpServers"]["spectra"].is_object()); + fs::remove_dir_all(root).unwrap(); +} + +#[test] +fn auto_detects_desktop_install_targets() { + let root = fixture(); + let claude = root.join("claude-desktop.json"); + let codex_home = root.join("codex-home"); + let gemini = root.join("gemini.json"); + let output = Command::new(env!("CARGO_BIN_EXE_spectra")) + .args(["install", "--yes", "--topology-only"]) + .env("PATH", &root) + .env("SPECTRA_HOME", &root) + .env("SPECTRA_CLAUDE_DESKTOP_CONFIG", &claude) + .env("SPECTRA_CODEX_DESKTOP", "1") + .env("SPECTRA_CODEX_HOME", &codex_home) + .env("SPECTRA_GEMINI_DESKTOP", "1") + .env("SPECTRA_GEMINI_CONFIG", &gemini) + .output() + .unwrap(); + assert!( + output.status.success(), + "{}", + String::from_utf8_lossy(&output.stderr) + ); + let stdout = String::from_utf8_lossy(&output.stdout); + assert!(stdout.contains("Claude Desktop:")); + assert!(stdout.contains("Codex CLI/Desktop:")); + assert!(stdout.contains("Gemini CLI/Code Assist (VS Code):")); + assert!(claude.exists()); + assert!(codex_home.join("config.toml").exists()); + assert!(gemini.exists()); + fs::remove_dir_all(root).unwrap(); +} + #[cfg(unix)] #[test] fn installer_preserves_symlinked_configuration_files() { diff --git a/crates/spectra-render/Cargo.toml b/crates/spectra-render/Cargo.toml index 48825f6..dc0e941 100644 --- a/crates/spectra-render/Cargo.toml +++ b/crates/spectra-render/Cargo.toml @@ -17,5 +17,5 @@ name = "spectra_render" [dependencies] resvg.workspace = true -spectra-core = { package = "spectra-context-core", version = "0.4.0", path = "../spectra-core" } +spectra-core = { package = "spectra-context-core", version = "0.4.1", path = "../spectra-core" } tiny-skia.workspace = true diff --git a/docs/agent-support.md b/docs/agent-support.md index 92c40d5..b68efb2 100644 --- a/docs/agent-support.md +++ b/docs/agent-support.md @@ -7,16 +7,19 @@ Spectra keeps agent-specific setup behind small adapters. The topology engine an | Agent | Global configuration | MCP schema | Capability | | --- | --- | --- | --- | | Claude Code | `~/.claude.json` and `~/.claude/settings.json` | `mcpServers.spectra` plus lifecycle hooks | `topology+ledger` | +| Claude Desktop | platform application-data `Claude/claude_desktop_config.json` | `mcpServers.spectra` | Topology | | Cursor | `~/.cursor/mcp.json` and `~/.cursor/hooks.json` | `mcpServers.spectra` plus lifecycle hooks | `topology+ledger-partial` | -| Codex | `codex mcp` and `~/.codex/hooks.json` | CLI-managed stdio server plus lifecycle hooks | `topology+ledger` | +| Codex CLI/Desktop | `~/.codex/config.toml` and `~/.codex/hooks.json` | stdio server plus lifecycle hooks | `topology+ledger` | | OpenCode | `$XDG_CONFIG_HOME/opencode/opencode.json` | `mcp.spectra`, local command array | Topology | | Hermes Agent | `$HERMES_HOME/config.yaml` | `mcp_servers.spectra` | Topology | -| Gemini CLI | `~/.gemini/settings.json` | `mcpServers.spectra` plus lifecycle hooks | `topology+ledger` | +| Gemini CLI/Code Assist (VS Code) | `~/.gemini/settings.json` | `mcpServers.spectra` plus lifecycle hooks | `topology+ledger` | | Antigravity | `~/.gemini/config/mcp_config.json` | `mcpServers.spectra` | Topology | | Kiro | `~/.kiro/settings/mcp.json` | `mcpServers.spectra` | Topology | Run `spectra install` in a TTY for a guided scan, target/location choice, conflict preflight, confirmation, and final capability report. Use `--agent ` for one agent, `--agent all` to attempt every adapter, or `--yes` for unattended detected-target installation with global defaults. Non-interactive input requires an explicit target or `--yes` and never waits for prompts. A failure in one target is reported without preventing the other selected targets from being processed. +Claude Desktop is selected explicitly with `--agent claude-desktop` because it has a separate configuration file. Codex Desktop and Gemini Code Assist for VS Code share their respective CLI configurations, so `codex-desktop`, `gemini-desktop`, and `gemini-code-assist` are accepted aliases. The installer detects Codex and Claude desktop installations on macOS, Windows, and Linux, plus the VS Code Gemini Code Assist extension. An existing shared configuration also counts as detection. Claude Desktop receives topology only because its standalone MCP surface does not expose the verified coding-agent lifecycle hooks. + Codex, Claude Code, Gemini CLI, and Cursor also support `--location local --path ` through their documented project configuration layers. Codex uses `.codex/config.toml` and `.codex/hooks.json`; Claude uses `.mcp.json` and `.claude/settings.json`; Gemini uses `.gemini/settings.json`; Cursor uses `.cursor/mcp.json` and `.cursor/hooks.json`. Unsupported local combinations are rejected before mutation. Every adapter provides: @@ -45,6 +48,7 @@ Any other harness can integrate through the stable [`spectra lifecycle ingest` J - [Claude Code MCP](https://code.claude.com/docs/en/mcp) - [Claude Code hooks](https://code.claude.com/docs/en/hooks) +- [Claude Desktop local MCP servers](https://support.claude.com/en/articles/10949351-getting-started-with-local-mcp-servers-on-claude-desktop) - [Cursor MCP](https://docs.cursor.com/context/model-context-protocol) - [Cursor hooks](https://cursor.com/docs/hooks) - [OpenCode configuration](https://dev.opencode.ai/docs/config) diff --git a/docs/code-adapters.md b/docs/code-adapters.md index 25d2605..7bfc510 100644 --- a/docs/code-adapters.md +++ b/docs/code-adapters.md @@ -64,8 +64,8 @@ C, C++, Dart, Lua, and Luau already have registered parser-backed core and seman The common resolver prefers exact-case definitions compatible with the edge type, then falls back to case-insensitive matching. Multiple eligible candidates produce a typed uncertain boundary, preserving ambiguity in the rendered topology. Conventional page routes use local `routes_to` edges, while embedded script symbols remain contained by their component and participate in normal cross-file resolution. -## Parity baseline +## Adapter baseline -The baseline is CodeGraph v1.3.0 as installed when v0.2 development began. Its complete language and extension surface is represented by the 39 adapters above. CUDA and Metal are C++ dialects in CodeGraph; Spectra uses dedicated adapters for their kernel semantics while preserving C++-family resolution. Erlang application manifests (`.app` and `.app.src`) are recognized through path-aware detection. +The v0.2 baseline is represented by the 39 adapters above. Spectra uses dedicated CUDA and Metal adapters for their kernel semantics while preserving C++-family resolution. Erlang application manifests (`.app` and `.app.src`) are recognized through path-aware detection. -Parity is pinned to this baseline so a moving upstream language list cannot silently change the release gate. The registry, extraction fixtures, route-resolution tests, and native-bridge tests enforce that surface. The reviewed v0.2 real-repository gate found all 22 CodeGraph route labels in Spectra and 40 Spectra routes overall across the pinned FastAPI/React, Laravel, NestJS, Spring MVC, and Vapor corpus. The topology corpus and provider-backed multimodal results are recorded in the [v0.2 baseline](../benchmarks/v0.2-baseline.md). Later CodeGraph additions can be adopted deliberately without silently changing the v0.2 contract. +The baseline is pinned so a moving upstream language list cannot silently change the release gate. The registry, extraction fixtures, route-resolution tests, and native-bridge tests enforce that surface. The reviewed v0.2 real-repository gate found all 22 reference route labels in Spectra and 40 Spectra routes overall across the pinned FastAPI/React, Laravel, NestJS, Spring MVC, and Vapor corpus. The topology corpus and provider-backed multimodal results are recorded in the [v0.2 baseline](../benchmarks/v0.2-baseline.md). Later adapter additions can be adopted deliberately without silently changing the v0.2 contract.