From a96143955a754e3d83d9d92b71988818247c8aee Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 18 Sep 2026 00:03:06 +0000 Subject: [PATCH] chore(docs): delete stale mid-delivery markdown Remove unused NEXT.md, archived dated plans, and unreferenced status leftovers. Trim pointers that treated NEXT.md as current delivery authority. Co-authored-by: Zack Jackson --- AGENTS.md | 3 +- docs/V2-OPERATING-MODEL.md | 5 +- ...2-26-codegraph-rust-implementation-plan.md | 3308 ----------------- .../2026-02-26-codegraph-rust-port-design.md | 266 -- ...2-26-multi-language-implementation-plan.md | 1371 ------- ...026-02-26-multi-language-support-design.md | 158 - ...2026-07-01-macos-launchd-daemon-support.md | 544 --- ...26-07-04-tool-cli-args-agent-ergonomics.md | 842 ----- .../plans/2026-08-08-v2-rc-recovery-design.md | 194 - docs/plans/tracedecay-v2/00-plan-set-index.md | 5 +- ...ask-plan-graph-and-multi-agent-executor.md | 2 +- .../39-embedded-grafeo-graph-database.md | 4 +- docs/plans/tracedecay-v2/NEXT.md | 88 - docs/plans/tracedecay-v2/README.md | 15 +- .../audits/ci-triage-plan-2026-08-14.md | 874 ----- .../audits/lane-staleness-2026-08-13.md | 51 - .../v2-implementation-audit-2026-08-14.md | 389 -- .../plans/2026-07-31-one-shot-crate-split.md | 94 - .../plans/2026-08-01-test-support-features.md | 6 +- .../plans/2026-08-08-v2-rc-recovery.md | 454 --- .../2026-08-23-pr663-agent-handoff-prompt.md | 204 - .../2026-08-23-pr663-performance-recovery.md | 449 --- .../superpowers/plans/v2/pr16-remote-brain.md | 56 - docs/superpowers/plans/v2/pr18-public-sdks.md | 67 - .../plans/v2/pr19-cutover-runtime.md | 49 - docs/superpowers/plans/v2/pr20-performance.md | 44 - 26 files changed, 16 insertions(+), 9526 deletions(-) delete mode 100644 docs/plans/2026-02-26-codegraph-rust-implementation-plan.md delete mode 100644 docs/plans/2026-02-26-codegraph-rust-port-design.md delete mode 100644 docs/plans/2026-02-26-multi-language-implementation-plan.md delete mode 100644 docs/plans/2026-02-26-multi-language-support-design.md delete mode 100644 docs/plans/2026-07-01-macos-launchd-daemon-support.md delete mode 100644 docs/plans/2026-07-04-tool-cli-args-agent-ergonomics.md delete mode 100644 docs/plans/2026-08-08-v2-rc-recovery-design.md delete mode 100644 docs/plans/tracedecay-v2/NEXT.md delete mode 100644 docs/plans/tracedecay-v2/audits/ci-triage-plan-2026-08-14.md delete mode 100644 docs/plans/tracedecay-v2/audits/lane-staleness-2026-08-13.md delete mode 100644 docs/plans/tracedecay-v2/audits/v2-implementation-audit-2026-08-14.md delete mode 100644 docs/superpowers/plans/2026-07-31-one-shot-crate-split.md delete mode 100644 docs/superpowers/plans/2026-08-08-v2-rc-recovery.md delete mode 100644 docs/superpowers/plans/2026-08-23-pr663-agent-handoff-prompt.md delete mode 100644 docs/superpowers/plans/2026-08-23-pr663-performance-recovery.md delete mode 100644 docs/superpowers/plans/v2/pr16-remote-brain.md delete mode 100644 docs/superpowers/plans/v2/pr18-public-sdks.md delete mode 100644 docs/superpowers/plans/v2/pr19-cutover-runtime.md delete mode 100644 docs/superpowers/plans/v2/pr20-performance.md diff --git a/AGENTS.md b/AGENTS.md index 2ec7fd6918..0c9d0cbe56 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -149,8 +149,7 @@ unauthorized external action after completing independent, authorized work. production behavior. Wire it now or omit it truthfully. - Comments and docs explain invariants and why; remove narration, stale PR language, and superseded plan authority. `00-plan-set-index.md` is the sole - roadmap precedence; `NEXT.md` records current outcomes only, while historical - plans and benchmarks are archival. + roadmap precedence; historical plans and benchmarks are archival. - Name production modules, APIs, tests, scripts, and CI jobs for durable product capabilities—not PR numbers, milestones, phases, or temporary gates. Keep PR/milestone labels only in clearly archival plans and benchmark provenance. diff --git a/docs/V2-OPERATING-MODEL.md b/docs/V2-OPERATING-MODEL.md index fab26882ec..5e72fed7a8 100644 --- a/docs/V2-OPERATING-MODEL.md +++ b/docs/V2-OPERATING-MODEL.md @@ -4,9 +4,8 @@ This is a concise operator and contributor summary of final-V2 storage, scope, host ingestion, and retrieval. The [V2 roadmap](plans/tracedecay-v2/00-plan-set-index.md) is the sole authority for precedence, rejected mechanisms, delivery order, and acceptance; its numbered -plans own detailed behavior, and -[`NEXT.md`](plans/tracedecay-v2/NEXT.md) reports current delivery status. -Runtime status remains the truth for capabilities not yet delivered. +plans own detailed behavior. Runtime status remains the truth for capabilities +not yet delivered. ## Authorities diff --git a/docs/plans/2026-02-26-codegraph-rust-implementation-plan.md b/docs/plans/2026-02-26-codegraph-rust-implementation-plan.md deleted file mode 100644 index d611abd820..0000000000 --- a/docs/plans/2026-02-26-codegraph-rust-implementation-plan.md +++ /dev/null @@ -1,3308 +0,0 @@ -# CodeGraph Rust Port — Implementation Plan - -> **Archived record — not implementation authority.** This document preserves -> historical intent and evidence. Current requirements come only from the -> `docs/plans/tracedecay-v2/` hierarchy. Exact tests and counts, source-string -> checks, branch/commit/worktree choreography, snapshots, receipts, -> attestations, PR packets, and gate matrices below are not rebuild -> instructions; validate current parser, runtime, and product behavior directly. - -**Goal:** Port CodeGraph from TypeScript to Rust as a single-binary code intelligence tool for Rust codebases. - -**Architecture:** Single crate with module-based structure. SQLite for storage (rusqlite), tree-sitter-rust for AST parsing, ort for ONNX embeddings, clap for CLI, tokio for async MCP server. All data flows through a central `CodeGraph` orchestrator. - -**Tech Stack:** Rust 2021, rusqlite (bundled), tree-sitter + tree-sitter-rust, ort, clap, serde/serde_json, tokio, thiserror, tracing, sha2 - ---- - -## Task 1: Project Scaffold & Core Types - -**Files:** -- Create: `Cargo.toml` -- Create: `src/lib.rs` -- Create: `src/main.rs` -- Create: `src/types.rs` -- Create: `src/errors.rs` -- Test: `tests/types_test.rs` - -**Step 1: Initialize Cargo project** - -```bash -cd /Users/enzolombardi/Code/code-graph -cargo init --name codegraph -``` - -**Step 2: Set up Cargo.toml with all dependencies** - -```toml -[package] -name = "codegraph" -version = "0.1.0" -edition = "2021" -description = "Code intelligence tool that builds a semantic knowledge graph from Rust codebases" - -[dependencies] -rusqlite = { version = "0.31", features = ["bundled", "vtab"] } -tree-sitter = "0.24" -tree-sitter-rust = "0.23" -ort = { version = "2", features = ["load-dynamic"] } -ndarray = "0.16" -clap = { version = "4", features = ["derive"] } -serde = { version = "1", features = ["derive"] } -serde_json = "1" -tokio = { version = "1", features = ["full"] } -thiserror = "2" -tracing = "0.1" -tracing-subscriber = { version = "0.3", features = ["env-filter"] } -sha2 = "0.10" -glob = "0.3" -walkdir = "2" - -[dev-dependencies] -tempfile = "3" -``` - -**Step 3: Write the failing test for core types** - -Create `tests/types_test.rs`: - -```rust -use codegraph::types::*; - -#[test] -fn test_node_kind_display() { - assert_eq!(NodeKind::Function.as_str(), "function"); - assert_eq!(NodeKind::Struct.as_str(), "struct"); - assert_eq!(NodeKind::Impl.as_str(), "impl"); - assert_eq!(NodeKind::Use.as_str(), "use"); -} - -#[test] -fn test_edge_kind_display() { - assert_eq!(EdgeKind::Contains.as_str(), "contains"); - assert_eq!(EdgeKind::Calls.as_str(), "calls"); - assert_eq!(EdgeKind::Implements.as_str(), "implements"); -} - -#[test] -fn test_node_kind_from_str() { - assert_eq!(NodeKind::from_str("function"), Some(NodeKind::Function)); - assert_eq!(NodeKind::from_str("struct"), Some(NodeKind::Struct)); - assert_eq!(NodeKind::from_str("bogus"), None); -} - -#[test] -fn test_edge_kind_from_str() { - assert_eq!(EdgeKind::from_str("calls"), Some(EdgeKind::Calls)); - assert_eq!(EdgeKind::from_str("contains"), Some(EdgeKind::Contains)); - assert_eq!(EdgeKind::from_str("bogus"), None); -} - -#[test] -fn test_visibility_default() { - assert_eq!(Visibility::default(), Visibility::Private); -} - -#[test] -fn test_node_id_generation_is_deterministic() { - let id1 = generate_node_id("src/main.rs", NodeKind::Function, "main", 1); - let id2 = generate_node_id("src/main.rs", NodeKind::Function, "main", 1); - assert_eq!(id1, id2); - - let id3 = generate_node_id("src/main.rs", NodeKind::Function, "other", 1); - assert_ne!(id1, id3); -} - -#[test] -fn test_node_id_format() { - let id = generate_node_id("src/main.rs", NodeKind::Function, "main", 1); - assert!(id.starts_with("function:")); - assert_eq!(id.len(), "function:".len() + 32); // kind: + 32-char hash -} - -#[test] -fn test_node_serde_roundtrip() { - let node = Node { - id: "function:abc123".to_string(), - kind: NodeKind::Function, - name: "main".to_string(), - qualified_name: "src/main.rs::main".to_string(), - file_path: "src/main.rs".to_string(), - start_line: 1, - end_line: 5, - start_column: 0, - end_column: 1, - signature: Some("fn main()".to_string()), - docstring: None, - visibility: Visibility::Private, - is_async: false, - updated_at: 0, - }; - - let json = serde_json::to_string(&node).unwrap(); - let deserialized: Node = serde_json::from_str(&json).unwrap(); - assert_eq!(node.id, deserialized.id); - assert_eq!(node.kind, deserialized.kind); - assert_eq!(node.name, deserialized.name); -} - -#[test] -fn test_edge_serde_roundtrip() { - let edge = Edge { - source: "function:abc".to_string(), - target: "function:def".to_string(), - kind: EdgeKind::Calls, - line: Some(10), - }; - - let json = serde_json::to_string(&edge).unwrap(); - let deserialized: Edge = serde_json::from_str(&json).unwrap(); - assert_eq!(edge.source, deserialized.source); - assert_eq!(edge.kind, deserialized.kind); -} -``` - -**Step 4: Run test to verify it fails** - -```bash -cargo test --test types_test -``` - -Expected: FAIL — module `codegraph::types` not found. - -**Step 5: Implement types.rs** - -Create `src/types.rs`: - -```rust -use serde::{Deserialize, Serialize}; -use sha2::{Digest, Sha256}; - -/// The kind of code symbol a node represents. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] -#[serde(rename_all = "snake_case")] -pub enum NodeKind { - File, - Module, - Struct, - Enum, - EnumVariant, - Trait, - Function, - Method, - Impl, - Const, - Static, - TypeAlias, - Field, - Macro, - Use, -} - -impl NodeKind { - pub fn as_str(&self) -> &'static str { - match self { - Self::File => "file", - Self::Module => "module", - Self::Struct => "struct", - Self::Enum => "enum", - Self::EnumVariant => "enum_variant", - Self::Trait => "trait", - Self::Function => "function", - Self::Method => "method", - Self::Impl => "impl", - Self::Const => "constant", - Self::Static => "static", - Self::TypeAlias => "type_alias", - Self::Field => "field", - Self::Macro => "macro", - Self::Use => "use", - } - } - - pub fn from_str(s: &str) -> Option { - match s { - "file" => Some(Self::File), - "module" => Some(Self::Module), - "struct" => Some(Self::Struct), - "enum" => Some(Self::Enum), - "enum_variant" => Some(Self::EnumVariant), - "trait" => Some(Self::Trait), - "function" => Some(Self::Function), - "method" => Some(Self::Method), - "impl" => Some(Self::Impl), - "constant" => Some(Self::Const), - "static" => Some(Self::Static), - "type_alias" => Some(Self::TypeAlias), - "field" => Some(Self::Field), - "macro" => Some(Self::Macro), - "use" => Some(Self::Use), - _ => None, - } - } -} - -/// The kind of relationship between two nodes. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] -#[serde(rename_all = "snake_case")] -pub enum EdgeKind { - Contains, - Calls, - Uses, - Implements, - TypeOf, - Returns, - DerivesMacro, -} - -impl EdgeKind { - pub fn as_str(&self) -> &'static str { - match self { - Self::Contains => "contains", - Self::Calls => "calls", - Self::Uses => "uses", - Self::Implements => "implements", - Self::TypeOf => "type_of", - Self::Returns => "returns", - Self::DerivesMacro => "derives_macro", - } - } - - pub fn from_str(s: &str) -> Option { - match s { - "contains" => Some(Self::Contains), - "calls" => Some(Self::Calls), - "uses" => Some(Self::Uses), - "implements" => Some(Self::Implements), - "type_of" => Some(Self::TypeOf), - "returns" => Some(Self::Returns), - "derives_macro" => Some(Self::DerivesMacro), - _ => None, - } - } -} - -/// Visibility of a code symbol. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)] -#[serde(rename_all = "snake_case")] -pub enum Visibility { - Pub, - PubCrate, - PubSuper, - #[default] - Private, -} - -/// A code symbol extracted from the AST. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct Node { - pub id: String, - pub kind: NodeKind, - pub name: String, - pub qualified_name: String, - pub file_path: String, - pub start_line: u32, - pub end_line: u32, - pub start_column: u32, - pub end_column: u32, - pub signature: Option, - pub docstring: Option, - pub visibility: Visibility, - pub is_async: bool, - pub updated_at: i64, -} - -/// A relationship between two nodes. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct Edge { - pub source: String, - pub target: String, - pub kind: EdgeKind, - pub line: Option, -} - -/// A tracked file in the project. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct FileRecord { - pub path: String, - pub content_hash: String, - pub size: u64, - pub modified_at: i64, - pub indexed_at: i64, - pub node_count: u32, -} - -/// An unresolved reference found during extraction. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct UnresolvedRef { - pub from_node_id: String, - pub reference_name: String, - pub reference_kind: EdgeKind, - pub line: u32, - pub column: u32, - pub file_path: String, -} - -/// Result of extracting symbols from a single file. -#[derive(Debug, Clone, Default)] -pub struct ExtractionResult { - pub nodes: Vec, - pub edges: Vec, - pub unresolved_refs: Vec, - pub errors: Vec, - pub duration_ms: u64, -} - -/// A subset of the graph. -#[derive(Debug, Clone, Default, Serialize, Deserialize)] -pub struct Subgraph { - pub nodes: Vec, - pub edges: Vec, - pub roots: Vec, -} - -/// A search result with relevance score. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct SearchResult { - pub node: Node, - pub score: f64, -} - -/// Options for graph traversal. -#[derive(Debug, Clone)] -pub struct TraversalOptions { - pub max_depth: usize, - pub edge_kinds: Vec, - pub node_kinds: Vec, - pub direction: TraversalDirection, - pub limit: usize, - pub include_start: bool, -} - -impl Default for TraversalOptions { - fn default() -> Self { - Self { - max_depth: usize::MAX, - edge_kinds: Vec::new(), - node_kinds: Vec::new(), - direction: TraversalDirection::Outgoing, - limit: usize::MAX, - include_start: true, - } - } -} - -/// Direction of graph traversal. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum TraversalDirection { - Outgoing, - Incoming, - Both, -} - -/// Statistics about the graph database. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct GraphStats { - pub node_count: usize, - pub edge_count: usize, - pub file_count: usize, - pub nodes_by_kind: Vec<(String, usize)>, - pub edges_by_kind: Vec<(String, usize)>, - pub db_size_bytes: u64, - pub last_updated: i64, -} - -/// Options for building task context. -#[derive(Debug, Clone)] -pub struct BuildContextOptions { - pub max_nodes: usize, - pub max_code_blocks: usize, - pub max_code_block_size: usize, - pub include_code: bool, - pub format: OutputFormat, - pub search_limit: usize, - pub traversal_depth: usize, - pub min_score: f64, -} - -impl Default for BuildContextOptions { - fn default() -> Self { - Self { - max_nodes: 20, - max_code_blocks: 5, - max_code_block_size: 1500, - include_code: true, - format: OutputFormat::Markdown, - search_limit: 3, - traversal_depth: 1, - min_score: 0.3, - } - } -} - -/// Output format for context. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum OutputFormat { - Markdown, - Json, -} - -/// Task context built for an AI query. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct TaskContext { - pub query: String, - pub summary: String, - pub subgraph: Subgraph, - pub entry_points: Vec, - pub code_blocks: Vec, - pub related_files: Vec, -} - -/// A block of source code. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct CodeBlock { - pub content: String, - pub file_path: String, - pub start_line: u32, - pub end_line: u32, - pub node_id: Option, -} - -/// Generates a deterministic node ID from its identifying properties. -pub fn generate_node_id(file_path: &str, kind: NodeKind, name: &str, line: u32) -> String { - let input = format!("{}:{}:{}:{}", file_path, kind.as_str(), name, line); - let mut hasher = Sha256::new(); - hasher.update(input.as_bytes()); - let hash = hasher.finalize(); - let hex = hex::encode(hash); - format!("{}:{}", kind.as_str(), &hex[..32]) -} - -/// Result of reference resolution. -#[derive(Debug, Clone, Default)] -pub struct ResolutionResult { - pub resolved: Vec, - pub unresolved: Vec, - pub total: usize, - pub resolved_count: usize, -} - -/// A resolved reference. -#[derive(Debug, Clone)] -pub struct ResolvedRef { - pub original: UnresolvedRef, - pub target_node_id: String, - pub confidence: f64, - pub resolved_by: String, -} -``` - -**Step 6: Implement errors.rs** - -Create `src/errors.rs`: - -```rust -use thiserror::Error; - -#[derive(Error, Debug)] -pub enum CodeGraphError { - #[error("file error: {message} (path: {path})")] - File { message: String, path: String }, - - #[error("parse error: {message} (path: {path}, line: {line:?})")] - Parse { - message: String, - path: String, - line: Option, - }, - - #[error("database error: {message} (operation: {operation})")] - Database { message: String, operation: String }, - - #[error("search error: {message} (query: {query})")] - Search { message: String, query: String }, - - #[error("config error: {message}")] - Config { message: String }, - - #[error("vector error: {message}")] - Vector { message: String }, - - #[error("io error: {0}")] - Io(#[from] std::io::Error), - - #[error("sqlite error: {0}")] - Sqlite(#[from] rusqlite::Error), - - #[error("json error: {0}")] - Json(#[from] serde_json::Error), -} - -pub type Result = std::result::Result; -``` - -**Step 7: Wire up lib.rs** - -Create `src/lib.rs`: - -```rust -pub mod errors; -pub mod types; -``` - -Add `hex` dependency to `Cargo.toml` under `[dependencies]`: - -```toml -hex = "0.4" -``` - -**Step 8: Create minimal main.rs** - -Create `src/main.rs`: - -```rust -fn main() { - println!("codegraph - code intelligence for Rust"); -} -``` - -**Step 9: Run tests to verify they pass** - -```bash -cargo test --test types_test -``` - -Expected: All tests PASS. - -**Step 10: Commit** - -```bash -git init -git add Cargo.toml src/ tests/ -git commit -m "feat: scaffold project with core types and error handling" -``` - ---- - -## Task 2: Configuration Module - -**Files:** -- Create: `src/config.rs` -- Modify: `src/lib.rs` -- Test: `tests/config_test.rs` - -**Step 1: Write the failing test** - -Create `tests/config_test.rs`: - -```rust -use codegraph::config::*; -use tempfile::TempDir; - -#[test] -fn test_default_config_has_rust_patterns() { - let config = CodeGraphConfig::default(); - assert!(config.include.iter().any(|p| p == "**/*.rs")); - assert!(config.exclude.iter().any(|p| p == "target/**")); -} - -#[test] -fn test_save_and_load_config() { - let dir = TempDir::new().unwrap(); - let config = CodeGraphConfig::default(); - save_config(dir.path(), &config).unwrap(); - let loaded = load_config(dir.path()).unwrap(); - assert_eq!(config.version, loaded.version); - assert_eq!(config.include, loaded.include); -} - -#[test] -fn test_should_include_file() { - let config = CodeGraphConfig::default(); - assert!(should_include_file("src/main.rs", &config)); - assert!(!should_include_file("target/debug/foo", &config)); - assert!(!should_include_file("node_modules/foo.rs", &config)); -} - -#[test] -fn test_codegraph_dir_creation() { - let dir = TempDir::new().unwrap(); - let cg_dir = get_codegraph_dir(dir.path()); - assert!(cg_dir.ends_with(".codegraph")); -} - -#[test] -fn test_config_serde_roundtrip() { - let config = CodeGraphConfig::default(); - let json = serde_json::to_string_pretty(&config).unwrap(); - let deserialized: CodeGraphConfig = serde_json::from_str(&json).unwrap(); - assert_eq!(config.version, deserialized.version); - assert_eq!(config.max_file_size, deserialized.max_file_size); -} -``` - -**Step 2: Run test to verify it fails** - -```bash -cargo test --test config_test -``` - -Expected: FAIL — module `codegraph::config` not found. - -**Step 3: Implement config.rs** - -Create `src/config.rs`: - -```rust -use crate::errors::{CodeGraphError, Result}; -use serde::{Deserialize, Serialize}; -use std::path::{Path, PathBuf}; - -const CONFIG_FILENAME: &str = "config.json"; -const CODEGRAPH_DIR: &str = ".codegraph"; - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct CodeGraphConfig { - pub version: u32, - pub root_dir: String, - pub include: Vec, - pub exclude: Vec, - pub max_file_size: u64, - pub extract_docstrings: bool, - pub track_call_sites: bool, - pub enable_embeddings: bool, -} - -impl Default for CodeGraphConfig { - fn default() -> Self { - Self { - version: 1, - root_dir: ".".to_string(), - include: vec!["**/*.rs".to_string()], - exclude: vec![ - "target/**".to_string(), - ".git/**".to_string(), - ".codegraph/**".to_string(), - "node_modules/**".to_string(), - "vendor/**".to_string(), - "**/*.min.*".to_string(), - ], - max_file_size: 1_048_576, - extract_docstrings: true, - track_call_sites: true, - enable_embeddings: false, - } - } -} - -/// Get the path to the .codegraph directory for a project. -pub fn get_codegraph_dir(project_root: &Path) -> PathBuf { - project_root.join(CODEGRAPH_DIR) -} - -/// Get the path to the config file. -pub fn get_config_path(project_root: &Path) -> PathBuf { - get_codegraph_dir(project_root).join(CONFIG_FILENAME) -} - -/// Load configuration from disk, falling back to defaults. -pub fn load_config(project_root: &Path) -> Result { - let config_path = get_config_path(project_root); - if !config_path.exists() { - return Ok(CodeGraphConfig::default()); - } - let contents = std::fs::read_to_string(&config_path).map_err(|e| CodeGraphError::Config { - message: format!("failed to read config: {}", e), - })?; - let config: CodeGraphConfig = - serde_json::from_str(&contents).map_err(|e| CodeGraphError::Config { - message: format!("failed to parse config: {}", e), - })?; - Ok(config) -} - -/// Save configuration to disk atomically. -pub fn save_config(project_root: &Path, config: &CodeGraphConfig) -> Result<()> { - let cg_dir = get_codegraph_dir(project_root); - std::fs::create_dir_all(&cg_dir)?; - - let config_path = get_config_path(project_root); - let tmp_path = config_path.with_extension("json.tmp"); - - let json = - serde_json::to_string_pretty(config).map_err(|e| CodeGraphError::Config { - message: format!("failed to serialize config: {}", e), - })?; - - std::fs::write(&tmp_path, &json)?; - std::fs::rename(&tmp_path, &config_path)?; - Ok(()) -} - -/// Check if a file path should be included based on config patterns. -pub fn should_include_file(file_path: &str, config: &CodeGraphConfig) -> bool { - let path = Path::new(file_path); - - // Check excludes first - for pattern in &config.exclude { - if glob_match(pattern, file_path) { - return false; - } - } - - // Check includes - for pattern in &config.include { - if glob_match(pattern, file_path) { - return true; - } - } - - false -} - -/// Simple glob matching supporting ** and * patterns. -fn glob_match(pattern: &str, path: &str) -> bool { - let glob = glob::Pattern::new(pattern); - match glob { - Ok(g) => g.matches_with( - path, - glob::MatchOptions { - case_sensitive: true, - require_literal_separator: false, - require_literal_leading_dot: false, - }, - ), - Err(_) => false, - } -} -``` - -**Step 4: Add to lib.rs** - -```rust -pub mod config; -pub mod errors; -pub mod types; -``` - -**Step 5: Run tests to verify they pass** - -```bash -cargo test --test config_test -``` - -Expected: All PASS. - -**Step 6: Commit** - -```bash -git add src/config.rs src/lib.rs tests/config_test.rs -git commit -m "feat: add configuration module with glob-based file filtering" -``` - ---- - -## Task 3: SQLite Database Layer - -**Files:** -- Create: `src/db/mod.rs` -- Create: `src/db/connection.rs` -- Create: `src/db/queries.rs` -- Create: `src/db/schema.sql` -- Modify: `src/lib.rs` -- Test: `tests/db_test.rs` - -**Step 1: Write the failing test** - -Create `tests/db_test.rs`: - -```rust -use codegraph::db::*; -use codegraph::types::*; -use tempfile::TempDir; - -#[test] -fn test_initialize_creates_database() { - let dir = TempDir::new().unwrap(); - let db_path = dir.path().join("codegraph.db"); - let conn = Database::initialize(&db_path).unwrap(); - assert!(db_path.exists()); - conn.close(); -} - -#[test] -fn test_insert_and_get_node() { - let dir = TempDir::new().unwrap(); - let db_path = dir.path().join("codegraph.db"); - let db = Database::initialize(&db_path).unwrap(); - - let node = Node { - id: "function:abc123def456abc123def456abc12345".to_string(), - kind: NodeKind::Function, - name: "main".to_string(), - qualified_name: "src/main.rs::main".to_string(), - file_path: "src/main.rs".to_string(), - start_line: 1, - end_line: 5, - start_column: 0, - end_column: 1, - signature: Some("fn main()".to_string()), - docstring: None, - visibility: Visibility::Private, - is_async: false, - updated_at: 1000, - }; - - db.insert_node(&node).unwrap(); - - let fetched = db.get_node_by_id(&node.id).unwrap(); - assert!(fetched.is_some()); - let fetched = fetched.unwrap(); - assert_eq!(fetched.name, "main"); - assert_eq!(fetched.kind, NodeKind::Function); - assert_eq!(fetched.signature, Some("fn main()".to_string())); - - db.close(); -} - -#[test] -fn test_insert_and_get_edge() { - let dir = TempDir::new().unwrap(); - let db_path = dir.path().join("codegraph.db"); - let db = Database::initialize(&db_path).unwrap(); - - let node1 = Node { - id: "function:aaa".to_string(), - kind: NodeKind::Function, - name: "caller".to_string(), - qualified_name: "caller".to_string(), - file_path: "src/lib.rs".to_string(), - start_line: 1, end_line: 5, - start_column: 0, end_column: 1, - signature: None, docstring: None, - visibility: Visibility::Pub, - is_async: false, updated_at: 0, - }; - let node2 = Node { - id: "function:bbb".to_string(), - kind: NodeKind::Function, - name: "callee".to_string(), - qualified_name: "callee".to_string(), - file_path: "src/lib.rs".to_string(), - start_line: 10, end_line: 15, - start_column: 0, end_column: 1, - signature: None, docstring: None, - visibility: Visibility::Pub, - is_async: false, updated_at: 0, - }; - db.insert_node(&node1).unwrap(); - db.insert_node(&node2).unwrap(); - - let edge = Edge { - source: "function:aaa".to_string(), - target: "function:bbb".to_string(), - kind: EdgeKind::Calls, - line: Some(3), - }; - db.insert_edge(&edge).unwrap(); - - let outgoing = db.get_outgoing_edges("function:aaa", &[]).unwrap(); - assert_eq!(outgoing.len(), 1); - assert_eq!(outgoing[0].kind, EdgeKind::Calls); - - let incoming = db.get_incoming_edges("function:bbb", &[]).unwrap(); - assert_eq!(incoming.len(), 1); - assert_eq!(incoming[0].source, "function:aaa"); - - db.close(); -} - -#[test] -fn test_upsert_file() { - let dir = TempDir::new().unwrap(); - let db_path = dir.path().join("codegraph.db"); - let db = Database::initialize(&db_path).unwrap(); - - let file = FileRecord { - path: "src/main.rs".to_string(), - content_hash: "abc123".to_string(), - size: 1024, - modified_at: 1000, - indexed_at: 1001, - node_count: 5, - }; - db.upsert_file(&file).unwrap(); - - let fetched = db.get_file("src/main.rs").unwrap(); - assert!(fetched.is_some()); - assert_eq!(fetched.unwrap().content_hash, "abc123"); - - db.close(); -} - -#[test] -fn test_fts_search() { - let dir = TempDir::new().unwrap(); - let db_path = dir.path().join("codegraph.db"); - let db = Database::initialize(&db_path).unwrap(); - - let node = Node { - id: "function:search_test".to_string(), - kind: NodeKind::Function, - name: "process_request".to_string(), - qualified_name: "server::process_request".to_string(), - file_path: "src/server.rs".to_string(), - start_line: 10, end_line: 20, - start_column: 0, end_column: 1, - signature: Some("fn process_request(req: Request) -> Response".to_string()), - docstring: Some("Processes an incoming HTTP request".to_string()), - visibility: Visibility::Pub, - is_async: true, updated_at: 0, - }; - db.insert_node(&node).unwrap(); - - let results = db.search_nodes("process", 10).unwrap(); - assert!(!results.is_empty()); - assert_eq!(results[0].node.name, "process_request"); - - db.close(); -} - -#[test] -fn test_get_stats() { - let dir = TempDir::new().unwrap(); - let db_path = dir.path().join("codegraph.db"); - let db = Database::initialize(&db_path).unwrap(); - - let node = Node { - id: "function:stats_test".to_string(), - kind: NodeKind::Function, - name: "test_fn".to_string(), - qualified_name: "test_fn".to_string(), - file_path: "src/lib.rs".to_string(), - start_line: 1, end_line: 5, - start_column: 0, end_column: 1, - signature: None, docstring: None, - visibility: Visibility::Pub, - is_async: false, updated_at: 0, - }; - db.insert_node(&node).unwrap(); - - let stats = db.get_stats().unwrap(); - assert_eq!(stats.node_count, 1); - - db.close(); -} - -#[test] -fn test_delete_nodes_by_file() { - let dir = TempDir::new().unwrap(); - let db_path = dir.path().join("codegraph.db"); - let db = Database::initialize(&db_path).unwrap(); - - let node = Node { - id: "function:del_test".to_string(), - kind: NodeKind::Function, - name: "to_delete".to_string(), - qualified_name: "to_delete".to_string(), - file_path: "src/old.rs".to_string(), - start_line: 1, end_line: 5, - start_column: 0, end_column: 1, - signature: None, docstring: None, - visibility: Visibility::Pub, - is_async: false, updated_at: 0, - }; - db.insert_node(&node).unwrap(); - assert!(db.get_node_by_id("function:del_test").unwrap().is_some()); - - db.delete_nodes_by_file("src/old.rs").unwrap(); - assert!(db.get_node_by_id("function:del_test").unwrap().is_none()); - - db.close(); -} - -#[test] -fn test_unresolved_refs() { - let dir = TempDir::new().unwrap(); - let db_path = dir.path().join("codegraph.db"); - let db = Database::initialize(&db_path).unwrap(); - - let uref = UnresolvedRef { - from_node_id: "function:caller".to_string(), - reference_name: "some_fn".to_string(), - reference_kind: EdgeKind::Calls, - line: 10, - column: 4, - file_path: "src/lib.rs".to_string(), - }; - db.insert_unresolved_ref(&uref).unwrap(); - - let refs = db.get_unresolved_refs().unwrap(); - assert_eq!(refs.len(), 1); - assert_eq!(refs[0].reference_name, "some_fn"); - - db.close(); -} -``` - -**Step 2: Run test to verify it fails** - -```bash -cargo test --test db_test -``` - -Expected: FAIL — module `codegraph::db` not found. - -**Step 3: Create the SQL schema** - -Create `src/db/schema.sql`: - -```sql --- Schema version tracking -CREATE TABLE IF NOT EXISTS schema_versions ( - version INTEGER PRIMARY KEY, - applied_at INTEGER NOT NULL, - description TEXT -); - --- Code symbols -CREATE TABLE IF NOT EXISTS nodes ( - id TEXT PRIMARY KEY, - kind TEXT NOT NULL, - name TEXT NOT NULL, - qualified_name TEXT NOT NULL, - file_path TEXT NOT NULL, - start_line INTEGER NOT NULL, - end_line INTEGER NOT NULL, - start_column INTEGER NOT NULL, - end_column INTEGER NOT NULL, - docstring TEXT, - signature TEXT, - visibility TEXT NOT NULL DEFAULT 'private', - is_async INTEGER NOT NULL DEFAULT 0, - updated_at INTEGER NOT NULL -); - --- Relationships between nodes -CREATE TABLE IF NOT EXISTS edges ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - source TEXT NOT NULL, - target TEXT NOT NULL, - kind TEXT NOT NULL, - line INTEGER, - FOREIGN KEY (source) REFERENCES nodes(id) ON DELETE CASCADE, - FOREIGN KEY (target) REFERENCES nodes(id) ON DELETE CASCADE -); - --- Tracked files -CREATE TABLE IF NOT EXISTS files ( - path TEXT PRIMARY KEY, - content_hash TEXT NOT NULL, - size INTEGER NOT NULL, - modified_at INTEGER NOT NULL, - indexed_at INTEGER NOT NULL, - node_count INTEGER NOT NULL DEFAULT 0 -); - --- Pending reference resolution -CREATE TABLE IF NOT EXISTS unresolved_refs ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - from_node_id TEXT NOT NULL, - reference_name TEXT NOT NULL, - reference_kind TEXT NOT NULL, - line INTEGER NOT NULL, - col INTEGER NOT NULL, - file_path TEXT NOT NULL, - FOREIGN KEY (from_node_id) REFERENCES nodes(id) ON DELETE CASCADE -); - --- Embedding vectors -CREATE TABLE IF NOT EXISTS vectors ( - node_id TEXT PRIMARY KEY, - embedding BLOB NOT NULL, - model TEXT NOT NULL, - created_at INTEGER NOT NULL, - FOREIGN KEY (node_id) REFERENCES nodes(id) ON DELETE CASCADE -); - --- Full-text search on nodes -CREATE VIRTUAL TABLE IF NOT EXISTS nodes_fts USING fts5( - name, qualified_name, docstring, signature, - content='nodes', - content_rowid='rowid' -); - --- Triggers to keep FTS in sync -CREATE TRIGGER IF NOT EXISTS nodes_ai AFTER INSERT ON nodes BEGIN - INSERT INTO nodes_fts(rowid, name, qualified_name, docstring, signature) - VALUES (new.rowid, new.name, new.qualified_name, new.docstring, new.signature); -END; - -CREATE TRIGGER IF NOT EXISTS nodes_ad AFTER DELETE ON nodes BEGIN - INSERT INTO nodes_fts(nodes_fts, rowid, name, qualified_name, docstring, signature) - VALUES ('delete', old.rowid, old.name, old.qualified_name, old.docstring, old.signature); -END; - -CREATE TRIGGER IF NOT EXISTS nodes_au AFTER UPDATE ON nodes BEGIN - INSERT INTO nodes_fts(nodes_fts, rowid, name, qualified_name, docstring, signature) - VALUES ('delete', old.rowid, old.name, old.qualified_name, old.docstring, old.signature); - INSERT INTO nodes_fts(rowid, name, qualified_name, docstring, signature) - VALUES (new.rowid, new.name, new.qualified_name, new.docstring, new.signature); -END; - --- Indexes -CREATE INDEX IF NOT EXISTS idx_nodes_kind ON nodes(kind); -CREATE INDEX IF NOT EXISTS idx_nodes_name ON nodes(name); -CREATE INDEX IF NOT EXISTS idx_nodes_qualified_name ON nodes(qualified_name); -CREATE INDEX IF NOT EXISTS idx_nodes_file_path ON nodes(file_path); -CREATE INDEX IF NOT EXISTS idx_nodes_file_line ON nodes(file_path, start_line); - -CREATE INDEX IF NOT EXISTS idx_edges_source ON edges(source); -CREATE INDEX IF NOT EXISTS idx_edges_target ON edges(target); -CREATE INDEX IF NOT EXISTS idx_edges_kind ON edges(kind); -CREATE INDEX IF NOT EXISTS idx_edges_source_kind ON edges(source, kind); -CREATE INDEX IF NOT EXISTS idx_edges_target_kind ON edges(target, kind); - -CREATE INDEX IF NOT EXISTS idx_unresolved_from ON unresolved_refs(from_node_id); -CREATE INDEX IF NOT EXISTS idx_unresolved_name ON unresolved_refs(reference_name); -CREATE INDEX IF NOT EXISTS idx_unresolved_file ON unresolved_refs(file_path); - --- Record initial schema version -INSERT OR IGNORE INTO schema_versions (version, applied_at, description) -VALUES (1, strftime('%s', 'now'), 'Initial schema'); -``` - -**Step 4: Implement connection.rs and queries** - -Create `src/db/mod.rs`: - -```rust -mod connection; -mod queries; - -pub use connection::Database; -``` - -Create `src/db/connection.rs`: - -```rust -use crate::errors::{CodeGraphError, Result}; -use rusqlite::Connection; -use std::path::Path; - -pub struct Database { - conn: Connection, -} - -impl Database { - /// Initialize a new database with schema. - pub fn initialize(db_path: &Path) -> Result { - if let Some(parent) = db_path.parent() { - std::fs::create_dir_all(parent)?; - } - - let conn = Connection::open(db_path).map_err(|e| CodeGraphError::Database { - message: e.to_string(), - operation: "initialize".to_string(), - })?; - - // Set pragmas for performance - conn.execute_batch( - "PRAGMA journal_mode = WAL; - PRAGMA foreign_keys = ON; - PRAGMA busy_timeout = 120000; - PRAGMA synchronous = NORMAL; - PRAGMA cache_size = -65536; - PRAGMA temp_store = MEMORY; - PRAGMA mmap_size = 268435456;", - ) - .map_err(|e| CodeGraphError::Database { - message: e.to_string(), - operation: "set_pragmas".to_string(), - })?; - - // Apply schema - let schema = include_str!("schema.sql"); - conn.execute_batch(schema).map_err(|e| CodeGraphError::Database { - message: e.to_string(), - operation: "apply_schema".to_string(), - })?; - - Ok(Self { conn }) - } - - /// Open an existing database. - pub fn open(db_path: &Path) -> Result { - if !db_path.exists() { - return Err(CodeGraphError::Database { - message: format!("database not found: {}", db_path.display()), - operation: "open".to_string(), - }); - } - Self::initialize(db_path) - } - - /// Get a reference to the underlying connection. - pub(crate) fn conn(&self) -> &Connection { - &self.conn - } - - /// Close the database connection. - pub fn close(self) { - drop(self.conn); - } - - /// Optimize the database (VACUUM + ANALYZE). - pub fn optimize(&self) -> Result<()> { - self.conn - .execute_batch("VACUUM; ANALYZE;") - .map_err(|e| CodeGraphError::Database { - message: e.to_string(), - operation: "optimize".to_string(), - }) - } - - /// Get database file size in bytes. - pub fn size(&self) -> Result { - let path: String = - self.conn - .query_row("PRAGMA database_list", [], |row| row.get(2)) - .map_err(|e| CodeGraphError::Database { - message: e.to_string(), - operation: "get_size".to_string(), - })?; - Ok(std::fs::metadata(path).map(|m| m.len()).unwrap_or(0)) - } -} -``` - -Create `src/db/queries.rs` — This is a larger file implementing all query methods on `Database`: - -```rust -use crate::errors::{CodeGraphError, Result}; -use crate::types::*; -use rusqlite::params; - -use super::Database; - -impl Database { - // ── Node Operations ── - - pub fn insert_node(&self, node: &Node) -> Result<()> { - self.conn().execute( - "INSERT OR REPLACE INTO nodes - (id, kind, name, qualified_name, file_path, - start_line, end_line, start_column, end_column, - docstring, signature, visibility, is_async, updated_at) - VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14)", - params![ - node.id, - node.kind.as_str(), - node.name, - node.qualified_name, - node.file_path, - node.start_line, - node.end_line, - node.start_column, - node.end_column, - node.docstring, - node.signature, - visibility_to_str(node.visibility), - node.is_async as i32, - node.updated_at, - ], - ).map_err(|e| CodeGraphError::Database { - message: e.to_string(), - operation: "insert_node".to_string(), - })?; - Ok(()) - } - - pub fn insert_nodes(&self, nodes: &[Node]) -> Result<()> { - let tx = self.conn().unchecked_transaction().map_err(|e| CodeGraphError::Database { - message: e.to_string(), - operation: "insert_nodes_tx".to_string(), - })?; - for node in nodes { - self.insert_node(node)?; - } - tx.commit().map_err(|e| CodeGraphError::Database { - message: e.to_string(), - operation: "insert_nodes_commit".to_string(), - })?; - Ok(()) - } - - pub fn get_node_by_id(&self, id: &str) -> Result> { - let mut stmt = self.conn().prepare( - "SELECT id, kind, name, qualified_name, file_path, - start_line, end_line, start_column, end_column, - docstring, signature, visibility, is_async, updated_at - FROM nodes WHERE id = ?1" - ).map_err(|e| CodeGraphError::Database { - message: e.to_string(), - operation: "get_node_by_id".to_string(), - })?; - - let node = stmt.query_row(params![id], row_to_node).optional().map_err(|e| CodeGraphError::Database { - message: e.to_string(), - operation: "get_node_by_id".to_string(), - })?; - - Ok(node) - } - - pub fn get_nodes_by_file(&self, file_path: &str) -> Result> { - let mut stmt = self.conn().prepare( - "SELECT id, kind, name, qualified_name, file_path, - start_line, end_line, start_column, end_column, - docstring, signature, visibility, is_async, updated_at - FROM nodes WHERE file_path = ?1 ORDER BY start_line" - ).map_err(|e| CodeGraphError::Database { - message: e.to_string(), - operation: "get_nodes_by_file".to_string(), - })?; - - let nodes = stmt.query_map(params![file_path], row_to_node) - .map_err(|e| CodeGraphError::Database { - message: e.to_string(), - operation: "get_nodes_by_file".to_string(), - })? - .filter_map(|r| r.ok()) - .collect(); - - Ok(nodes) - } - - pub fn get_nodes_by_kind(&self, kind: NodeKind) -> Result> { - let mut stmt = self.conn().prepare( - "SELECT id, kind, name, qualified_name, file_path, - start_line, end_line, start_column, end_column, - docstring, signature, visibility, is_async, updated_at - FROM nodes WHERE kind = ?1" - ).map_err(|e| CodeGraphError::Database { - message: e.to_string(), - operation: "get_nodes_by_kind".to_string(), - })?; - - let nodes = stmt.query_map(params![kind.as_str()], row_to_node) - .map_err(|e| CodeGraphError::Database { - message: e.to_string(), - operation: "get_nodes_by_kind".to_string(), - })? - .filter_map(|r| r.ok()) - .collect(); - - Ok(nodes) - } - - pub fn get_all_nodes(&self) -> Result> { - let mut stmt = self.conn().prepare( - "SELECT id, kind, name, qualified_name, file_path, - start_line, end_line, start_column, end_column, - docstring, signature, visibility, is_async, updated_at - FROM nodes" - ).map_err(|e| CodeGraphError::Database { - message: e.to_string(), - operation: "get_all_nodes".to_string(), - })?; - - let nodes = stmt.query_map([], row_to_node) - .map_err(|e| CodeGraphError::Database { - message: e.to_string(), - operation: "get_all_nodes".to_string(), - })? - .filter_map(|r| r.ok()) - .collect(); - - Ok(nodes) - } - - pub fn delete_nodes_by_file(&self, file_path: &str) -> Result<()> { - // Delete edges referencing these nodes first - self.conn().execute( - "DELETE FROM edges WHERE source IN (SELECT id FROM nodes WHERE file_path = ?1) - OR target IN (SELECT id FROM nodes WHERE file_path = ?1)", - params![file_path], - ).map_err(|e| CodeGraphError::Database { - message: e.to_string(), - operation: "delete_edges_by_file".to_string(), - })?; - - // Delete unresolved refs - self.conn().execute( - "DELETE FROM unresolved_refs WHERE from_node_id IN (SELECT id FROM nodes WHERE file_path = ?1)", - params![file_path], - ).map_err(|e| CodeGraphError::Database { - message: e.to_string(), - operation: "delete_unresolved_by_file".to_string(), - })?; - - // Delete vectors - self.conn().execute( - "DELETE FROM vectors WHERE node_id IN (SELECT id FROM nodes WHERE file_path = ?1)", - params![file_path], - ).map_err(|e| CodeGraphError::Database { - message: e.to_string(), - operation: "delete_vectors_by_file".to_string(), - })?; - - // Delete nodes - self.conn().execute( - "DELETE FROM nodes WHERE file_path = ?1", - params![file_path], - ).map_err(|e| CodeGraphError::Database { - message: e.to_string(), - operation: "delete_nodes_by_file".to_string(), - })?; - - Ok(()) - } - - // ── Edge Operations ── - - pub fn insert_edge(&self, edge: &Edge) -> Result<()> { - self.conn().execute( - "INSERT INTO edges (source, target, kind, line) VALUES (?1, ?2, ?3, ?4)", - params![edge.source, edge.target, edge.kind.as_str(), edge.line], - ).map_err(|e| CodeGraphError::Database { - message: e.to_string(), - operation: "insert_edge".to_string(), - })?; - Ok(()) - } - - pub fn insert_edges(&self, edges: &[Edge]) -> Result<()> { - let tx = self.conn().unchecked_transaction().map_err(|e| CodeGraphError::Database { - message: e.to_string(), - operation: "insert_edges_tx".to_string(), - })?; - for edge in edges { - self.insert_edge(edge)?; - } - tx.commit().map_err(|e| CodeGraphError::Database { - message: e.to_string(), - operation: "insert_edges_commit".to_string(), - })?; - Ok(()) - } - - pub fn get_outgoing_edges(&self, source_id: &str, kinds: &[EdgeKind]) -> Result> { - if kinds.is_empty() { - let mut stmt = self.conn().prepare( - "SELECT source, target, kind, line FROM edges WHERE source = ?1" - ).map_err(|e| CodeGraphError::Database { - message: e.to_string(), - operation: "get_outgoing_edges".to_string(), - })?; - - let edges = stmt.query_map(params![source_id], row_to_edge) - .map_err(|e| CodeGraphError::Database { - message: e.to_string(), - operation: "get_outgoing_edges".to_string(), - })? - .filter_map(|r| r.ok()) - .collect(); - Ok(edges) - } else { - let kind_strs: Vec<&str> = kinds.iter().map(|k| k.as_str()).collect(); - let placeholders: Vec = (0..kind_strs.len()).map(|i| format!("?{}", i + 2)).collect(); - let sql = format!( - "SELECT source, target, kind, line FROM edges WHERE source = ?1 AND kind IN ({})", - placeholders.join(", ") - ); - let mut stmt = self.conn().prepare(&sql).map_err(|e| CodeGraphError::Database { - message: e.to_string(), - operation: "get_outgoing_edges_filtered".to_string(), - })?; - - let mut params_vec: Vec> = vec![Box::new(source_id.to_string())]; - for k in &kind_strs { - params_vec.push(Box::new(k.to_string())); - } - - let edges = stmt.query_map(rusqlite::params_from_iter(params_vec.iter().map(|b| b.as_ref())), row_to_edge) - .map_err(|e| CodeGraphError::Database { - message: e.to_string(), - operation: "get_outgoing_edges_filtered".to_string(), - })? - .filter_map(|r| r.ok()) - .collect(); - Ok(edges) - } - } - - pub fn get_incoming_edges(&self, target_id: &str, kinds: &[EdgeKind]) -> Result> { - if kinds.is_empty() { - let mut stmt = self.conn().prepare( - "SELECT source, target, kind, line FROM edges WHERE target = ?1" - ).map_err(|e| CodeGraphError::Database { - message: e.to_string(), - operation: "get_incoming_edges".to_string(), - })?; - - let edges = stmt.query_map(params![target_id], row_to_edge) - .map_err(|e| CodeGraphError::Database { - message: e.to_string(), - operation: "get_incoming_edges".to_string(), - })? - .filter_map(|r| r.ok()) - .collect(); - Ok(edges) - } else { - let kind_strs: Vec<&str> = kinds.iter().map(|k| k.as_str()).collect(); - let placeholders: Vec = (0..kind_strs.len()).map(|i| format!("?{}", i + 2)).collect(); - let sql = format!( - "SELECT source, target, kind, line FROM edges WHERE target = ?1 AND kind IN ({})", - placeholders.join(", ") - ); - let mut stmt = self.conn().prepare(&sql).map_err(|e| CodeGraphError::Database { - message: e.to_string(), - operation: "get_incoming_edges_filtered".to_string(), - })?; - - let mut params_vec: Vec> = vec![Box::new(target_id.to_string())]; - for k in &kind_strs { - params_vec.push(Box::new(k.to_string())); - } - - let edges = stmt.query_map(rusqlite::params_from_iter(params_vec.iter().map(|b| b.as_ref())), row_to_edge) - .map_err(|e| CodeGraphError::Database { - message: e.to_string(), - operation: "get_incoming_edges_filtered".to_string(), - })? - .filter_map(|r| r.ok()) - .collect(); - Ok(edges) - } - } - - pub fn delete_edges_by_source(&self, source_id: &str) -> Result<()> { - self.conn().execute( - "DELETE FROM edges WHERE source = ?1", - params![source_id], - ).map_err(|e| CodeGraphError::Database { - message: e.to_string(), - operation: "delete_edges_by_source".to_string(), - })?; - Ok(()) - } - - // ── File Operations ── - - pub fn upsert_file(&self, file: &FileRecord) -> Result<()> { - self.conn().execute( - "INSERT OR REPLACE INTO files - (path, content_hash, size, modified_at, indexed_at, node_count) - VALUES (?1, ?2, ?3, ?4, ?5, ?6)", - params![file.path, file.content_hash, file.size, file.modified_at, file.indexed_at, file.node_count], - ).map_err(|e| CodeGraphError::Database { - message: e.to_string(), - operation: "upsert_file".to_string(), - })?; - Ok(()) - } - - pub fn get_file(&self, path: &str) -> Result> { - let mut stmt = self.conn().prepare( - "SELECT path, content_hash, size, modified_at, indexed_at, node_count - FROM files WHERE path = ?1" - ).map_err(|e| CodeGraphError::Database { - message: e.to_string(), - operation: "get_file".to_string(), - })?; - - let file = stmt.query_row(params![path], row_to_file).optional().map_err(|e| CodeGraphError::Database { - message: e.to_string(), - operation: "get_file".to_string(), - })?; - Ok(file) - } - - pub fn get_all_files(&self) -> Result> { - let mut stmt = self.conn().prepare( - "SELECT path, content_hash, size, modified_at, indexed_at, node_count FROM files" - ).map_err(|e| CodeGraphError::Database { - message: e.to_string(), - operation: "get_all_files".to_string(), - })?; - - let files = stmt.query_map([], row_to_file) - .map_err(|e| CodeGraphError::Database { - message: e.to_string(), - operation: "get_all_files".to_string(), - })? - .filter_map(|r| r.ok()) - .collect(); - Ok(files) - } - - pub fn delete_file(&self, path: &str) -> Result<()> { - self.delete_nodes_by_file(path)?; - self.conn().execute("DELETE FROM files WHERE path = ?1", params![path]) - .map_err(|e| CodeGraphError::Database { - message: e.to_string(), - operation: "delete_file".to_string(), - })?; - Ok(()) - } - - // ── Unresolved References ── - - pub fn insert_unresolved_ref(&self, uref: &UnresolvedRef) -> Result<()> { - self.conn().execute( - "INSERT INTO unresolved_refs - (from_node_id, reference_name, reference_kind, line, col, file_path) - VALUES (?1, ?2, ?3, ?4, ?5, ?6)", - params![ - uref.from_node_id, - uref.reference_name, - uref.reference_kind.as_str(), - uref.line, - uref.column, - uref.file_path, - ], - ).map_err(|e| CodeGraphError::Database { - message: e.to_string(), - operation: "insert_unresolved_ref".to_string(), - })?; - Ok(()) - } - - pub fn insert_unresolved_refs(&self, refs: &[UnresolvedRef]) -> Result<()> { - let tx = self.conn().unchecked_transaction().map_err(|e| CodeGraphError::Database { - message: e.to_string(), - operation: "insert_unresolved_refs_tx".to_string(), - })?; - for uref in refs { - self.insert_unresolved_ref(uref)?; - } - tx.commit().map_err(|e| CodeGraphError::Database { - message: e.to_string(), - operation: "insert_unresolved_refs_commit".to_string(), - })?; - Ok(()) - } - - pub fn get_unresolved_refs(&self) -> Result> { - let mut stmt = self.conn().prepare( - "SELECT from_node_id, reference_name, reference_kind, line, col, file_path - FROM unresolved_refs" - ).map_err(|e| CodeGraphError::Database { - message: e.to_string(), - operation: "get_unresolved_refs".to_string(), - })?; - - let refs = stmt.query_map([], row_to_unresolved_ref) - .map_err(|e| CodeGraphError::Database { - message: e.to_string(), - operation: "get_unresolved_refs".to_string(), - })? - .filter_map(|r| r.ok()) - .collect(); - Ok(refs) - } - - pub fn clear_unresolved_refs(&self) -> Result<()> { - self.conn().execute("DELETE FROM unresolved_refs", []) - .map_err(|e| CodeGraphError::Database { - message: e.to_string(), - operation: "clear_unresolved_refs".to_string(), - })?; - Ok(()) - } - - // ── Search ── - - pub fn search_nodes(&self, query: &str, limit: usize) -> Result> { - // Try FTS5 first - let fts_query = format!("{}*", query); - let mut stmt = self.conn().prepare( - "SELECT n.id, n.kind, n.name, n.qualified_name, n.file_path, - n.start_line, n.end_line, n.start_column, n.end_column, - n.docstring, n.signature, n.visibility, n.is_async, n.updated_at, - rank - FROM nodes_fts fts - JOIN nodes n ON n.rowid = fts.rowid - WHERE nodes_fts MATCH ?1 - ORDER BY rank - LIMIT ?2" - ).map_err(|e| CodeGraphError::Database { - message: e.to_string(), - operation: "search_nodes_fts".to_string(), - })?; - - let results: Vec = stmt.query_map(params![fts_query, limit as i64], |row| { - let node = row_to_node(row)?; - let rank: f64 = row.get(14)?; - Ok(SearchResult { node, score: -rank }) // FTS5 rank is negative - }).map_err(|e| CodeGraphError::Database { - message: e.to_string(), - operation: "search_nodes_fts".to_string(), - })? - .filter_map(|r| r.ok()) - .collect(); - - if !results.is_empty() { - return Ok(results); - } - - // Fall back to LIKE search - let like_query = format!("%{}%", query); - let mut stmt = self.conn().prepare( - "SELECT id, kind, name, qualified_name, file_path, - start_line, end_line, start_column, end_column, - docstring, signature, visibility, is_async, updated_at - FROM nodes - WHERE name LIKE ?1 OR qualified_name LIKE ?1 - LIMIT ?2" - ).map_err(|e| CodeGraphError::Database { - message: e.to_string(), - operation: "search_nodes_like".to_string(), - })?; - - let results = stmt.query_map(params![like_query, limit as i64], |row| { - let node = row_to_node(row)?; - Ok(SearchResult { node, score: 0.5 }) - }).map_err(|e| CodeGraphError::Database { - message: e.to_string(), - operation: "search_nodes_like".to_string(), - })? - .filter_map(|r| r.ok()) - .collect(); - - Ok(results) - } - - // ── Statistics ── - - pub fn get_stats(&self) -> Result { - let node_count: usize = self.conn() - .query_row("SELECT COUNT(*) FROM nodes", [], |row| row.get(0)) - .unwrap_or(0); - - let edge_count: usize = self.conn() - .query_row("SELECT COUNT(*) FROM edges", [], |row| row.get(0)) - .unwrap_or(0); - - let file_count: usize = self.conn() - .query_row("SELECT COUNT(*) FROM files", [], |row| row.get(0)) - .unwrap_or(0); - - let mut stmt = self.conn().prepare( - "SELECT kind, COUNT(*) FROM nodes GROUP BY kind ORDER BY COUNT(*) DESC" - ).map_err(|e| CodeGraphError::Database { - message: e.to_string(), - operation: "get_stats_nodes_by_kind".to_string(), - })?; - - let nodes_by_kind: Vec<(String, usize)> = stmt.query_map([], |row| { - Ok((row.get::<_, String>(0)?, row.get::<_, usize>(1)?)) - }).map_err(|e| CodeGraphError::Database { - message: e.to_string(), - operation: "get_stats_nodes_by_kind".to_string(), - })? - .filter_map(|r| r.ok()) - .collect(); - - let mut stmt = self.conn().prepare( - "SELECT kind, COUNT(*) FROM edges GROUP BY kind ORDER BY COUNT(*) DESC" - ).map_err(|e| CodeGraphError::Database { - message: e.to_string(), - operation: "get_stats_edges_by_kind".to_string(), - })?; - - let edges_by_kind: Vec<(String, usize)> = stmt.query_map([], |row| { - Ok((row.get::<_, String>(0)?, row.get::<_, usize>(1)?)) - }).map_err(|e| CodeGraphError::Database { - message: e.to_string(), - operation: "get_stats_edges_by_kind".to_string(), - })? - .filter_map(|r| r.ok()) - .collect(); - - let db_size_bytes = self.size().unwrap_or(0); - - Ok(GraphStats { - node_count, - edge_count, - file_count, - nodes_by_kind, - edges_by_kind, - db_size_bytes, - last_updated: 0, - }) - } - - // ── Clear ── - - pub fn clear(&self) -> Result<()> { - self.conn().execute_batch( - "DELETE FROM vectors; - DELETE FROM unresolved_refs; - DELETE FROM edges; - DELETE FROM nodes; - DELETE FROM files;" - ).map_err(|e| CodeGraphError::Database { - message: e.to_string(), - operation: "clear".to_string(), - })?; - Ok(()) - } -} - -// ── Row Conversion Functions ── - -fn row_to_node(row: &rusqlite::Row) -> rusqlite::Result { - Ok(Node { - id: row.get(0)?, - kind: NodeKind::from_str(&row.get::<_, String>(1)?).unwrap_or(NodeKind::Function), - name: row.get(2)?, - qualified_name: row.get(3)?, - file_path: row.get(4)?, - start_line: row.get(5)?, - end_line: row.get(6)?, - start_column: row.get(7)?, - end_column: row.get(8)?, - docstring: row.get(9)?, - signature: row.get(10)?, - visibility: visibility_from_str(&row.get::<_, String>(11)?), - is_async: row.get::<_, i32>(12)? != 0, - updated_at: row.get(13)?, - }) -} - -fn row_to_edge(row: &rusqlite::Row) -> rusqlite::Result { - Ok(Edge { - source: row.get(0)?, - target: row.get(1)?, - kind: EdgeKind::from_str(&row.get::<_, String>(2)?).unwrap_or(EdgeKind::Contains), - line: row.get(3)?, - }) -} - -fn row_to_file(row: &rusqlite::Row) -> rusqlite::Result { - Ok(FileRecord { - path: row.get(0)?, - content_hash: row.get(1)?, - size: row.get(2)?, - modified_at: row.get(3)?, - indexed_at: row.get(4)?, - node_count: row.get(5)?, - }) -} - -fn row_to_unresolved_ref(row: &rusqlite::Row) -> rusqlite::Result { - Ok(UnresolvedRef { - from_node_id: row.get(0)?, - reference_name: row.get(1)?, - reference_kind: EdgeKind::from_str(&row.get::<_, String>(2)?).unwrap_or(EdgeKind::Calls), - line: row.get(3)?, - column: row.get(4)?, - file_path: row.get(5)?, - }) -} - -fn visibility_to_str(v: Visibility) -> &'static str { - match v { - Visibility::Pub => "public", - Visibility::PubCrate => "pub_crate", - Visibility::PubSuper => "pub_super", - Visibility::Private => "private", - } -} - -fn visibility_from_str(s: &str) -> Visibility { - match s { - "public" => Visibility::Pub, - "pub_crate" => Visibility::PubCrate, - "pub_super" => Visibility::PubSuper, - _ => Visibility::Private, - } -} - -// Extension trait for optional query results -trait OptionalExt { - fn optional(self) -> rusqlite::Result>; -} - -impl OptionalExt for rusqlite::Result { - fn optional(self) -> rusqlite::Result> { - match self { - Ok(v) => Ok(Some(v)), - Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None), - Err(e) => Err(e), - } - } -} -``` - -**Step 5: Update lib.rs** - -```rust -pub mod config; -pub mod db; -pub mod errors; -pub mod types; -``` - -**Step 6: Run tests to verify they pass** - -```bash -cargo test --test db_test -``` - -Expected: All PASS. - -**Step 7: Commit** - -```bash -git add src/db/ src/lib.rs tests/db_test.rs -git commit -m "feat: add SQLite database layer with FTS5 search" -``` - ---- - -## Task 4: Tree-Sitter Extraction for Rust - -**Files:** -- Create: `src/extraction/mod.rs` -- Create: `src/extraction/rust_extractor.rs` -- Modify: `src/lib.rs` -- Test: `tests/extraction_test.rs` - -**Step 1: Write the failing test** - -Create `tests/extraction_test.rs`: - -```rust -use codegraph::extraction::RustExtractor; -use codegraph::types::*; - -#[test] -fn test_extract_function() { - let source = r#" -/// Adds two numbers. -pub fn add(a: i32, b: i32) -> i32 { - a + b -} -"#; - let result = RustExtractor::extract("src/math.rs", source); - assert!(result.errors.is_empty(), "errors: {:?}", result.errors); - - let functions: Vec<_> = result.nodes.iter().filter(|n| n.kind == NodeKind::Function).collect(); - assert_eq!(functions.len(), 1); - assert_eq!(functions[0].name, "add"); - assert_eq!(functions[0].visibility, Visibility::Pub); - assert!(functions[0].signature.as_ref().unwrap().contains("fn add")); - assert!(functions[0].docstring.as_ref().unwrap().contains("Adds two numbers")); -} - -#[test] -fn test_extract_struct_with_fields() { - let source = r#" -pub struct Point { - pub x: f64, - pub y: f64, -} -"#; - let result = RustExtractor::extract("src/geo.rs", source); - - let structs: Vec<_> = result.nodes.iter().filter(|n| n.kind == NodeKind::Struct).collect(); - assert_eq!(structs.len(), 1); - assert_eq!(structs[0].name, "Point"); - - let fields: Vec<_> = result.nodes.iter().filter(|n| n.kind == NodeKind::Field).collect(); - assert_eq!(fields.len(), 2); - - // Check contains edges - let contains: Vec<_> = result.edges.iter().filter(|e| e.kind == EdgeKind::Contains).collect(); - assert!(contains.len() >= 2); // struct contains fields -} - -#[test] -fn test_extract_enum() { - let source = r#" -pub enum Color { - Red, - Green, - Blue, -} -"#; - let result = RustExtractor::extract("src/color.rs", source); - - let enums: Vec<_> = result.nodes.iter().filter(|n| n.kind == NodeKind::Enum).collect(); - assert_eq!(enums.len(), 1); - assert_eq!(enums[0].name, "Color"); - - let variants: Vec<_> = result.nodes.iter().filter(|n| n.kind == NodeKind::EnumVariant).collect(); - assert_eq!(variants.len(), 3); -} - -#[test] -fn test_extract_trait() { - let source = r#" -pub trait Drawable { - fn draw(&self); - fn area(&self) -> f64; -} -"#; - let result = RustExtractor::extract("src/draw.rs", source); - - let traits: Vec<_> = result.nodes.iter().filter(|n| n.kind == NodeKind::Trait).collect(); - assert_eq!(traits.len(), 1); - assert_eq!(traits[0].name, "Drawable"); - - let methods: Vec<_> = result.nodes.iter().filter(|n| n.kind == NodeKind::Method).collect(); - assert_eq!(methods.len(), 2); -} - -#[test] -fn test_extract_impl_block() { - let source = r#" -struct Circle { - radius: f64, -} - -impl Circle { - pub fn new(radius: f64) -> Self { - Circle { radius } - } - - pub fn area(&self) -> f64 { - std::f64::consts::PI * self.radius * self.radius - } -} -"#; - let result = RustExtractor::extract("src/circle.rs", source); - - let impls: Vec<_> = result.nodes.iter().filter(|n| n.kind == NodeKind::Impl).collect(); - assert_eq!(impls.len(), 1); - - let methods: Vec<_> = result.nodes.iter().filter(|n| n.kind == NodeKind::Method).collect(); - assert_eq!(methods.len(), 2); -} - -#[test] -fn test_extract_trait_impl() { - let source = r#" -trait Greet { - fn hello(&self) -> String; -} - -struct Person { - name: String, -} - -impl Greet for Person { - fn hello(&self) -> String { - format!("Hello, {}", self.name) - } -} -"#; - let result = RustExtractor::extract("src/greet.rs", source); - - let implements: Vec<_> = result.edges.iter().filter(|e| e.kind == EdgeKind::Implements).collect(); - assert!(!implements.is_empty(), "should have implements edge"); -} - -#[test] -fn test_extract_use_declarations() { - let source = r#" -use std::collections::HashMap; -use crate::types::Node; -"#; - let result = RustExtractor::extract("src/lib.rs", source); - - let uses: Vec<_> = result.nodes.iter().filter(|n| n.kind == NodeKind::Use).collect(); - assert_eq!(uses.len(), 2); -} - -#[test] -fn test_extract_call_sites() { - let source = r#" -fn helper() -> i32 { - 42 -} - -fn main() { - let x = helper(); - println!("{}", x); -} -"#; - let result = RustExtractor::extract("src/main.rs", source); - - // Should have unresolved call references - assert!(!result.unresolved_refs.is_empty(), "should have unresolved refs for calls"); - let call_refs: Vec<_> = result.unresolved_refs.iter() - .filter(|r| r.reference_kind == EdgeKind::Calls) - .collect(); - assert!(!call_refs.is_empty(), "should have call refs"); -} - -#[test] -fn test_extract_async_function() { - let source = r#" -pub async fn fetch_data(url: &str) -> Result { - Ok("data".to_string()) -} -"#; - let result = RustExtractor::extract("src/http.rs", source); - - let functions: Vec<_> = result.nodes.iter().filter(|n| n.kind == NodeKind::Function).collect(); - assert_eq!(functions.len(), 1); - assert!(functions[0].is_async); -} - -#[test] -fn test_extract_const_and_static() { - let source = r#" -pub const MAX_SIZE: usize = 1024; -static COUNTER: AtomicU64 = AtomicU64::new(0); -"#; - let result = RustExtractor::extract("src/globals.rs", source); - - let consts: Vec<_> = result.nodes.iter().filter(|n| n.kind == NodeKind::Const).collect(); - assert_eq!(consts.len(), 1); - assert_eq!(consts[0].name, "MAX_SIZE"); - - let statics: Vec<_> = result.nodes.iter().filter(|n| n.kind == NodeKind::Static).collect(); - assert_eq!(statics.len(), 1); - assert_eq!(statics[0].name, "COUNTER"); -} - -#[test] -fn test_extract_type_alias() { - let source = r#" -pub type Result = std::result::Result; -"#; - let result = RustExtractor::extract("src/types.rs", source); - - let aliases: Vec<_> = result.nodes.iter().filter(|n| n.kind == NodeKind::TypeAlias).collect(); - assert_eq!(aliases.len(), 1); - assert_eq!(aliases[0].name, "Result"); -} - -#[test] -fn test_extract_module() { - let source = r#" -pub mod utils { - pub fn helper() {} -} -"#; - let result = RustExtractor::extract("src/lib.rs", source); - - let modules: Vec<_> = result.nodes.iter().filter(|n| n.kind == NodeKind::Module).collect(); - assert_eq!(modules.len(), 1); - assert_eq!(modules[0].name, "utils"); -} - -#[test] -fn test_extract_derive_macros() { - let source = r#" -#[derive(Debug, Clone, Serialize)] -pub struct Config { - pub name: String, -} -"#; - let result = RustExtractor::extract("src/config.rs", source); - - let derives: Vec<_> = result.edges.iter().filter(|e| e.kind == EdgeKind::DerivesMacro).collect(); - assert!(!derives.is_empty(), "should have derives_macro edges"); -} - -#[test] -fn test_file_node_is_root() { - let source = "fn main() {}"; - let result = RustExtractor::extract("src/main.rs", source); - - let files: Vec<_> = result.nodes.iter().filter(|n| n.kind == NodeKind::File).collect(); - assert_eq!(files.len(), 1); - assert_eq!(files[0].name, "src/main.rs"); -} - -#[test] -fn test_qualified_names() { - let source = r#" -mod server { - pub fn handle_request() {} -} -"#; - let result = RustExtractor::extract("src/lib.rs", source); - - let fns: Vec<_> = result.nodes.iter().filter(|n| n.kind == NodeKind::Function).collect(); - assert_eq!(fns.len(), 1); - assert!(fns[0].qualified_name.contains("server")); - assert!(fns[0].qualified_name.contains("handle_request")); -} -``` - -**Step 2: Run test to verify it fails** - -```bash -cargo test --test extraction_test -``` - -Expected: FAIL — module `codegraph::extraction` not found. - -**Step 3: Implement the Rust extractor** - -Create `src/extraction/mod.rs`: - -```rust -mod rust_extractor; - -pub use rust_extractor::RustExtractor; -``` - -Create `src/extraction/rust_extractor.rs` — This is the core AST extraction module. It uses `tree-sitter-rust` to parse Rust source and emit nodes and edges. The implementation should: - -1. Parse source with tree-sitter -2. Create a file node as root -3. Walk the AST recursively with `visit_node()` -4. Maintain a `node_stack` for parent context and qualified names -5. For each relevant AST node type, extract a `Node` with metadata -6. Emit `Contains` edges from parent to child automatically -7. Emit unresolved references for call sites, use declarations -8. Extract docstrings from preceding comment nodes -9. Extract signatures from function/method declarations -10. Detect visibility from `visibility_modifier` nodes -11. Detect async functions -12. Extract derive macro attributes - -Key tree-sitter-rust node types to handle: -- `function_item` → Function or Method (if inside impl) -- `struct_item` → Struct -- `enum_item` → Enum -- `enum_variant` → EnumVariant -- `trait_item` → Trait -- `impl_item` → Impl -- `use_declaration` → Use -- `const_item` → Const -- `static_item` → Static -- `type_item` → TypeAlias -- `field_declaration` → Field -- `mod_item` → Module -- `call_expression` → unresolved Calls ref -- `macro_invocation` → unresolved Calls ref -- `attribute_item` with `derive` → DerivesMacro edges - -**Step 4: Update lib.rs** - -```rust -pub mod config; -pub mod db; -pub mod errors; -pub mod extraction; -pub mod types; -``` - -**Step 5: Run tests to verify they pass** - -```bash -cargo test --test extraction_test -``` - -Expected: All PASS. - -**Step 6: Commit** - -```bash -git add src/extraction/ src/lib.rs tests/extraction_test.rs -git commit -m "feat: add tree-sitter Rust extraction with full AST node coverage" -``` - ---- - -## Task 5: Reference Resolution - -**Files:** -- Create: `src/resolution/mod.rs` -- Create: `src/resolution/imports.rs` -- Create: `src/resolution/names.rs` -- Modify: `src/lib.rs` -- Test: `tests/resolution_test.rs` - -**Step 1: Write the failing test** - -Create `tests/resolution_test.rs`: - -```rust -use codegraph::db::Database; -use codegraph::resolution::ReferenceResolver; -use codegraph::types::*; -use tempfile::TempDir; - -fn setup_db_with_nodes() -> (TempDir, Database) { - let dir = TempDir::new().unwrap(); - let db = Database::initialize(&dir.path().join("test.db")).unwrap(); - - // Insert a function that is called - let callee = Node { - id: generate_node_id("src/utils.rs", NodeKind::Function, "helper", 1), - kind: NodeKind::Function, - name: "helper".to_string(), - qualified_name: "src/utils.rs::helper".to_string(), - file_path: "src/utils.rs".to_string(), - start_line: 1, end_line: 5, - start_column: 0, end_column: 1, - signature: Some("fn helper() -> i32".to_string()), - docstring: None, - visibility: Visibility::Pub, - is_async: false, updated_at: 0, - }; - - // Insert the caller - let caller = Node { - id: generate_node_id("src/main.rs", NodeKind::Function, "main", 1), - kind: NodeKind::Function, - name: "main".to_string(), - qualified_name: "src/main.rs::main".to_string(), - file_path: "src/main.rs".to_string(), - start_line: 1, end_line: 5, - start_column: 0, end_column: 1, - signature: Some("fn main()".to_string()), - docstring: None, - visibility: Visibility::Private, - is_async: false, updated_at: 0, - }; - - db.insert_node(&callee).unwrap(); - db.insert_node(&caller).unwrap(); - - (dir, db) -} - -#[test] -fn test_resolve_exact_name_match() { - let (_dir, db) = setup_db_with_nodes(); - let resolver = ReferenceResolver::new(&db); - - let uref = UnresolvedRef { - from_node_id: generate_node_id("src/main.rs", NodeKind::Function, "main", 1), - reference_name: "helper".to_string(), - reference_kind: EdgeKind::Calls, - line: 3, - column: 12, - file_path: "src/main.rs".to_string(), - }; - - let result = resolver.resolve_one(&uref); - assert!(result.is_some(), "should resolve 'helper' by exact name"); - let resolved = result.unwrap(); - assert!(resolved.confidence >= 0.7); -} - -#[test] -fn test_resolve_all() { - let (_dir, db) = setup_db_with_nodes(); - let resolver = ReferenceResolver::new(&db); - - let refs = vec![ - UnresolvedRef { - from_node_id: generate_node_id("src/main.rs", NodeKind::Function, "main", 1), - reference_name: "helper".to_string(), - reference_kind: EdgeKind::Calls, - line: 3, column: 12, - file_path: "src/main.rs".to_string(), - }, - ]; - - let result = resolver.resolve_all(&refs); - assert_eq!(result.total, 1); - assert_eq!(result.resolved_count, 1); - assert_eq!(result.resolved.len(), 1); -} - -#[test] -fn test_unresolvable_reference() { - let (_dir, db) = setup_db_with_nodes(); - let resolver = ReferenceResolver::new(&db); - - let uref = UnresolvedRef { - from_node_id: "function:caller".to_string(), - reference_name: "nonexistent_function".to_string(), - reference_kind: EdgeKind::Calls, - line: 5, column: 8, - file_path: "src/main.rs".to_string(), - }; - - let result = resolver.resolve_one(&uref); - assert!(result.is_none(), "should not resolve nonexistent function"); -} - -#[test] -fn test_creates_edges_from_resolved() { - let (_dir, db) = setup_db_with_nodes(); - let resolver = ReferenceResolver::new(&db); - - let resolved = ResolvedRef { - original: UnresolvedRef { - from_node_id: generate_node_id("src/main.rs", NodeKind::Function, "main", 1), - reference_name: "helper".to_string(), - reference_kind: EdgeKind::Calls, - line: 3, column: 12, - file_path: "src/main.rs".to_string(), - }, - target_node_id: generate_node_id("src/utils.rs", NodeKind::Function, "helper", 1), - confidence: 0.9, - resolved_by: "exact-match".to_string(), - }; - - let edges = resolver.create_edges(&[resolved]); - assert_eq!(edges.len(), 1); - assert_eq!(edges[0].kind, EdgeKind::Calls); - assert_eq!(edges[0].line, Some(3)); -} -``` - -**Step 2: Run test to verify it fails** - -```bash -cargo test --test resolution_test -``` - -**Step 3: Implement resolution module** - -Create `src/resolution/mod.rs`, `src/resolution/imports.rs`, `src/resolution/names.rs`. - -The resolver should: -1. Build in-memory caches from all nodes (by name, qualified name, kind) -2. For each unresolved ref, try strategies in order: - - Exact name match (confidence 0.9 for single match, 0.7 for multiple with scoring) - - Qualified name match (confidence 0.95) - - Use-path resolution (follow `use crate::` paths) -3. Score candidates: same file +100, same module +50, exported +10 -4. Create edges from resolved references - -**Step 4: Update lib.rs** - -```rust -pub mod config; -pub mod db; -pub mod errors; -pub mod extraction; -pub mod resolution; -pub mod types; -``` - -**Step 5: Run tests** - -```bash -cargo test --test resolution_test -``` - -**Step 6: Commit** - -```bash -git add src/resolution/ src/lib.rs tests/resolution_test.rs -git commit -m "feat: add reference resolution with name and import-based matching" -``` - ---- - -## Task 6: Graph Traversal & Queries - -**Files:** -- Create: `src/graph/mod.rs` -- Create: `src/graph/traversal.rs` -- Create: `src/graph/queries.rs` -- Modify: `src/lib.rs` -- Test: `tests/graph_test.rs` - -**Step 1: Write the failing test** - -Create `tests/graph_test.rs`: - -```rust -use codegraph::db::Database; -use codegraph::graph::{GraphTraverser, GraphQueryManager}; -use codegraph::types::*; -use tempfile::TempDir; - -fn setup_call_graph() -> (TempDir, Database) { - let dir = TempDir::new().unwrap(); - let db = Database::initialize(&dir.path().join("test.db")).unwrap(); - - // Create: main -> process -> validate -> check - let nodes = vec!["main", "process", "validate", "check"]; - for (i, name) in nodes.iter().enumerate() { - let node = Node { - id: format!("function:{}", name), - kind: NodeKind::Function, - name: name.to_string(), - qualified_name: format!("src/lib.rs::{}", name), - file_path: "src/lib.rs".to_string(), - start_line: (i as u32) * 10 + 1, - end_line: (i as u32) * 10 + 9, - start_column: 0, end_column: 1, - signature: Some(format!("fn {}()", name)), - docstring: None, - visibility: Visibility::Pub, - is_async: false, updated_at: 0, - }; - db.insert_node(&node).unwrap(); - } - - let call_edges = vec![ - ("main", "process"), - ("process", "validate"), - ("validate", "check"), - ]; - for (source, target) in call_edges { - let edge = Edge { - source: format!("function:{}", source), - target: format!("function:{}", target), - kind: EdgeKind::Calls, - line: None, - }; - db.insert_edge(&edge).unwrap(); - } - - (dir, db) -} - -#[test] -fn test_get_callers() { - let (_dir, db) = setup_call_graph(); - let traverser = GraphTraverser::new(&db); - - let callers = traverser.get_callers("function:process", 1).unwrap(); - assert_eq!(callers.len(), 1); - assert_eq!(callers[0].0.name, "main"); -} - -#[test] -fn test_get_callees() { - let (_dir, db) = setup_call_graph(); - let traverser = GraphTraverser::new(&db); - - let callees = traverser.get_callees("function:process", 1).unwrap(); - assert_eq!(callees.len(), 1); - assert_eq!(callees[0].0.name, "validate"); -} - -#[test] -fn test_impact_radius() { - let (_dir, db) = setup_call_graph(); - let traverser = GraphTraverser::new(&db); - - // Impact of "check" should include validate, process, main - let impact = traverser.get_impact_radius("function:check", 10).unwrap(); - assert!(impact.nodes.len() >= 3, "impact should include transitive callers"); -} - -#[test] -fn test_call_graph_bidirectional() { - let (_dir, db) = setup_call_graph(); - let traverser = GraphTraverser::new(&db); - - let graph = traverser.get_call_graph("function:process", 2).unwrap(); - // Should include main (caller) and validate (callee) - assert!(graph.nodes.len() >= 3); -} - -#[test] -fn test_bfs_traversal_with_depth_limit() { - let (_dir, db) = setup_call_graph(); - let traverser = GraphTraverser::new(&db); - - let opts = TraversalOptions { - max_depth: 1, - direction: TraversalDirection::Outgoing, - ..Default::default() - }; - - let subgraph = traverser.traverse_bfs("function:main", &opts).unwrap(); - // Depth 1: main + process only - assert!(subgraph.nodes.len() <= 2); -} - -#[test] -fn test_find_dead_code() { - let (_dir, db) = setup_call_graph(); - let qm = GraphQueryManager::new(&db); - - // Add an isolated function (no incoming edges) - let orphan = Node { - id: "function:orphan".to_string(), - kind: NodeKind::Function, - name: "orphan".to_string(), - qualified_name: "src/lib.rs::orphan".to_string(), - file_path: "src/lib.rs".to_string(), - start_line: 50, end_line: 55, - start_column: 0, end_column: 1, - signature: None, docstring: None, - visibility: Visibility::Private, // private, no callers = dead code - is_async: false, updated_at: 0, - }; - db.insert_node(&orphan).unwrap(); - - let dead = qm.find_dead_code(&[NodeKind::Function]).unwrap(); - let dead_names: Vec<_> = dead.iter().map(|n| n.name.as_str()).collect(); - assert!(dead_names.contains(&"orphan"), "orphan should be dead code"); - // main has no incoming edges but is named "main" — should be excluded -} -``` - -**Step 2: Run test to verify it fails** - -```bash -cargo test --test graph_test -``` - -**Step 3: Implement graph module** - -Implement `src/graph/traversal.rs` with `GraphTraverser` providing BFS/DFS traversal, callers/callees, impact radius, call graph, type hierarchy, and path finding. - -Implement `src/graph/queries.rs` with `GraphQueryManager` providing dead code detection, node metrics, file dependencies, and circular dependency detection. - -**Step 4: Run tests** - -```bash -cargo test --test graph_test -``` - -**Step 5: Commit** - -```bash -git add src/graph/ src/lib.rs tests/graph_test.rs -git commit -m "feat: add graph traversal with BFS/DFS, impact analysis, and dead code detection" -``` - ---- - -## Task 7: CLI Interface - -**Files:** -- Modify: `src/main.rs` -- Create: `src/codegraph.rs` (main orchestrator) -- Modify: `src/lib.rs` -- Test: manual CLI testing - -**Step 1: Implement the CodeGraph orchestrator** - -Create `src/codegraph.rs` — the central orchestrator that wires all subsystems together: - -```rust -pub struct CodeGraph { - db: Database, - config: CodeGraphConfig, - project_root: PathBuf, -} -``` - -Methods: -- `init(project_root)` — create `.codegraph/`, init DB, save config -- `open(project_root)` — open existing project -- `index_all()` — scan files, extract, resolve, store -- `sync()` — incremental update via content hashing -- `search(query, limit)` — FTS5 search -- `get_stats()` — graph statistics -- All graph query delegations (callers, callees, impact, etc.) - -**Step 2: Implement CLI with clap** - -Modify `src/main.rs`: - -```rust -use clap::{Parser, Subcommand}; - -#[derive(Parser)] -#[command(name = "codegraph", about = "Code intelligence for Rust codebases")] -struct Cli { - #[command(subcommand)] - command: Commands, -} - -#[derive(Subcommand)] -enum Commands { - Init { path: Option }, - Index { path: Option, #[arg(short, long)] force: bool }, - Sync { path: Option }, - Status { path: Option, #[arg(short, long)] json: bool }, - Query { search: String, #[arg(short, long)] path: Option, #[arg(short, long, default_value = "10")] limit: usize }, - Context { task: String, #[arg(short, long)] path: Option }, - Serve { #[arg(short, long)] path: Option }, -} -``` - -**Step 3: Test CLI manually** - -```bash -cargo run -- init . -cargo run -- index . -cargo run -- status . -cargo run -- query "main" -``` - -**Step 4: Commit** - -```bash -git add src/main.rs src/codegraph.rs src/lib.rs -git commit -m "feat: add CLI with init, index, sync, status, query, context, serve commands" -``` - ---- - -## Task 8: Context Builder - -**Files:** -- Create: `src/context/mod.rs` -- Create: `src/context/builder.rs` -- Create: `src/context/formatter.rs` -- Modify: `src/lib.rs` -- Test: `tests/context_test.rs` - -**Step 1: Write the failing test** - -Create `tests/context_test.rs`: - -```rust -use codegraph::context::*; -use codegraph::db::Database; -use codegraph::graph::GraphTraverser; -use codegraph::types::*; -use tempfile::TempDir; - -fn setup_context_db() -> (TempDir, Database) { - let dir = TempDir::new().unwrap(); - let db = Database::initialize(&dir.path().join("test.db")).unwrap(); - - let node = Node { - id: "function:process_request".to_string(), - kind: NodeKind::Function, - name: "process_request".to_string(), - qualified_name: "src/server.rs::process_request".to_string(), - file_path: "src/server.rs".to_string(), - start_line: 10, end_line: 25, - start_column: 0, end_column: 1, - signature: Some("pub fn process_request(req: Request) -> Response".to_string()), - docstring: Some("Handles incoming HTTP requests".to_string()), - visibility: Visibility::Pub, - is_async: false, updated_at: 0, - }; - db.insert_node(&node).unwrap(); - (dir, db) -} - -#[test] -fn test_extract_symbols_from_query() { - let symbols = extract_symbols_from_query("fix the process_request function"); - assert!(symbols.contains(&"process_request".to_string())); -} - -#[test] -fn test_extract_camel_case_symbols() { - let symbols = extract_symbols_from_query("update UserService handler"); - assert!(symbols.contains(&"UserService".to_string())); -} - -#[test] -fn test_format_context_markdown() { - let context = TaskContext { - query: "test query".to_string(), - summary: "Test summary".to_string(), - subgraph: Subgraph::default(), - entry_points: vec![], - code_blocks: vec![], - related_files: vec![], - }; - - let md = format_context_as_markdown(&context); - assert!(md.contains("## Code Context")); - assert!(md.contains("test query")); -} - -#[test] -fn test_format_context_json() { - let context = TaskContext { - query: "test".to_string(), - summary: "Summary".to_string(), - subgraph: Subgraph::default(), - entry_points: vec![], - code_blocks: vec![], - related_files: vec![], - }; - - let json = format_context_as_json(&context); - let parsed: serde_json::Value = serde_json::from_str(&json).unwrap(); - assert_eq!(parsed["query"], "test"); -} -``` - -**Step 2: Implement context module** - -The `ContextBuilder` should: -1. Extract symbol names from natural language queries (CamelCase, snake_case patterns) -2. Search for matching nodes via FTS5 and exact name lookup -3. Expand graph around entry points using BFS -4. Extract code blocks by reading source files -5. Format output as markdown or JSON - -**Step 3: Run tests, commit** - -```bash -cargo test --test context_test -git add src/context/ tests/context_test.rs -git commit -m "feat: add context builder with symbol extraction and markdown/JSON formatting" -``` - ---- - -## Task 9: Vector Embeddings - -**Files:** -- Create: `src/vectors/mod.rs` -- Create: `src/vectors/embedder.rs` -- Create: `src/vectors/search.rs` -- Modify: `src/lib.rs` -- Test: `tests/vectors_test.rs` - -**Step 1: Write the failing test** - -Create `tests/vectors_test.rs`: - -```rust -use codegraph::vectors::*; -use codegraph::db::Database; -use codegraph::types::*; -use tempfile::TempDir; - -#[test] -fn test_cosine_similarity_identical() { - let a = vec![1.0, 0.0, 0.0]; - let b = vec![1.0, 0.0, 0.0]; - let sim = cosine_similarity(&a, &b); - assert!((sim - 1.0).abs() < 1e-6); -} - -#[test] -fn test_cosine_similarity_orthogonal() { - let a = vec![1.0, 0.0]; - let b = vec![0.0, 1.0]; - let sim = cosine_similarity(&a, &b); - assert!(sim.abs() < 1e-6); -} - -#[test] -fn test_store_and_retrieve_vector() { - let dir = TempDir::new().unwrap(); - let db = Database::initialize(&dir.path().join("test.db")).unwrap(); - - // Must have a node to reference - let node = Node { - id: "function:test_fn".to_string(), - kind: NodeKind::Function, - name: "test_fn".to_string(), - qualified_name: "test_fn".to_string(), - file_path: "src/lib.rs".to_string(), - start_line: 1, end_line: 5, - start_column: 0, end_column: 1, - signature: None, docstring: None, - visibility: Visibility::Pub, - is_async: false, updated_at: 0, - }; - db.insert_node(&node).unwrap(); - - let embedding: Vec = vec![0.1, 0.2, 0.3, 0.4, 0.5]; - store_vector(&db, "function:test_fn", &embedding, "test-model").unwrap(); - - let retrieved = get_vector(&db, "function:test_fn").unwrap(); - assert!(retrieved.is_some()); - let retrieved = retrieved.unwrap(); - assert_eq!(retrieved.len(), 5); - assert!((retrieved[0] - 0.1).abs() < 1e-6); -} - -#[test] -fn test_brute_force_search() { - let dir = TempDir::new().unwrap(); - let db = Database::initialize(&dir.path().join("test.db")).unwrap(); - - // Insert nodes and embeddings - for i in 0..5 { - let node = Node { - id: format!("function:fn_{}", i), - kind: NodeKind::Function, - name: format!("fn_{}", i), - qualified_name: format!("fn_{}", i), - file_path: "src/lib.rs".to_string(), - start_line: i + 1, end_line: i + 5, - start_column: 0, end_column: 1, - signature: None, docstring: None, - visibility: Visibility::Pub, - is_async: false, updated_at: 0, - }; - db.insert_node(&node).unwrap(); - - let mut embedding = vec![0.0f32; 5]; - embedding[i as usize] = 1.0; // one-hot encoding - store_vector(&db, &format!("function:fn_{}", i), &embedding, "test").unwrap(); - } - - // Search for vector close to fn_2 - let query = vec![0.0, 0.0, 0.9, 0.1, 0.0]; - let results = brute_force_search(&db, &query, 3).unwrap(); - assert!(!results.is_empty()); - assert_eq!(results[0].0, "function:fn_2"); // closest match -} - -#[test] -fn test_create_node_text() { - let node = Node { - id: "function:test".to_string(), - kind: NodeKind::Function, - name: "process_data".to_string(), - qualified_name: "src/lib.rs::process_data".to_string(), - file_path: "src/lib.rs".to_string(), - start_line: 1, end_line: 10, - start_column: 0, end_column: 1, - signature: Some("fn process_data(input: &str) -> Result".to_string()), - docstring: Some("Processes raw data input".to_string()), - visibility: Visibility::Pub, - is_async: false, updated_at: 0, - }; - - let text = create_node_text(&node); - assert!(text.contains("process_data")); - assert!(text.contains("function")); - assert!(text.contains("Processes raw data")); -} -``` - -**Step 2: Implement vectors module** - -The vectors module should provide: -- `cosine_similarity(a, b)` — compute cosine similarity -- `store_vector(db, node_id, embedding, model)` — store as BLOB -- `get_vector(db, node_id)` — retrieve and decode BLOB -- `brute_force_search(db, query, limit)` — load all vectors, compute similarity, return top-k -- `create_node_text(node)` — create searchable text representation -- `TextEmbedder` — wrapper around `ort` for ONNX inference (initialize with model path, embed text, embed query) - -For the ONNX embedder, use the `ort` crate with `nomic-embed-text-v1.5` model. Add "search_query: " / "search_document: " prefixes per nomic model requirements. - -**Step 3: Run tests, commit** - -```bash -cargo test --test vectors_test -git add src/vectors/ tests/vectors_test.rs -git commit -m "feat: add vector embeddings with brute-force cosine similarity search" -``` - ---- - -## Task 10: MCP Server - -**Files:** -- Create: `src/mcp/mod.rs` -- Create: `src/mcp/server.rs` -- Create: `src/mcp/tools.rs` -- Create: `src/mcp/transport.rs` -- Modify: `src/lib.rs` -- Test: `tests/mcp_test.rs` - -**Step 1: Write the failing test** - -Create `tests/mcp_test.rs`: - -```rust -use codegraph::mcp::transport::*; -use codegraph::mcp::tools::*; -use serde_json::json; - -#[test] -fn test_parse_jsonrpc_request() { - let msg = json!({ - "jsonrpc": "2.0", - "id": 1, - "method": "tools/list", - "params": {} - }); - - let request: JsonRpcRequest = serde_json::from_value(msg).unwrap(); - assert_eq!(request.method, "tools/list"); - assert_eq!(request.id, serde_json::Value::Number(1.into())); -} - -#[test] -fn test_tool_definitions() { - let tools = get_tool_definitions(); - assert!(!tools.is_empty()); - - let tool_names: Vec<&str> = tools.iter().map(|t| t.name.as_str()).collect(); - assert!(tool_names.contains(&"codegraph_search")); - assert!(tool_names.contains(&"codegraph_context")); - assert!(tool_names.contains(&"codegraph_callers")); - assert!(tool_names.contains(&"codegraph_callees")); - assert!(tool_names.contains(&"codegraph_impact")); - assert!(tool_names.contains(&"codegraph_node")); - assert!(tool_names.contains(&"codegraph_status")); -} - -#[test] -fn test_serialize_jsonrpc_response() { - let response = JsonRpcResponse { - jsonrpc: "2.0".to_string(), - id: serde_json::Value::Number(1.into()), - result: Some(json!({"tools": []})), - error: None, - }; - - let json = serde_json::to_string(&response).unwrap(); - assert!(json.contains("\"jsonrpc\":\"2.0\"")); -} - -#[test] -fn test_error_response() { - let response = JsonRpcResponse::error( - serde_json::Value::Number(1.into()), - ErrorCode::MethodNotFound, - "Method not found".to_string(), - ); - - let json = serde_json::to_string(&response).unwrap(); - assert!(json.contains("-32601")); -} -``` - -**Step 2: Implement MCP module** - -The MCP server should: -- Read JSON-RPC 2.0 messages from stdin line-by-line -- Handle `initialize`, `tools/list`, `tools/call`, `ping` requests -- Expose 7 tools: search, context, callers, callees, impact, node, status -- Format output for minimal token usage -- Truncate responses > 15000 chars -- Use `tokio` for async I/O - -**Step 3: Run tests, commit** - -```bash -cargo test --test mcp_test -git add src/mcp/ tests/mcp_test.rs -git commit -m "feat: add MCP server with JSON-RPC transport and tool handlers" -``` - ---- - -## Task 11: Incremental Sync - -**Files:** -- Create: `src/sync.rs` -- Modify: `src/codegraph.rs` -- Modify: `src/lib.rs` -- Test: `tests/sync_test.rs` - -**Step 1: Write the failing test** - -Create `tests/sync_test.rs`: - -```rust -use codegraph::sync::*; - -#[test] -fn test_content_hash_deterministic() { - let content = "fn main() {}"; - let hash1 = content_hash(content); - let hash2 = content_hash(content); - assert_eq!(hash1, hash2); -} - -#[test] -fn test_content_hash_different_for_different_content() { - let hash1 = content_hash("fn main() {}"); - let hash2 = content_hash("fn main() { println!(\"hello\"); }"); - assert_ne!(hash1, hash2); -} - -#[test] -fn test_detect_changed_files() { - // Test that changed files are detected by comparing stored vs current hashes - use codegraph::db::Database; - use codegraph::types::FileRecord; - use tempfile::TempDir; - - let dir = TempDir::new().unwrap(); - let db = Database::initialize(&dir.path().join("test.db")).unwrap(); - - // Store a file with hash "old_hash" - db.upsert_file(&FileRecord { - path: "src/main.rs".to_string(), - content_hash: "old_hash".to_string(), - size: 100, - modified_at: 1000, - indexed_at: 1001, - node_count: 5, - }).unwrap(); - - // Current hash is different - let current_hashes = vec![ - ("src/main.rs".to_string(), "new_hash".to_string()), - ]; - - let stale = find_stale_files(&db, ¤t_hashes).unwrap(); - assert_eq!(stale.len(), 1); - assert_eq!(stale[0], "src/main.rs"); -} -``` - -**Step 2: Implement sync module** - -The sync module should: -- `content_hash(content)` — SHA256 hash of file content -- `find_stale_files(db, current_hashes)` — compare stored vs current content hashes -- `find_new_files(db, current_files)` — files not yet in database -- `find_removed_files(db, current_files)` — files in DB but not on disk - -**Step 3: Run tests, commit** - -```bash -cargo test --test sync_test -git add src/sync.rs tests/sync_test.rs -git commit -m "feat: add incremental sync with content hash change detection" -``` - ---- - -## Task 12: Integration Test & Polish - -**Files:** -- Create: `tests/integration_test.rs` -- Modify: various files for fixes - -**Step 1: Write end-to-end integration test** - -Create `tests/integration_test.rs`: - -```rust -use codegraph::codegraph::CodeGraph; -use tempfile::TempDir; -use std::fs; - -#[test] -fn test_full_pipeline() { - let dir = TempDir::new().unwrap(); - let project = dir.path(); - - // Create a small Rust project - fs::create_dir_all(project.join("src")).unwrap(); - fs::write(project.join("src/main.rs"), r#" -use crate::utils::helper; - -mod utils; - -fn main() { - let result = helper(); - println!("{}", result); -} -"#).unwrap(); - - fs::write(project.join("src/utils.rs"), r#" -/// Returns a greeting string. -pub fn helper() -> String { - format_greeting("world") -} - -fn format_greeting(name: &str) -> String { - format!("Hello, {}!", name) -} -"#).unwrap(); - - // Init - let cg = CodeGraph::init(project).unwrap(); - - // Index - let index_result = cg.index_all().unwrap(); - assert!(index_result.file_count > 0); - assert!(index_result.node_count > 0); - - // Stats - let stats = cg.get_stats().unwrap(); - assert!(stats.node_count > 0); - assert!(stats.file_count >= 2); - - // Search - let results = cg.search("helper", 10).unwrap(); - assert!(!results.is_empty()); - assert!(results.iter().any(|r| r.node.name == "helper")); - - // Status - let stats = cg.get_stats().unwrap(); - assert!(stats.edge_count > 0); // should have contains + calls edges -} - -#[test] -fn test_incremental_sync() { - let dir = TempDir::new().unwrap(); - let project = dir.path(); - - fs::create_dir_all(project.join("src")).unwrap(); - fs::write(project.join("src/lib.rs"), "pub fn original() {}").unwrap(); - - let cg = CodeGraph::init(project).unwrap(); - cg.index_all().unwrap(); - - let initial_stats = cg.get_stats().unwrap(); - - // Modify file - fs::write(project.join("src/lib.rs"), "pub fn modified() {}\npub fn added() {}").unwrap(); - - // Sync - let sync_result = cg.sync().unwrap(); - assert!(sync_result.files_modified > 0 || sync_result.files_added > 0); - - let new_stats = cg.get_stats().unwrap(); - // Should have the new function - let results = cg.search("modified", 10).unwrap(); - assert!(!results.is_empty()); -} -``` - -**Step 2: Run integration tests** - -```bash -cargo test --test integration_test -``` - -**Step 3: Fix any issues found** - -**Step 4: Run full test suite** - -```bash -cargo test -``` - -**Step 5: Run clippy and fix warnings** - -```bash -cargo clippy --all-targets -cargo fmt --all -``` - -**Step 6: Commit** - -```bash -git add . -git commit -m "feat: add integration tests and polish for full pipeline" -``` - ---- - -## Summary - -| Task | Module | Estimated Complexity | -|------|--------|---------------------| -| 1 | Project scaffold, types, errors | Low | -| 2 | Configuration | Low | -| 3 | SQLite database layer | Medium | -| 4 | Tree-sitter Rust extraction | High | -| 5 | Reference resolution | Medium | -| 6 | Graph traversal & queries | Medium | -| 7 | CLI interface | Low | -| 8 | Context builder | Medium | -| 9 | Vector embeddings | Medium | -| 10 | MCP server | Medium | -| 11 | Incremental sync | Low | -| 12 | Integration tests & polish | Low | diff --git a/docs/plans/2026-02-26-codegraph-rust-port-design.md b/docs/plans/2026-02-26-codegraph-rust-port-design.md deleted file mode 100644 index 7eab5fb7bc..0000000000 --- a/docs/plans/2026-02-26-codegraph-rust-port-design.md +++ /dev/null @@ -1,266 +0,0 @@ -# CodeGraph Rust Port — Design Document - -> **Archived record — not implementation authority.** This document preserves -> historical intent and evidence. Current requirements come only from the -> `docs/plans/tracedecay-v2/` hierarchy. Exact tests and counts, source-string -> checks, branch/commit/worktree choreography, snapshots, receipts, -> attestations, PR packets, and gate matrices below are not rebuild -> instructions; validate current parser, runtime, and product behavior directly. - -**Date:** 2026-02-26 -**Goal:** Replace the TypeScript CodeGraph implementation with a Rust-native version -**Status:** Design approved - -## Motivation - -The Rust version will become the canonical CodeGraph implementation, replacing the TypeScript version entirely. Benefits include single-binary distribution (no Node.js dependency), better performance for large codebases, and lower memory usage. - -## Scope - -### In Scope -- Tree-sitter AST extraction (Rust language only) -- SQLite graph database with FTS5 full-text search -- Graph queries (callers, callees, impact radius, call graph, dead code, type hierarchy) -- Vector embeddings via `ort` (ONNX Runtime) for semantic search -- MCP server for Claude Code integration (stdio transport) -- CLI interface - -### Out of Scope -- Multi-language support (Rust only for now — can be added later) -- Framework-specific resolvers (React, Express, Laravel, etc.) -- Interactive installer (unnecessary for a single binary) - -## Architecture - -Single crate, module-based structure: - -``` -code-graph/ -├── Cargo.toml -├── src/ -│ ├── main.rs # CLI entry point -│ ├── lib.rs # Library root -│ ├── config.rs # Configuration -│ ├── types.rs # Core types (Node, Edge, etc.) -│ ├── db/ # SQLite layer -│ │ ├── mod.rs -│ │ ├── connection.rs -│ │ ├── queries.rs -│ │ └── schema.sql -│ ├── extraction/ # Tree-sitter parsing -│ │ ├── mod.rs -│ │ └── rust.rs # Rust-specific extraction -│ ├── resolution/ # Reference resolution -│ │ ├── mod.rs -│ │ ├── imports.rs -│ │ └── names.rs -│ ├── graph/ # Graph traversal & queries -│ │ ├── mod.rs -│ │ ├── traversal.rs -│ │ └── queries.rs -│ ├── vectors/ # Embeddings -│ │ ├── mod.rs -│ │ ├── embedder.rs -│ │ └── search.rs -│ ├── context/ # Context building -│ │ ├── mod.rs -│ │ └── formatter.rs -│ ├── sync.rs # Incremental updates -│ └── mcp/ # MCP server -│ ├── mod.rs -│ ├── server.rs -│ └── tools.rs -``` - -## Core Types - -### Node Kinds - -```rust -enum NodeKind { - File, - Module, - Struct, - Enum, - EnumVariant, - Trait, - Function, - Method, - Impl, - Const, - Static, - TypeAlias, - Field, - Macro, - Use, -} -``` - -### Node - -```rust -struct Node { - id: String, // deterministic: "file_path::symbol_path" - kind: NodeKind, - name: String, - file_path: String, - start_line: u32, - end_line: u32, - signature: Option, - docstring: Option, - visibility: Visibility, // Pub, PubCrate, Private - body_hash: Option, -} -``` - -### Edge Kinds - -```rust -enum EdgeKind { - Contains, // file/module contains items - Calls, // function calls function - Uses, // references a symbol (use statement) - Implements, // impl Trait for Struct - TypeOf, // field/variable type references - Returns, // function return type - DerivesMacro, // #[derive(Debug, Clone)] -} -``` - -### Edge - -```rust -struct Edge { - source_id: String, - target_id: String, - kind: EdgeKind, - line: Option, -} -``` - -## SQLite Schema - -Tables: -- `nodes` — all extracted symbols with metadata -- `edges` — relationships between nodes -- `files` — tracked files with content hashes (for incremental sync) -- `nodes_fts` — FTS5 virtual table on node names and signatures -- `vectors` — embeddings stored as BLOBs with node_id foreign key - -## Extraction Pipeline - -Uses `tree-sitter` + `tree-sitter-rust` (native bindings, not WASM). - -### What We Extract - -| Rust Construct | Node Kind | Edges Emitted | -|---|---|---| -| `fn foo()` | Function | Contains (from parent), Calls (to callees), Returns | -| `struct Foo` | Struct | Contains (fields) | -| `enum Bar` | Enum | Contains (variants) | -| `impl Trait for S` | Impl | Implements (trait → struct) | -| `impl S` | Impl | Contains (methods) | -| `use crate::x` | Use | Uses (resolved target) | -| `#[derive(..)]` | — | DerivesMacro edges | -| `mod foo` | Module | Contains | -| `const`/`static` | Const/Static | TypeOf | - -### Processing Flow - -``` -file path → read source → tree-sitter parse → walk AST → emit Nodes + Edges - → resolve use statements → resolve call targets → store in SQLite -``` - -## Reference Resolution - -Two strategies (no framework-specific resolvers): - -1. **Use-statement resolution:** Follow `use` paths to find target symbols. Handles `use crate::`, `use super::`, `use self::`, and external crate references. - -2. **Name-based matching:** For method calls (`.foo()`), match by method name against all known methods. Ranking: same module > same crate > external. - -Type-informed matching (narrowing method resolution by receiver type) is a stretch goal — treat as best-effort. - -## Graph Queries - -| Query | Description | Implementation | -|---|---|---| -| `callers(node_id)` | What calls this function? | edges WHERE target = ? AND kind = calls | -| `callees(node_id)` | What does this call? | edges WHERE source = ? AND kind = calls | -| `impact(node_id, depth)` | Transitive callers to N levels | BFS over caller edges | -| `call_graph(node_id)` | Bidirectional call relationships | BFS both directions | -| `dead_code()` | Unreferenced symbols | Zero in-degree, excluding main/#[test]/pub | -| `type_hierarchy(node_id)` | Trait implementation chain | Follow Implements edges | -| `search(query)` | Full-text symbol search | FTS5 on nodes_fts | -| `semantic_search(query, k)` | Vector similarity | Cosine similarity on embeddings | - -## MCP Server - -Stdio transport, JSON-RPC protocol. Tools exposed to Claude Code: - -- `codegraph_search` — find symbols by name (FTS5) -- `codegraph_context` — build context for a task (semantic search + graph expansion) -- `codegraph_callers` / `codegraph_callees` — call relationships -- `codegraph_impact` — impact radius analysis -- `codegraph_node` — get full symbol details -- `codegraph_status` — index stats and health - -Implementation: `tokio` async I/O, read/write JSON-RPC over stdin/stdout. - -## CLI - -``` -codegraph init [path] # Create .codegraph/ config -codegraph index [path] # Full index -codegraph sync [path] # Incremental update -codegraph status [path] # Show stats -codegraph query # Search symbols -codegraph context # Build context -codegraph serve # Start MCP server (stdio) -``` - -## Dependencies - -| Crate | Purpose | -|---|---| -| `rusqlite` (bundled) | SQLite database | -| `tree-sitter` | AST parsing framework | -| `tree-sitter-rust` | Rust grammar | -| `ort` | ONNX Runtime for embeddings | -| `clap` | CLI argument parsing | -| `serde` / `serde_json` | Serialization | -| `tokio` | Async runtime (MCP server) | -| `thiserror` | Error types | -| `tracing` | Structured logging | -| `sha2` | Content hashing for sync | - -## Configuration - -Per-project `.codegraph/config.json`: - -```json -{ - "version": 1, - "root_dir": ".", - "include": ["**/*.rs"], - "exclude": ["target/**", "tests/**"], - "max_file_size": 1048576, - "extract_docstrings": true, - "track_call_sites": true, - "enable_embeddings": false -} -``` - -## Implementation Order - -1. **Types & config** — Core types, configuration, error handling -2. **SQLite layer** — Schema, connection, CRUD operations, FTS5 -3. **Tree-sitter extraction** — Parse Rust files, emit nodes and edges -4. **Reference resolution** — Use-statement and name-based resolution -5. **Graph queries** — Callers, callees, impact, dead code, etc. -6. **CLI** — Init, index, sync, status, query commands -7. **Context builder** — Semantic search + graph expansion for context -8. **Vector embeddings** — ONNX runtime integration, embedding storage -9. **MCP server** — Stdio JSON-RPC transport, tool handlers -10. **Incremental sync** — Content hashing, dirty detection, partial re-index diff --git a/docs/plans/2026-02-26-multi-language-implementation-plan.md b/docs/plans/2026-02-26-multi-language-implementation-plan.md deleted file mode 100644 index f0aa15e263..0000000000 --- a/docs/plans/2026-02-26-multi-language-implementation-plan.md +++ /dev/null @@ -1,1371 +0,0 @@ -# Multi-Language Support (Go + Java) Implementation Plan - -> **Archived record — not implementation authority.** This document preserves -> historical intent and evidence. Current requirements come only from the -> `docs/plans/tracedecay-v2/` hierarchy. Exact tests and counts, source-string -> checks, branch/commit/worktree choreography, snapshots, receipts, -> attestations, PR packets, and gate matrices below are not rebuild -> instructions; validate current parser, runtime, and product behavior directly. - -**Goal:** Add Go and Java language support to codegraph with deep extraction, using a trait-based abstraction layer. - -**Architecture:** Introduce a `LanguageExtractor` trait and `LanguageRegistry` that dispatches to per-language extractors based on file extension. Each extractor uses tree-sitter with a language-specific grammar. The existing `RustExtractor` is retrofitted to implement the trait. - -**Tech Stack:** tree-sitter, tree-sitter-go, tree-sitter-java, Rust traits - ---- - -### Task 1: Add tree-sitter dependencies to Cargo.toml - -**Files:** -- Modify: `Cargo.toml` - -**Step 1: Add dependencies** - -Add `tree-sitter-go` and `tree-sitter-java` to `[dependencies]`: - -```toml -tree-sitter-go = "0.23" -tree-sitter-java = "0.23" -``` - -**Step 2: Verify it compiles** - -Run: `cargo check` -Expected: compiles successfully (new deps are unused but that's OK) - -**Step 3: Commit** - -```bash -git add Cargo.toml Cargo.lock -git commit -m "feat: add tree-sitter-go and tree-sitter-java dependencies" -``` - ---- - -### Task 2: Expand NodeKind and EdgeKind enums - -**Files:** -- Modify: `src/types.rs` -- Test: `tests/types_test.rs` - -**Step 1: Write tests for new NodeKind variants** - -Add to `tests/types_test.rs`: - -```rust -#[test] -fn test_new_node_kinds_roundtrip() { - let kinds = vec![ - (NodeKind::Class, "class"), - (NodeKind::Interface, "interface"), - (NodeKind::Constructor, "constructor"), - (NodeKind::Annotation, "annotation"), - (NodeKind::AnnotationUsage, "annotation_usage"), - (NodeKind::Package, "package"), - (NodeKind::InnerClass, "inner_class"), - (NodeKind::InitBlock, "init_block"), - (NodeKind::AbstractMethod, "abstract_method"), - (NodeKind::InterfaceType, "interface_type"), - (NodeKind::StructMethod, "struct_method"), - (NodeKind::GoPackage, "go_package"), - (NodeKind::StructTag, "struct_tag"), - (NodeKind::GenericParam, "generic_param"), - ]; - for (kind, expected_str) in kinds { - assert_eq!(kind.as_str(), expected_str); - assert_eq!(NodeKind::from_str(expected_str), Some(kind)); - } -} - -#[test] -fn test_new_edge_kinds_roundtrip() { - let kinds = vec![ - (EdgeKind::Extends, "extends"), - (EdgeKind::Annotates, "annotates"), - (EdgeKind::Receives, "receives"), - ]; - for (kind, expected_str) in kinds { - assert_eq!(kind.as_str(), expected_str); - assert_eq!(EdgeKind::from_str(expected_str), Some(kind)); - } -} -``` - -**Step 2: Run tests to verify they fail** - -Run: `cargo test --test types_test test_new_node_kinds_roundtrip test_new_edge_kinds_roundtrip` -Expected: FAIL — variants don't exist yet - -**Step 3: Add NodeKind variants** - -In `src/types.rs`, add to `NodeKind` enum after `Use`: - -```rust - // Java-specific - Class, - Interface, - Constructor, - Annotation, - AnnotationUsage, - Package, - InnerClass, - InitBlock, - AbstractMethod, - // Go-specific - InterfaceType, - StructMethod, - GoPackage, - StructTag, - // Shared - GenericParam, -``` - -Add corresponding arms to `as_str()`: - -```rust - NodeKind::Class => "class", - NodeKind::Interface => "interface", - NodeKind::Constructor => "constructor", - NodeKind::Annotation => "annotation", - NodeKind::AnnotationUsage => "annotation_usage", - NodeKind::Package => "package", - NodeKind::InnerClass => "inner_class", - NodeKind::InitBlock => "init_block", - NodeKind::AbstractMethod => "abstract_method", - NodeKind::InterfaceType => "interface_type", - NodeKind::StructMethod => "struct_method", - NodeKind::GoPackage => "go_package", - NodeKind::StructTag => "struct_tag", - NodeKind::GenericParam => "generic_param", -``` - -Add corresponding arms to `from_str()`: - -```rust - "class" => Some(NodeKind::Class), - "interface" => Some(NodeKind::Interface), - "constructor" => Some(NodeKind::Constructor), - "annotation" => Some(NodeKind::Annotation), - "annotation_usage" => Some(NodeKind::AnnotationUsage), - "package" => Some(NodeKind::Package), - "inner_class" => Some(NodeKind::InnerClass), - "init_block" => Some(NodeKind::InitBlock), - "abstract_method" => Some(NodeKind::AbstractMethod), - "interface_type" => Some(NodeKind::InterfaceType), - "struct_method" => Some(NodeKind::StructMethod), - "go_package" => Some(NodeKind::GoPackage), - "struct_tag" => Some(NodeKind::StructTag), - "generic_param" => Some(NodeKind::GenericParam), -``` - -**Step 4: Add EdgeKind variants** - -In `src/types.rs`, add to `EdgeKind` enum after `DerivesMacro`: - -```rust - Extends, - Annotates, - Receives, -``` - -Add to `EdgeKind::as_str()`: - -```rust - EdgeKind::Extends => "extends", - EdgeKind::Annotates => "annotates", - EdgeKind::Receives => "receives", -``` - -Add to `EdgeKind::from_str()`: - -```rust - "extends" => Some(EdgeKind::Extends), - "annotates" => Some(EdgeKind::Annotates), - "receives" => Some(EdgeKind::Receives), -``` - -**Step 5: Run tests to verify they pass** - -Run: `cargo test --test types_test` -Expected: PASS - -**Step 6: Run full test suite to check no regressions** - -Run: `cargo test` -Expected: PASS — existing code doesn't break since we only added new variants - -**Step 7: Commit** - -```bash -git add src/types.rs tests/types_test.rs -git commit -m "feat: expand NodeKind and EdgeKind enums for Go and Java support" -``` - ---- - -### Task 3: Create LanguageExtractor trait and LanguageRegistry - -**Files:** -- Modify: `src/extraction/mod.rs` -- Test: `tests/extraction_test.rs` - -**Step 1: Write test for language registry** - -Add to `tests/extraction_test.rs`: - -```rust -use codegraph::extraction::LanguageRegistry; - -#[test] -fn test_language_registry_finds_rust_extractor() { - let registry = LanguageRegistry::new(); - assert!(registry.extractor_for_file("src/main.rs").is_some()); - assert!(registry.extractor_for_file("lib.rs").is_some()); -} - -#[test] -fn test_language_registry_finds_go_extractor() { - let registry = LanguageRegistry::new(); - assert!(registry.extractor_for_file("main.go").is_some()); - assert!(registry.extractor_for_file("pkg/server.go").is_some()); -} - -#[test] -fn test_language_registry_finds_java_extractor() { - let registry = LanguageRegistry::new(); - assert!(registry.extractor_for_file("Main.java").is_some()); - assert!(registry.extractor_for_file("src/com/example/App.java").is_some()); -} - -#[test] -fn test_language_registry_returns_none_for_unknown() { - let registry = LanguageRegistry::new(); - assert!(registry.extractor_for_file("script.py").is_none()); - assert!(registry.extractor_for_file("style.css").is_none()); - assert!(registry.extractor_for_file("README.md").is_none()); -} - -#[test] -fn test_language_registry_supported_extensions() { - let registry = LanguageRegistry::new(); - let exts = registry.supported_extensions(); - assert!(exts.contains(&"rs")); - assert!(exts.contains(&"go")); - assert!(exts.contains(&"java")); -} -``` - -**Step 2: Run tests to verify they fail** - -Run: `cargo test --test extraction_test test_language_registry` -Expected: FAIL — `LanguageRegistry` doesn't exist - -**Step 3: Define trait and registry in `src/extraction/mod.rs`** - -Replace the contents of `src/extraction/mod.rs` with: - -```rust -/// Tree-sitter based source code extraction module. -/// -/// This module provides extractors that parse source files using tree-sitter -/// and produce structured graph nodes and edges. -mod rust_extractor; -mod go_extractor; -mod java_extractor; - -pub use rust_extractor::RustExtractor; -pub use go_extractor::GoExtractor; -pub use java_extractor::JavaExtractor; - -use crate::types::ExtractionResult; - -/// Trait for language-specific source code extractors. -/// -/// Each implementation handles a single programming language, -/// using tree-sitter to parse source and emit graph nodes and edges. -pub trait LanguageExtractor: Send + Sync { - /// File extensions this extractor handles (without leading dot). - fn extensions(&self) -> &[&str]; - - /// Human-readable language name. - fn language_name(&self) -> &str; - - /// Extract nodes, edges, and unresolved refs from source code. - /// - /// `file_path` is the relative path used for qualified names and node IDs. - /// `source` is the source code to parse. - fn extract(&self, file_path: &str, source: &str) -> ExtractionResult; -} - -/// Registry of all available language extractors. -/// -/// Dispatches to the correct extractor based on file extension. -pub struct LanguageRegistry { - extractors: Vec>, -} - -impl LanguageRegistry { - /// Creates a new registry with all built-in language extractors. - pub fn new() -> Self { - Self { - extractors: vec![ - Box::new(RustExtractor), - Box::new(GoExtractor), - Box::new(JavaExtractor), - ], - } - } - - /// Returns the extractor for a file path based on its extension. - pub fn extractor_for_file(&self, path: &str) -> Option<&dyn LanguageExtractor> { - let ext = path.rsplit('.').next()?; - self.extractors - .iter() - .find(|e| e.extensions().contains(&ext)) - .map(|e| e.as_ref()) - } - - /// Returns all supported file extensions across all extractors. - pub fn supported_extensions(&self) -> Vec<&str> { - self.extractors - .iter() - .flat_map(|e| e.extensions().iter().copied()) - .collect() - } -} - -impl Default for LanguageRegistry { - fn default() -> Self { - Self::new() - } -} -``` - -**Step 4: Create stub Go and Java extractors** - -Create `src/extraction/go_extractor.rs` with a minimal stub: - -```rust -/// Tree-sitter based Go source code extractor. -use crate::extraction::LanguageExtractor; -use crate::types::ExtractionResult; - -/// Extracts code graph nodes and edges from Go source files. -pub struct GoExtractor; - -impl LanguageExtractor for GoExtractor { - fn extensions(&self) -> &[&str] { - &["go"] - } - - fn language_name(&self) -> &str { - "Go" - } - - fn extract(&self, _file_path: &str, _source: &str) -> ExtractionResult { - ExtractionResult { - nodes: Vec::new(), - edges: Vec::new(), - unresolved_refs: Vec::new(), - errors: vec!["Go extraction not yet implemented".to_string()], - duration_ms: 0, - } - } -} -``` - -Create `src/extraction/java_extractor.rs` with a minimal stub: - -```rust -/// Tree-sitter based Java source code extractor. -use crate::extraction::LanguageExtractor; -use crate::types::ExtractionResult; - -/// Extracts code graph nodes and edges from Java source files. -pub struct JavaExtractor; - -impl LanguageExtractor for JavaExtractor { - fn extensions(&self) -> &[&str] { - &["java"] - } - - fn language_name(&self) -> &str { - "Java" - } - - fn extract(&self, _file_path: &str, _source: &str) -> ExtractionResult { - ExtractionResult { - nodes: Vec::new(), - edges: Vec::new(), - unresolved_refs: Vec::new(), - errors: vec!["Java extraction not yet implemented".to_string()], - duration_ms: 0, - } - } -} -``` - -**Step 5: Implement LanguageExtractor for RustExtractor** - -Add to bottom of `src/extraction/rust_extractor.rs`: - -```rust -impl crate::extraction::LanguageExtractor for RustExtractor { - fn extensions(&self) -> &[&str] { - &["rs"] - } - - fn language_name(&self) -> &str { - "Rust" - } - - fn extract(&self, file_path: &str, source: &str) -> ExtractionResult { - RustExtractor::extract(file_path, source) - } -} -``` - -**Step 6: Run tests to verify they pass** - -Run: `cargo test --test extraction_test` -Expected: PASS — all existing extraction tests still pass, plus new registry tests - -**Step 7: Commit** - -```bash -git add src/extraction/mod.rs src/extraction/rust_extractor.rs src/extraction/go_extractor.rs src/extraction/java_extractor.rs tests/extraction_test.rs -git commit -m "feat: add LanguageExtractor trait, LanguageRegistry, and stub extractors" -``` - ---- - -### Task 4: Integrate LanguageRegistry into CodeGraph - -**Files:** -- Modify: `src/codegraph.rs` -- Modify: `src/config.rs` - -**Step 1: Update config defaults** - -In `src/config.rs`, change the `Default` impl: - -Replace the `include` default: -```rust -include: vec!["**/*.rs".to_string()], -``` -with: -```rust -include: vec![ - "**/*.rs".to_string(), - "**/*.go".to_string(), - "**/*.java".to_string(), -], -``` - -Add to the `exclude` default list: -```rust - "bin/**".to_string(), - "build/**".to_string(), - "out/**".to_string(), - ".gradle/**".to_string(), -``` - -**Step 2: Update CodeGraph to use LanguageRegistry** - -In `src/codegraph.rs`: - -1. Add import: `use crate::extraction::LanguageRegistry;` -2. Remove: `use crate::extraction::RustExtractor;` -3. Add `registry` field to the `CodeGraph` struct: - ```rust - pub struct CodeGraph { - db: Database, - config: CodeGraphConfig, - project_root: PathBuf, - registry: LanguageRegistry, - } - ``` -4. Add `registry: LanguageRegistry::new()` to both `init()` and `open()` constructors. -5. In `index_all()`, replace line 146: - ```rust - let result = RustExtractor::extract(file_path, &source); - ``` - with: - ```rust - let extractor = match self.registry.extractor_for_file(file_path) { - Some(e) => e, - None => continue, - }; - let result = extractor.extract(file_path, &source); - ``` -6. In `sync()`, replace line 227: - ```rust - let result = RustExtractor::extract(file_path, &source); - ``` - with: - ```rust - let extractor = match self.registry.extractor_for_file(file_path) { - Some(e) => e, - None => continue, - }; - let result = extractor.extract(file_path, &source); - ``` - -**Step 3: Verify compilation and tests** - -Run: `cargo test` -Expected: PASS — all existing tests pass, Rust extraction behavior unchanged - -**Step 4: Commit** - -```bash -git add src/codegraph.rs src/config.rs -git commit -m "feat: integrate LanguageRegistry into CodeGraph for multi-language dispatch" -``` - ---- - -### Task 5: Implement Go extractor — types and package - -**Files:** -- Modify: `src/extraction/go_extractor.rs` -- Test: `tests/go_extraction_test.rs` (new) - -**Step 1: Write tests for Go package and struct extraction** - -Create `tests/go_extraction_test.rs`: - -```rust -use codegraph::extraction::GoExtractor; -use codegraph::extraction::LanguageExtractor; -use codegraph::types::*; - -#[test] -fn test_go_extract_package() { - let source = r#"package main - -import "fmt" - -func main() { - fmt.Println("hello") -} -"#; - let extractor = GoExtractor; - let result = extractor.extract("main.go", source); - assert!(result.errors.is_empty(), "errors: {:?}", result.errors); - let pkgs: Vec<_> = result.nodes.iter().filter(|n| n.kind == NodeKind::GoPackage).collect(); - assert_eq!(pkgs.len(), 1); - assert_eq!(pkgs[0].name, "main"); -} - -#[test] -fn test_go_extract_function() { - let source = r#"package main - -// Add adds two numbers. -func Add(a, b int) int { - return a + b -} - -func helper() {} -"#; - let extractor = GoExtractor; - let result = extractor.extract("math.go", source); - assert!(result.errors.is_empty(), "errors: {:?}", result.errors); - let fns: Vec<_> = result.nodes.iter().filter(|n| n.kind == NodeKind::Function).collect(); - assert_eq!(fns.len(), 2); - let add_fn = fns.iter().find(|f| f.name == "Add").unwrap(); - assert_eq!(add_fn.visibility, Visibility::Pub); // uppercase = exported - assert!(add_fn.docstring.as_ref().unwrap().contains("Add adds two numbers")); - let helper_fn = fns.iter().find(|f| f.name == "helper").unwrap(); - assert_eq!(helper_fn.visibility, Visibility::Private); // lowercase = unexported -} - -#[test] -fn test_go_extract_struct_with_fields() { - let source = r#"package model - -// Point represents a 2D point. -type Point struct { - X float64 - Y float64 - label string -} -"#; - let extractor = GoExtractor; - let result = extractor.extract("model/point.go", source); - assert!(result.errors.is_empty(), "errors: {:?}", result.errors); - let structs: Vec<_> = result.nodes.iter().filter(|n| n.kind == NodeKind::Struct).collect(); - assert_eq!(structs.len(), 1); - assert_eq!(structs[0].name, "Point"); - assert_eq!(structs[0].visibility, Visibility::Pub); - let fields: Vec<_> = result.nodes.iter().filter(|n| n.kind == NodeKind::Field).collect(); - assert_eq!(fields.len(), 3); - // X is exported, label is not - let x_field = fields.iter().find(|f| f.name == "X").unwrap(); - assert_eq!(x_field.visibility, Visibility::Pub); - let label_field = fields.iter().find(|f| f.name == "label").unwrap(); - assert_eq!(label_field.visibility, Visibility::Private); -} - -#[test] -fn test_go_extract_struct_tags() { - let source = r#"package model - -type Config struct { - Name string `json:"name" yaml:"name"` - Port int `json:"port"` -} -"#; - let extractor = GoExtractor; - let result = extractor.extract("model/config.go", source); - assert!(result.errors.is_empty(), "errors: {:?}", result.errors); - let tags: Vec<_> = result.nodes.iter().filter(|n| n.kind == NodeKind::StructTag).collect(); - assert!(tags.len() >= 2, "should extract struct tags"); -} - -#[test] -fn test_go_extract_interface() { - let source = r#"package io - -// Reader is the interface for reading. -type Reader interface { - Read(p []byte) (n int, err error) -} -"#; - let extractor = GoExtractor; - let result = extractor.extract("io/reader.go", source); - assert!(result.errors.is_empty(), "errors: {:?}", result.errors); - let ifaces: Vec<_> = result.nodes.iter().filter(|n| n.kind == NodeKind::InterfaceType).collect(); - assert_eq!(ifaces.len(), 1); - assert_eq!(ifaces[0].name, "Reader"); - assert_eq!(ifaces[0].visibility, Visibility::Pub); -} - -#[test] -fn test_go_extract_method_with_receiver() { - let source = r#"package model - -type Circle struct { - Radius float64 -} - -// Area calculates the area. -func (c *Circle) Area() float64 { - return 3.14159 * c.Radius * c.Radius -} - -func (c Circle) String() string { - return "circle" -} -"#; - let extractor = GoExtractor; - let result = extractor.extract("model/circle.go", source); - assert!(result.errors.is_empty(), "errors: {:?}", result.errors); - let methods: Vec<_> = result.nodes.iter().filter(|n| n.kind == NodeKind::StructMethod).collect(); - assert_eq!(methods.len(), 2); - // Check Receives edges - let receives: Vec<_> = result.edges.iter().filter(|e| e.kind == EdgeKind::Receives).collect(); - assert!(!receives.is_empty(), "should have Receives edges for methods with receivers"); -} - -#[test] -fn test_go_extract_imports() { - let source = r#"package main - -import ( - "fmt" - "os" - "github.com/pkg/errors" -) -"#; - let extractor = GoExtractor; - let result = extractor.extract("main.go", source); - assert!(result.errors.is_empty(), "errors: {:?}", result.errors); - let uses: Vec<_> = result.nodes.iter().filter(|n| n.kind == NodeKind::Use).collect(); - assert_eq!(uses.len(), 3); -} - -#[test] -fn test_go_extract_const_and_var() { - let source = r#"package main - -const MaxSize = 1024 - -var counter int -"#; - let extractor = GoExtractor; - let result = extractor.extract("main.go", source); - assert!(result.errors.is_empty(), "errors: {:?}", result.errors); - let consts: Vec<_> = result.nodes.iter().filter(|n| n.kind == NodeKind::Const).collect(); - assert_eq!(consts.len(), 1); - assert_eq!(consts[0].name, "MaxSize"); - let statics: Vec<_> = result.nodes.iter().filter(|n| n.kind == NodeKind::Static).collect(); - assert_eq!(statics.len(), 1); - assert_eq!(statics[0].name, "counter"); -} - -#[test] -fn test_go_extract_call_sites() { - let source = r#"package main - -import "fmt" - -func greet(name string) { - fmt.Println("Hello", name) -} - -func main() { - greet("world") -} -"#; - let extractor = GoExtractor; - let result = extractor.extract("main.go", source); - assert!(result.errors.is_empty(), "errors: {:?}", result.errors); - let call_refs: Vec<_> = result.unresolved_refs.iter() - .filter(|r| r.reference_kind == EdgeKind::Calls) - .collect(); - assert!(!call_refs.is_empty(), "should have call refs"); -} - -#[test] -fn test_go_extract_type_alias() { - let source = r#"package main - -type StringSlice = []string -"#; - let extractor = GoExtractor; - let result = extractor.extract("main.go", source); - assert!(result.errors.is_empty(), "errors: {:?}", result.errors); - let aliases: Vec<_> = result.nodes.iter().filter(|n| n.kind == NodeKind::TypeAlias).collect(); - assert_eq!(aliases.len(), 1); - assert_eq!(aliases[0].name, "StringSlice"); -} - -#[test] -fn test_go_extract_interface_embedding() { - let source = r#"package io - -type Reader interface { - Read(p []byte) (int, error) -} - -type ReadWriter interface { - Reader - Write(p []byte) (int, error) -} -"#; - let extractor = GoExtractor; - let result = extractor.extract("io/io.go", source); - assert!(result.errors.is_empty(), "errors: {:?}", result.errors); - // Should have an Extends edge or unresolved ref for Reader embedded in ReadWriter - let has_extends = result.edges.iter().any(|e| e.kind == EdgeKind::Extends) - || result.unresolved_refs.iter().any(|r| r.reference_kind == EdgeKind::Extends); - assert!(has_extends, "should detect interface embedding as Extends"); -} - -#[test] -fn test_go_extract_generic_function() { - let source = r#"package main - -func Map[T any, U any](s []T, f func(T) U) []U { - r := make([]U, len(s)) - for i, v := range s { - r[i] = f(v) - } - return r -} -"#; - let extractor = GoExtractor; - let result = extractor.extract("main.go", source); - assert!(result.errors.is_empty(), "errors: {:?}", result.errors); - let fns: Vec<_> = result.nodes.iter().filter(|n| n.kind == NodeKind::Function).collect(); - assert_eq!(fns.len(), 1); - assert_eq!(fns[0].name, "Map"); - let generics: Vec<_> = result.nodes.iter().filter(|n| n.kind == NodeKind::GenericParam).collect(); - assert!(generics.len() >= 2, "should extract generic type params T and U"); -} - -#[test] -fn test_go_file_node_is_root() { - let source = r#"package main - -func main() {} -"#; - let extractor = GoExtractor; - let result = extractor.extract("main.go", source); - let files: Vec<_> = result.nodes.iter().filter(|n| n.kind == NodeKind::File).collect(); - assert_eq!(files.len(), 1); - assert_eq!(files[0].name, "main.go"); -} - -#[test] -fn test_go_contains_edges() { - let source = r#"package main - -type Foo struct { - Bar int -} - -func (f Foo) Baz() {} -"#; - let extractor = GoExtractor; - let result = extractor.extract("main.go", source); - let contains: Vec<_> = result.edges.iter().filter(|e| e.kind == EdgeKind::Contains).collect(); - // File contains: GoPackage, Struct, StructMethod; Struct contains: Field - assert!(contains.len() >= 4, "should have Contains edges: {:?}", contains.len()); -} - -#[test] -fn test_go_qualified_names() { - let source = r#"package server - -func HandleRequest() {} -"#; - let extractor = GoExtractor; - let result = extractor.extract("pkg/server/handler.go", source); - let fns: Vec<_> = result.nodes.iter().filter(|n| n.kind == NodeKind::Function).collect(); - assert_eq!(fns.len(), 1); - assert!(fns[0].qualified_name.contains("HandleRequest")); - assert!(fns[0].qualified_name.contains("handler.go")); -} -``` - -**Step 2: Run tests to verify they fail** - -Run: `cargo test --test go_extraction_test` -Expected: FAIL — Go extractor is a stub - -**Step 3: Implement the full Go extractor** - -Replace `src/extraction/go_extractor.rs` with the full implementation. The implementation follows the same `ExtractionState` pattern as `RustExtractor`: - -- `parse_source()` uses `tree_sitter_go::LANGUAGE` -- `visit_node()` dispatches on tree-sitter Go node kinds: - - `package_clause` → `GoPackage` - - `function_declaration` → `Function` - - `method_declaration` → `StructMethod` + `Receives` edge - - `type_declaration` → dispatches on child type spec: - - `struct_type` → `Struct` with `Field` children (with `StructTag`) - - `interface_type` → `InterfaceType` with embedded interface `Extends` edges - - type alias (has `=`) → `TypeAlias` - - `import_declaration` → `Use` nodes (one per import spec) - - `const_declaration` → `Const` nodes - - `var_declaration` → `Static` nodes -- Visibility: first character uppercase → `Pub`, lowercase → `Private` -- Doc comments: collect `comment` nodes preceding declarations -- Signatures: text from start to `{` -- Call sites: scan for `call_expression` and `selector_expression` calls -- Generics: `type_parameter_list` → `GenericParam` nodes - -**Step 4: Run tests to verify they pass** - -Run: `cargo test --test go_extraction_test` -Expected: PASS - -**Step 5: Run full test suite** - -Run: `cargo test` -Expected: PASS — no regressions - -**Step 6: Commit** - -```bash -git add src/extraction/go_extractor.rs tests/go_extraction_test.rs -git commit -m "feat: implement Go extractor with deep extraction support" -``` - ---- - -### Task 6: Implement Java extractor - -**Files:** -- Modify: `src/extraction/java_extractor.rs` -- Test: `tests/java_extraction_test.rs` (new) - -**Step 1: Write tests for Java extraction** - -Create `tests/java_extraction_test.rs`: - -```rust -use codegraph::extraction::JavaExtractor; -use codegraph::extraction::LanguageExtractor; -use codegraph::types::*; - -#[test] -fn test_java_extract_package() { - let source = r#"package com.example.app; - -public class Main { - public static void main(String[] args) {} -} -"#; - let extractor = JavaExtractor; - let result = extractor.extract("src/Main.java", source); - assert!(result.errors.is_empty(), "errors: {:?}", result.errors); - let pkgs: Vec<_> = result.nodes.iter().filter(|n| n.kind == NodeKind::Package).collect(); - assert_eq!(pkgs.len(), 1); - assert_eq!(pkgs[0].name, "com.example.app"); -} - -#[test] -fn test_java_extract_class() { - let source = r#"package com.example; - -/** - * A simple calculator. - */ -public class Calculator { - public int add(int a, int b) { - return a + b; - } -} -"#; - let extractor = JavaExtractor; - let result = extractor.extract("Calculator.java", source); - assert!(result.errors.is_empty(), "errors: {:?}", result.errors); - let classes: Vec<_> = result.nodes.iter().filter(|n| n.kind == NodeKind::Class).collect(); - assert_eq!(classes.len(), 1); - assert_eq!(classes[0].name, "Calculator"); - assert_eq!(classes[0].visibility, Visibility::Pub); - assert!(classes[0].docstring.as_ref().unwrap().contains("simple calculator")); -} - -#[test] -fn test_java_extract_methods() { - let source = r#" -public class Foo { - public void doSomething() {} - private int compute(int x) { return x * 2; } - protected String getName() { return "foo"; } -} -"#; - let extractor = JavaExtractor; - let result = extractor.extract("Foo.java", source); - assert!(result.errors.is_empty(), "errors: {:?}", result.errors); - let methods: Vec<_> = result.nodes.iter().filter(|n| n.kind == NodeKind::Method).collect(); - assert_eq!(methods.len(), 3); - let do_something = methods.iter().find(|m| m.name == "doSomething").unwrap(); - assert_eq!(do_something.visibility, Visibility::Pub); - let compute = methods.iter().find(|m| m.name == "compute").unwrap(); - assert_eq!(compute.visibility, Visibility::Private); - let get_name = methods.iter().find(|m| m.name == "getName").unwrap(); - assert_eq!(get_name.visibility, Visibility::PubCrate); // protected maps to PubCrate -} - -#[test] -fn test_java_extract_constructor() { - let source = r#" -public class Person { - private String name; - public Person(String name) { - this.name = name; - } -} -"#; - let extractor = JavaExtractor; - let result = extractor.extract("Person.java", source); - assert!(result.errors.is_empty(), "errors: {:?}", result.errors); - let constructors: Vec<_> = result.nodes.iter().filter(|n| n.kind == NodeKind::Constructor).collect(); - assert_eq!(constructors.len(), 1); - assert_eq!(constructors[0].name, "Person"); -} - -#[test] -fn test_java_extract_interface() { - let source = r#" -public interface Drawable { - void draw(); - double area(); -} -"#; - let extractor = JavaExtractor; - let result = extractor.extract("Drawable.java", source); - assert!(result.errors.is_empty(), "errors: {:?}", result.errors); - let ifaces: Vec<_> = result.nodes.iter().filter(|n| n.kind == NodeKind::Interface).collect(); - assert_eq!(ifaces.len(), 1); - assert_eq!(ifaces[0].name, "Drawable"); - let methods: Vec<_> = result.nodes.iter() - .filter(|n| n.kind == NodeKind::Method || n.kind == NodeKind::AbstractMethod) - .collect(); - assert_eq!(methods.len(), 2); -} - -#[test] -fn test_java_extract_enum() { - let source = r#" -public enum Color { - RED, - GREEN, - BLUE -} -"#; - let extractor = JavaExtractor; - let result = extractor.extract("Color.java", source); - assert!(result.errors.is_empty(), "errors: {:?}", result.errors); - let enums: Vec<_> = result.nodes.iter().filter(|n| n.kind == NodeKind::Enum).collect(); - assert_eq!(enums.len(), 1); - let variants: Vec<_> = result.nodes.iter().filter(|n| n.kind == NodeKind::EnumVariant).collect(); - assert_eq!(variants.len(), 3); -} - -#[test] -fn test_java_extract_fields() { - let source = r#" -public class Config { - public static final int MAX_SIZE = 1024; - private String name; - protected int port; -} -"#; - let extractor = JavaExtractor; - let result = extractor.extract("Config.java", source); - assert!(result.errors.is_empty(), "errors: {:?}", result.errors); - let fields: Vec<_> = result.nodes.iter().filter(|n| n.kind == NodeKind::Field).collect(); - assert_eq!(fields.len(), 3); - let max_size = fields.iter().find(|f| f.name == "MAX_SIZE").unwrap(); - assert_eq!(max_size.visibility, Visibility::Pub); -} - -#[test] -fn test_java_extract_imports() { - let source = r#" -import java.util.List; -import java.util.Map; -import static java.lang.Math.PI; - -public class Foo {} -"#; - let extractor = JavaExtractor; - let result = extractor.extract("Foo.java", source); - assert!(result.errors.is_empty(), "errors: {:?}", result.errors); - let uses: Vec<_> = result.nodes.iter().filter(|n| n.kind == NodeKind::Use).collect(); - assert_eq!(uses.len(), 3); -} - -#[test] -fn test_java_extract_extends_implements() { - let source = r#" -interface Runnable { void run(); } -class Base {} -class Worker extends Base implements Runnable { - public void run() {} -} -"#; - let extractor = JavaExtractor; - let result = extractor.extract("Worker.java", source); - assert!(result.errors.is_empty(), "errors: {:?}", result.errors); - let has_extends = result.edges.iter().any(|e| e.kind == EdgeKind::Extends) - || result.unresolved_refs.iter().any(|r| r.reference_kind == EdgeKind::Extends); - assert!(has_extends, "should detect extends"); - let has_implements = result.edges.iter().any(|e| e.kind == EdgeKind::Implements) - || result.unresolved_refs.iter().any(|r| r.reference_kind == EdgeKind::Implements); - assert!(has_implements, "should detect implements"); -} - -#[test] -fn test_java_extract_annotations() { - let source = r#" -import java.lang.Override; - -public class Foo { - @Override - public String toString() { - return "Foo"; - } - - @Deprecated - public void oldMethod() {} -} -"#; - let extractor = JavaExtractor; - let result = extractor.extract("Foo.java", source); - assert!(result.errors.is_empty(), "errors: {:?}", result.errors); - let annots: Vec<_> = result.nodes.iter() - .filter(|n| n.kind == NodeKind::AnnotationUsage) - .collect(); - assert!(annots.len() >= 2, "should extract annotation usages"); - let has_annotates = result.edges.iter().any(|e| e.kind == EdgeKind::Annotates) - || result.unresolved_refs.iter().any(|r| r.reference_kind == EdgeKind::Annotates); - assert!(has_annotates, "should have Annotates edges"); -} - -#[test] -fn test_java_extract_inner_class() { - let source = r#" -public class Outer { - public class Inner { - public void innerMethod() {} - } -} -"#; - let extractor = JavaExtractor; - let result = extractor.extract("Outer.java", source); - assert!(result.errors.is_empty(), "errors: {:?}", result.errors); - let inners: Vec<_> = result.nodes.iter().filter(|n| n.kind == NodeKind::InnerClass).collect(); - assert_eq!(inners.len(), 1); - assert_eq!(inners[0].name, "Inner"); -} - -#[test] -fn test_java_extract_static_init_block() { - let source = r#" -public class Registry { - private static Map cache; - static { - cache = new HashMap<>(); - } -} -"#; - let extractor = JavaExtractor; - let result = extractor.extract("Registry.java", source); - assert!(result.errors.is_empty(), "errors: {:?}", result.errors); - let init_blocks: Vec<_> = result.nodes.iter().filter(|n| n.kind == NodeKind::InitBlock).collect(); - assert_eq!(init_blocks.len(), 1); -} - -#[test] -fn test_java_extract_abstract_method() { - let source = r#" -public abstract class Shape { - public abstract double area(); - public void describe() { System.out.println("shape"); } -} -"#; - let extractor = JavaExtractor; - let result = extractor.extract("Shape.java", source); - assert!(result.errors.is_empty(), "errors: {:?}", result.errors); - let abstract_methods: Vec<_> = result.nodes.iter().filter(|n| n.kind == NodeKind::AbstractMethod).collect(); - assert_eq!(abstract_methods.len(), 1); - assert_eq!(abstract_methods[0].name, "area"); -} - -#[test] -fn test_java_extract_generics() { - let source = r#" -public class Box { - private T value; - public T getValue() { return value; } -} -"#; - let extractor = JavaExtractor; - let result = extractor.extract("Box.java", source); - assert!(result.errors.is_empty(), "errors: {:?}", result.errors); - let generics: Vec<_> = result.nodes.iter().filter(|n| n.kind == NodeKind::GenericParam).collect(); - assert!(generics.len() >= 1, "should extract generic type param T"); -} - -#[test] -fn test_java_extract_call_sites() { - let source = r#" -public class App { - public void run() { - System.out.println("hello"); - helper(); - new ArrayList<>(); - } - private void helper() {} -} -"#; - let extractor = JavaExtractor; - let result = extractor.extract("App.java", source); - assert!(result.errors.is_empty(), "errors: {:?}", result.errors); - let call_refs: Vec<_> = result.unresolved_refs.iter() - .filter(|r| r.reference_kind == EdgeKind::Calls) - .collect(); - assert!(!call_refs.is_empty(), "should have call refs"); -} - -#[test] -fn test_java_extract_annotation_type() { - let source = r#" -public @interface MyAnnotation { - String value(); -} -"#; - let extractor = JavaExtractor; - let result = extractor.extract("MyAnnotation.java", source); - assert!(result.errors.is_empty(), "errors: {:?}", result.errors); - let annots: Vec<_> = result.nodes.iter().filter(|n| n.kind == NodeKind::Annotation).collect(); - assert_eq!(annots.len(), 1); - assert_eq!(annots[0].name, "MyAnnotation"); -} - -#[test] -fn test_java_file_node_is_root() { - let source = "public class Main {}"; - let extractor = JavaExtractor; - let result = extractor.extract("src/Main.java", source); - let files: Vec<_> = result.nodes.iter().filter(|n| n.kind == NodeKind::File).collect(); - assert_eq!(files.len(), 1); - assert_eq!(files[0].name, "src/Main.java"); -} - -#[test] -fn test_java_contains_edges() { - let source = r#" -public class Foo { - private int x; - public void bar() {} -} -"#; - let extractor = JavaExtractor; - let result = extractor.extract("Foo.java", source); - let contains: Vec<_> = result.edges.iter().filter(|e| e.kind == EdgeKind::Contains).collect(); - // File contains: Class; Class contains: Field, Method - assert!(contains.len() >= 3, "should have Contains edges: {}", contains.len()); -} - -#[test] -fn test_java_qualified_names() { - let source = r#" -package com.example; - -public class App { - public void run() {} -} -"#; - let extractor = JavaExtractor; - let result = extractor.extract("src/App.java", source); - let methods: Vec<_> = result.nodes.iter().filter(|n| n.kind == NodeKind::Method).collect(); - assert_eq!(methods.len(), 1); - assert!(methods[0].qualified_name.contains("App")); - assert!(methods[0].qualified_name.contains("run")); -} -``` - -**Step 2: Run tests to verify they fail** - -Run: `cargo test --test java_extraction_test` -Expected: FAIL — Java extractor is a stub - -**Step 3: Implement the full Java extractor** - -Replace `src/extraction/java_extractor.rs` with the full implementation. Same `ExtractionState` pattern: - -- `parse_source()` uses `tree_sitter_java::LANGUAGE` -- `visit_node()` dispatches on tree-sitter Java node kinds: - - `package_declaration` → `Package` - - `class_declaration` → `Class` (or `InnerClass` when nested inside another class) - - `interface_declaration` → `Interface` - - `enum_declaration` → `Enum` + `EnumVariant` children - - `annotation_type_declaration` → `Annotation` - - `constructor_declaration` → `Constructor` - - `method_declaration` → `Method` or `AbstractMethod` (detect `abstract` modifier) - - `field_declaration` → `Field` (one per variable declarator) - - `import_declaration` → `Use` (detect `static` keyword) - - `static_initializer` → `InitBlock` - - `marker_annotation` / `annotation` → `AnnotationUsage` + `Annotates` edge -- Visibility: scan for `modifiers` child → `public`=`Pub`, `protected`=`PubCrate`, `private`=`Private`, none=`Private` -- Doc comments: `block_comment` starting with `/**` preceding declarations -- Signatures: text from declaration start to `{` -- Call sites: `method_invocation`, `object_creation_expression` -- `extends`/`implements` from `superclass`/`interfaces` fields → `Extends`/`Implements` edges -- Generics: `type_parameters` → `GenericParam` nodes - -**Step 4: Run tests to verify they pass** - -Run: `cargo test --test java_extraction_test` -Expected: PASS - -**Step 5: Run full test suite** - -Run: `cargo test` -Expected: PASS — no regressions - -**Step 6: Commit** - -```bash -git add src/extraction/java_extractor.rs tests/java_extraction_test.rs -git commit -m "feat: implement Java extractor with deep extraction support" -``` - ---- - -### Task 7: Update resolver for new callable kinds - -**Files:** -- Modify: `src/resolution/resolver.rs` -- Test: `tests/resolution_test.rs` - -**Step 1: Update the `find_best_match` scoring** - -In `src/resolution/resolver.rs`, in `find_best_match()`, the callable kind bonus check (line ~206) currently only checks `Function` and `Method`. Add the new callable kinds: - -Replace: -```rust -if uref.reference_kind == EdgeKind::Calls - && (node.kind == NodeKind::Function || node.kind == NodeKind::Method) -{ - score += 25; -} -``` - -With: -```rust -if uref.reference_kind == EdgeKind::Calls - && matches!( - node.kind, - NodeKind::Function - | NodeKind::Method - | NodeKind::StructMethod - | NodeKind::Constructor - | NodeKind::AbstractMethod - ) -{ - score += 25; -} -``` - -**Step 2: Run tests** - -Run: `cargo test` -Expected: PASS — all tests pass including resolution tests - -**Step 3: Commit** - -```bash -git add src/resolution/resolver.rs -git commit -m "feat: update resolver scoring for Go/Java callable kinds" -``` - ---- - -### Task 8: Run clippy and final verification - -**Step 1: Run clippy** - -Run: `cargo clippy --all` -Expected: No warnings (fix any that appear) - -**Step 2: Run fmt** - -Run: `cargo fmt --all` - -**Step 3: Run full test suite** - -Run: `cargo test` -Expected: All tests pass - -**Step 4: Commit any fixes** - -```bash -git add -A -git commit -m "chore: clippy and fmt fixes" -``` - ---- - -## Summary - -| Task | Description | Key Files | -|------|-------------|-----------| -| 1 | Add tree-sitter deps | `Cargo.toml` | -| 2 | Expand NodeKind/EdgeKind | `src/types.rs` | -| 3 | Create trait + registry + stubs | `src/extraction/mod.rs`, `*_extractor.rs` | -| 4 | Integrate registry into CodeGraph | `src/codegraph.rs`, `src/config.rs` | -| 5 | Implement Go extractor (full) | `src/extraction/go_extractor.rs` | -| 6 | Implement Java extractor (full) | `src/extraction/java_extractor.rs` | -| 7 | Update resolver scoring | `src/resolution/resolver.rs` | -| 8 | Clippy + fmt + final verification | All files | diff --git a/docs/plans/2026-02-26-multi-language-support-design.md b/docs/plans/2026-02-26-multi-language-support-design.md deleted file mode 100644 index 1374b0bea7..0000000000 --- a/docs/plans/2026-02-26-multi-language-support-design.md +++ /dev/null @@ -1,158 +0,0 @@ -# Multi-Language Support Design: Go & Java - -> **Archived record — not implementation authority.** This document preserves -> historical intent and evidence. Current requirements come only from the -> `docs/plans/tracedecay-v2/` hierarchy. Exact tests and counts, source-string -> checks, branch/commit/worktree choreography, snapshots, receipts, -> attestations, PR packets, and gate matrices below are not rebuild -> instructions; validate current parser, runtime, and product behavior directly. - -**Date:** 2026-02-26 -**Status:** Approved - -## Overview - -Add Go and Java language support to codegraph, which currently only supports Rust. This requires: -1. An abstraction layer (trait + registry) to make language support pluggable -2. Expanded NodeKind/EdgeKind enums for language-specific constructs -3. Two new extractors: Go and Java (deep extraction) -4. Config and integration changes - -## 1. Extractor Trait & Language Registry - -### Trait Definition (`src/extraction/mod.rs`) - -```rust -pub trait LanguageExtractor: Send + Sync { - fn extensions(&self) -> &[&str]; - fn language_name(&self) -> &str; - fn extract(&self, file_path: &str, source: &str) -> Result; -} -``` - -### Language Registry - -```rust -pub struct LanguageRegistry { - extractors: Vec>, -} - -impl LanguageRegistry { - pub fn new() -> Self { /* register all extractors */ } - pub fn extractor_for_file(&self, path: &str) -> Option<&dyn LanguageExtractor>; - pub fn supported_extensions(&self) -> Vec<&str>; -} -``` - -- `CodeGraph` owns a `LanguageRegistry` -- `scan_files()` uses `registry.supported_extensions()` for include patterns -- `index_all()`/`sync()` call `registry.extractor_for_file(path)` -- `RustExtractor` implements `LanguageExtractor` - -## 2. Expanded NodeKind Enum - -### New Java variants: -- `Class` — class declarations -- `Interface` — interface declarations -- `Constructor` — constructor methods -- `Annotation` — annotation types (`@interface`) -- `AnnotationUsage` — annotation applications (`@Override`) -- `Package` — package declarations -- `InnerClass` — nested/inner classes -- `InitBlock` — static/instance initializer blocks -- `AbstractMethod` — abstract method declarations - -### New Go variants: -- `InterfaceType` — Go interface type definitions -- `StructMethod` — methods with receivers -- `GoPackage` — Go package declaration -- `StructTag` — struct field tags - -### Shared: -- `GenericParam` — type parameters - -### New EdgeKind variants: -- `Extends` — Java class inheritance, Go interface embedding -- `Annotates` — annotation → target -- `Receives` — Go method receiver type link - -## 3. Go Extractor (`src/extraction/go_extractor.rs`) - -Uses `tree-sitter-go`. - -### Declarations: -- `package_clause` → `GoPackage` -- `type_declaration` → `struct_type` → `Struct` + `Field` (with `StructTag`) -- `type_declaration` → `interface_type` → `InterfaceType` + method specs -- `function_declaration` → `Function` -- `method_declaration` → `StructMethod` (with `Receives` edge to receiver type) -- `const_declaration` / `var_declaration` → `Const` / `Static` -- `type_alias` → `TypeAlias` -- `import_declaration` → `Use` nodes - -### Edges: -- `Contains` — package → types → methods/fields -- `Calls` — scan bodies for `call_expression`, `selector_expression` -- `Receives` — method → receiver type -- `Uses` — import references -- `Extends` — interface embedding - -### Deep features: -- Generic type params → `GenericParam` nodes -- Doc comments (`//` preceding declarations) -- Visibility: uppercase = `Pub`, lowercase = `Private` -- Signatures: full function signature text -- Init functions: `func init()` detected - -## 4. Java Extractor (`src/extraction/java_extractor.rs`) - -Uses `tree-sitter-java`. - -### Declarations: -- `package_declaration` → `Package` -- `class_declaration` → `Class` or `InnerClass` (when nested) -- `interface_declaration` → `Interface` -- `enum_declaration` → `Enum` + `EnumVariant` -- `annotation_type_declaration` → `Annotation` -- `constructor_declaration` → `Constructor` -- `method_declaration` → `Method` or `AbstractMethod` -- `field_declaration` → `Field` -- `import_declaration` → `Use` (static imports flagged) -- `static_initializer` / `instance_initializer` → `InitBlock` - -### Edges: -- `Contains` — package → class → method/field, class → inner class -- `Calls` — scan bodies for `method_invocation`, `object_creation_expression` -- `Implements` — from `implements` clause -- `Extends` — from `extends` clause -- `Annotates` — `AnnotationUsage` → annotated element -- `Uses` — import references - -### Deep features: -- `GenericParam` — type parameters on classes/methods -- Annotations: `marker_annotation`, `annotation` → `AnnotationUsage` -- Visibility: `public` → `Pub`, `protected` → `PubCrate`, `private` → `Private` -- Doc comments: Javadoc `/** */` -- Signatures: method/constructor signature text -- Modifiers: `static`, `final`, `abstract`, `synchronized` captured in signature - -## 5. Integration & Config Changes - -### Config (`config.rs`): -- Default `include`: `["**/*.rs", "**/*.go", "**/*.java"]` -- Default `exclude` adds: `["vendor/**", "bin/**", "build/**", "out/**", ".gradle/**"]` - -### `codegraph.rs`: -- `CodeGraph::new()` creates `LanguageRegistry` -- `scan_files()` uses registry extensions -- `index_all()`/`sync()` use `registry.extractor_for_file()` - -### `Cargo.toml`: -- Add `tree-sitter-go` and `tree-sitter-java` dependencies - -### Resolver: -- Mostly language-agnostic already -- Qualified name separator stays `::` across all languages - -### DB Schema: -- No changes needed — NodeKind/EdgeKind stored as strings diff --git a/docs/plans/2026-07-01-macos-launchd-daemon-support.md b/docs/plans/2026-07-01-macos-launchd-daemon-support.md deleted file mode 100644 index 37a13da82e..0000000000 --- a/docs/plans/2026-07-01-macos-launchd-daemon-support.md +++ /dev/null @@ -1,544 +0,0 @@ -# Final Plan: macOS launchd Support for the TraceDecay Daemon - -> **Archived record — not implementation authority.** This document preserves -> historical intent and evidence. Current requirements come only from the -> `docs/plans/tracedecay-v2/` hierarchy. Exact tests and counts, source-string -> checks, branch/commit/worktree choreography, snapshots, receipts, -> attestations, PR packets, and gate matrices below are not rebuild -> instructions; validate current parser, runtime, and product behavior directly. - -**Status:** Final implementation plan -**Target branch:** `main` -**Scope:** `src/daemon.rs`, focused tests, README / user guide / security docs -**Outcome:** macOS gets the same user-facing daemon service support Linux already -has: `tracedecay daemon install-service`, `uninstall-service`, `status`, and -post-update service refresh work for a per-user background daemon. - ---- - -## 1. Goal - -Make the existing Linux daemon-service workflow work on macOS with native -launchd: - -```bash -tracedecay daemon install-service -tracedecay daemon status -tracedecay daemon uninstall-service -tracedecay update -``` - -Today macOS fails because the service layer always goes through -`systemd_user_service_path()` in `src/daemon.rs`, which hard-errors outside -Linux: - -> daemon service install is currently supported on Linux systemd user services - -After this change, macOS users can install TraceDecay as a per-user LaunchAgent -that starts at GUI login, restarts on failure, serves the same Unix socket -daemon as `tracedecay daemon run`, and is refreshed by `tracedecay update`. - -**Linux parity means OS-managed daemon process parity.** The macOS service should -match what Linux systemd support provides today: - -- write the service definition; -- start and enable it when requested; -- preserve a previously installed custom socket path during refresh; -- stop/disable/remove it during uninstall; -- report service path, socket reachability, and useful log/service commands; -- refresh the installed service after an update. - -## 2. Non-goals - -- **Windows service support.** Windows remains on the existing non-Unix fallback. -- **Auto-installing the daemon from `install --agent X`.** Linux does not do this - either; users explicitly opt into the OS service with `daemon install-service`. -- **Persisted project scheduler registry.** The daemon scheduler is currently - seeded when project clients connect and send a `DaemonHandshake`. This plan - does not add a boot-time registry of projects to resume before any client has - connected. That would be beyond Linux parity and should be a separate design. -- **Changing storage roots.** macOS continues to use the current TraceDecay - `user_data_dir()` behavior (`~/.tracedecay` unless `TRACEDECAY_DATA_DIR` is - set). Do not silently move daemon sockets or logs to - `~/Library/Application Support`. - -## 3. Existing Code Shape - -The current service API is already narrow enough for a clean platform dispatch. - -| Function | Current behavior | -|---|---| -| `install_service(spec, start)` | writes systemd unit, optionally `daemon-reload` + `enable --now` | -| `refresh_service(spec)` | rewrites systemd unit, `daemon-reload`, `enable`, `restart` | -| `refresh_installed_service(spec)` | skips missing unit, preserves installed socket path, refreshes | -| `uninstall_service(stop)` | optionally `disable --now`, removes unit, `daemon-reload` | -| `installed_service_socket_path()` | reads installed unit and parses `--socket` | -| `service_status(socket_path)` | prints service path, socket state, log command | - -Every one of those paths currently depends on `systemd_user_service_path()`. -That is the right seam to replace with platform dispatch. - -The daemon engine itself is already usable on macOS: - -- `run_foreground_unix` binds a Unix socket and handles SIGTERM; -- `notify_hook_event` has a Unix implementation; -- the scheduler code is Unix-gated, not Linux-specific; -- client handshake/profile handling is independent of systemd. - -## 4. Architecture Decision - -Keep the public Rust API signature-compatible and add private platform service -helpers inside `src/daemon.rs`. - -```text -Public API - install_service - refresh_service - refresh_installed_service - uninstall_service - installed_service_socket_path - service_status - | - v -ServiceRunner::current() - | - +-- Linux -> systemd user service - +-- macOS -> launchd per-user LaunchAgent - +-- other -> existing unsupported-service error -``` - -Use an enum, not traits, because there are only two supported backends and the -implementation is private: - -```rust -enum ServiceRunner { - Systemd, - Launchd, -} -``` - -The pure rendering/parsing helpers must remain unit-testable on any platform. -The process-control helpers are platform-gated and tested with fake command -runners where possible. - -## 5. macOS launchd Behavior - -Use modern launchd domain commands. Do **not** use legacy `launchctl load` / -`unload` for the implementation because they hide many errors and are explicitly -documented as legacy on current macOS. - -### 5.1 LaunchAgent identity - -```rust -const LAUNCHD_LABEL: &str = "com.tracedecay.daemon"; -const LAUNCHD_PLIST_NAME: &str = "com.tracedecay.daemon.plist"; -``` - -Paths: - -| Item | macOS path | -|---|---| -| LaunchAgent plist | `~/Library/LaunchAgents/com.tracedecay.daemon.plist` | -| socket | `/daemon.sock` unless `--socket` overrides | -| stdout log | `/daemon.out.log` | -| stderr log | `/daemon.err.log` | - -The plist path follows macOS convention. The socket/log paths follow existing -TraceDecay storage behavior for parity with the current daemon code. - -### 5.2 launchctl domain helpers - -Add: - -```rust -#[cfg(target_os = "macos")] -fn launchd_domain() -> Result; // "gui/" - -#[cfg(target_os = "macos")] -fn launchd_service_target() -> Result; // "gui//com.tracedecay.daemon" - -#[cfg(target_os = "macos")] -fn run_launchctl(args: &[&str]) -> Result; -``` - -`run_launchctl` should capture stdout/stderr and include both in errors. Keep the -shape close to `run_systemctl`, but return output for status checks. - -Use `gui/` because this is a per-user LaunchAgent that should start at GUI -login. If a future headless/background-user mode is needed, that should be a -separate option. - -### 5.3 Service-control mapping - -| Operation | Linux systemd | macOS launchd | -|---|---|---| -| install with start | `daemon-reload`; `enable --now tracedecay.service` | write plist; `bootout gui//com.tracedecay.daemon` (tolerating not-loaded, for idempotent re-install); `enable ...`; `bootstrap gui/ `; `kickstart -k ...` | -| install with `--no-start` | write unit only | write plist; `disable gui//com.tracedecay.daemon` so launchd does not autostart the agent at the next login | -| refresh | write unit; `daemon-reload`; `enable`; `restart` | write plist; `bootout gui//com.tracedecay.daemon` (tolerating not-loaded); `enable ...`; `bootstrap gui/ `; `kickstart -k ...` | -| uninstall with stop | `disable --now`; remove unit; `daemon-reload` | `bootout gui//com.tracedecay.daemon` if loaded; `disable gui//com.tracedecay.daemon`; remove plist | -| uninstall with `--no-stop` | remove unit only | remove plist only | -| status | unit path + socket + journald hint | plist path + socket + `launchctl print` / log hints | - -Implementation details: - -- Treat "not bootstrapped/not found" during `bootout` as non-fatal for uninstall - and refresh, just like the Linux uninstall ignores failed `disable --now`. -- After install/refresh with start, verify either the socket becomes connectable - briefly or `launchctl print ` succeeds. This catches command - failures that otherwise appear only in logs. -- Do not call `bootstrap` or `kickstart` for `--no-start`; persist a `disable` - instead so the plist in `~/Library/LaunchAgents` stays inert at login. - -## 6. Plist Rendering - -Add: - -```rust -impl DaemonServiceSpec { - pub fn render_launchd_plist(&self) -> Result; -} -``` - -The plist: - -```xml - - - - - Label - com.tracedecay.daemon - - ProgramArguments - - {absolute_tracedecay_bin} - daemon - run - --socket - {socket_path} - - - EnvironmentVariables - - PATH - {daemon_service_path_env(bin)} - HOME - {home} - - - RunAtLoad - - - KeepAlive - - SuccessfulExit - - - - ThrottleInterval - 2 - - StandardOutPath - {user_data_dir}/daemon.out.log - - StandardErrorPath - {user_data_dir}/daemon.err.log - - -``` - -Renderer requirements: - -- XML-escape `&`, `<`, `>`, `"`, and `'`. -- Require an absolute binary path for launchd. `which_tracedecay()` should already - produce one in normal installs; error clearly if it does not. -- Include `TRACEDECAY_DATA_DIR` in `EnvironmentVariables` when it is set during - install. This preserves custom profile roots across launchd restarts. -- Create the log/socket data directory before bootstrapping, because launchd can - create log files but cannot create missing parent directories. -- Set LaunchAgent plist permissions explicitly after writing. Use at most `0644` - and avoid group/world-writable files. - -## 7. Plist Parsing - -Add: - -```rust -fn socket_path_from_launchd_plist(plist: &str) -> Option; -``` - -Minimum acceptable parser: - -1. find the `ProgramArguments` array; -2. collect `...` values in order; -3. XML-unescape those string values; -4. return the value after `--socket`, or the value from `--socket=...` if ever - emitted in the future. - -Do not return escaped XML text. Existing custom socket preservation depends on -this parser during `refresh_installed_service`. - -If adding a small plist parsing dependency is acceptable, prefer a real plist -parser. If not, keep the ad hoc parser tightly scoped and heavily tested. - -## 8. `src/daemon.rs` Changes - -### 8.1 Platform path helpers - -Replace internal calls to `systemd_user_service_path()` with: - -```rust -fn service_unit_path() -> Result; -``` - -Behavior: - -- Linux: existing `~/.config/systemd/user/tracedecay.service`; -- macOS: `~/Library/LaunchAgents/com.tracedecay.daemon.plist`; -- other: service install unsupported. - -Keep `systemd_user_service_path()` as a private Linux helper. - -### 8.2 Render/parse dispatch - -Add: - -```rust -impl DaemonServiceSpec { - fn render_unit(&self) -> Result; -} - -fn socket_path_from_unit_text(text: &str) -> Option; -``` - -Linux dispatches to existing systemd helpers. macOS dispatches to the new plist -helpers. - -### 8.3 ServiceRunner methods - -```rust -impl ServiceRunner { - fn current() -> Result; - fn install(&self, service_path: &Path, start: bool, socket_path: &Path) -> Result<()>; - fn refresh(&self, service_path: &Path, socket_path: &Path) -> Result<()>; - fn uninstall(&self, service_path: &Path, stop: bool) -> Result<()>; - fn log_hint(&self) -> String; - fn service_detail_hint(&self) -> Option; -} -``` - -Use `socket_path` only for optional post-start verification. Keep the public API -signatures unchanged. - -### 8.4 Public function rewiring - -Refactor: - -- `install_service` -- `refresh_service` -- `refresh_installed_service` -- `write_service_unit` -- `installed_service_socket_path` -- `service_socket_path_from_unit_file` -- `uninstall_service` -- `service_status` - -The public surface remains unchanged. Only internal platform dispatch changes. - -### 8.5 Status output - -Keep status stable and useful: - -```text -service: /Users/you/Library/LaunchAgents/com.tracedecay.daemon.plist -socket: /Users/you/.tracedecay/daemon.sock (connectable) -service-detail: launchctl print gui/501/com.tracedecay.daemon -logs: tail -f "/Users/you/.tracedecay/daemon.err.log" -``` - -For Linux, keep the existing journald hint. - -Do not make status depend on parsing unstable `launchctl print` output. It is -fine to include the command as a diagnostic hint. If the implementation probes -service load state, treat it as best-effort. - -## 9. Docs - -Update all docs that currently describe daemon support as Linux-only or absent: - -- `README.md` - - daemon debugging section: show Linux and macOS commands; - - CLI reference: remove "Linux systemd" qualifier from `daemon install-service`; - - mention macOS logs under `/daemon.err.log`. -- `docs/USER-GUIDE.md` - - add macOS daemon setup under the install or keeping-fresh flow; - - show install, status, uninstall. -- `SECURITY.md` - - replace the current "No background daemon" statement with an accurate - opt-in model: no daemon is installed by default, but users can explicitly - install a per-user systemd/launchd service that runs with standard user - privileges. - -## 10. Tests - -### 10.1 Ungated unit tests - -These run on every platform: - -- `render_launchd_plist_includes_label_program_arguments_socket_and_logs` -- `render_launchd_plist_escapes_xml_special_characters` -- `render_launchd_plist_includes_trace_decay_data_dir_when_set` -- `socket_path_from_launchd_plist_round_trips_rendered_socket` -- `socket_path_from_launchd_plist_unescapes_xml` -- `socket_path_from_launchd_plist_returns_none_for_malformed_input` -- `service_unit_path_unsupported_platform_error_mentions_service_install` - if practical to test via helper injection. - -### 10.2 Linux regression tests - -Existing Linux tests must keep passing: - -- `user_service_runs_daemon_with_socket_path` -- `refresh_service_rewrites_unit_and_restarts_daemon` -- `refresh_installed_service_skips_missing_unit` -- `refresh_installed_service_preserves_existing_socket_path` - -If the systemd renderer signature changes to return `Result`, update the -tests mechanically without changing expected Linux output. - -### 10.3 macOS command tests without real launchd - -Add tests around command planning/fake command runner, not real `launchctl`: - -- install with start plans `bootout` (tolerated), `enable`, `bootstrap`, - `kickstart`; -- install with `--no-start` writes the plist and disables the agent; -- refresh preserves existing socket path and plans `bootout` (tolerated), - `enable`, `bootstrap`, `kickstart`; -- uninstall with stop plans `bootout`, `disable`, remove plist; -- uninstall with `--no-stop` removes plist only. - -These should not require root, a GUI session, or a real LaunchAgent. - -### 10.4 Optional ignored macOS smoke test - -One ignored/manual test is acceptable, but it must avoid clobbering a user's real -daemon: - -- use a test-only label like `com.tracedecay.daemon.test.`; -- write to a temporary plist path; -- use a temporary `TRACEDECAY_DATA_DIR`; -- always attempt cleanup with `bootout` and file removal. - -Do not use `com.tracedecay.daemon` in ignored tests. - -## 11. Risks and Decisions - -| Risk | Decision | -|---|---| -| legacy `launchctl load/unload` masks failures | use `bootstrap/bootout/enable/kickstart` | -| plist parent/log parent missing | create LaunchAgents dir and data dir before bootstrap | -| plist rejected due to permissions | set plist permissions explicitly | -| custom `TRACEDECAY_DATA_DIR` lost under launchd | persist it into plist env when set | -| custom socket path lost on update | parse plist and preserve existing `--socket` during refresh | -| status overpromises service state | show socket state and diagnostic launchctl command; probe best-effort only | -| scheduler expectations after reboot | document Linux parity: daemon starts at login; project schedulers start after project handshake | -| Homebrew binary path changes | `tracedecay update` refreshes plist with current binary path | -| log growth | same operational class as journald, but file rotation is a follow-up | - -## 12. Implementation Order - -1. **Pure service dispatch refactor** - - add `ServiceRunner`; - - add `service_unit_path`; - - add render/parse dispatch helpers; - - keep Linux behavior identical; - - run existing daemon tests. - -2. **Add launchd plist renderer/parser** - - add XML escape/unescape; - - add data-dir/log path handling; - - add ungated parser/renderer tests. - -3. **Add macOS launchctl backend** - - implement domain/target helpers; - - implement `bootstrap`, `bootout`, `enable`, `disable`, `kickstart`; - - create required directories and set plist permissions; - - add fake-command tests. - -4. **Wire refresh/uninstall/status** - - preserve existing socket paths; - - add macOS log and `launchctl print` hints; - - keep public API and CLI unchanged. - -5. **Docs** - - README; - - user guide; - - SECURITY.md. - -6. **Manual macOS verification** - - run on a real macOS GUI session; - - verify install/start/status/refresh/uninstall; - - verify reboot/login start. - -## 13. Verification - -Automated: - -```bash -cargo nextest run -p tracedecay daemon -cargo test -p tracedecay daemon_install_service_command_parses_socket_and_no_start -``` - -Manual macOS: - -```bash -tracedecay daemon install-service -tracedecay daemon status -launchctl print "gui/$(id -u)/com.tracedecay.daemon" -tail -f ~/.tracedecay/daemon.err.log -tracedecay update -tracedecay daemon uninstall-service -``` - -Expected: - -- plist exists at `~/Library/LaunchAgents/com.tracedecay.daemon.plist`; -- socket reports connectable after install/start; -- `launchctl print gui/$(id -u)/com.tracedecay.daemon` succeeds while installed; -- killing the daemon process causes launchd to restart it; -- after reboot and GUI login, launchd starts the daemon; -- `tracedecay update` refreshes plist binary path and keeps the installed socket - path; -- uninstall removes the plist and the launchd job. - -Scheduler verification, matching current Linux behavior: - -1. enable scheduler config for a project; -2. connect a project client through the daemon once; -3. verify `event=scheduler_tick` and task logs in `daemon.err.log`; -4. restart the daemon and reconnect the project client; -5. verify scheduler starts again. - -Do not require scheduler ticks immediately after reboot before any project -client has connected; that is not Linux parity. - -## 14. Source References - -| Topic | File:line | -|---|---| -| Linux-only service path gate | `src/daemon.rs:1715 systemd_user_service_path` | -| systemd unit renderer | `src/daemon.rs:154 render_systemd_user_unit` | -| public install/refresh/uninstall/status | `src/daemon.rs:455 / :466 / :474 / :540 / :560` | -| socket-path parser | `src/daemon.rs:523 socket_path_from_service_unit` | -| systemctl runner | `src/daemon.rs:1731 run_systemctl` | -| PATH env helper | `src/daemon.rs:177 daemon_service_path_env` | -| default socket path | `src/daemon.rs:237 default_socket_path` | -| foreground Unix daemon | `src/daemon.rs:988 run_foreground_unix` | -| scheduler starts from project server | `src/daemon.rs:1097 project_server` | -| scheduler config gate | `src/daemon.rs:1421 automation_scheduler_configured` | -| daemon action dispatch | `src/main.rs:742 Commands::Daemon` | -| post-update daemon refresh | `src/main.rs:413 refresh_daemon_service` | -| daemon CLI enum | `src/cli.rs:446 DaemonAction` | -| current security doc conflict | `SECURITY.md:103 No background daemon` | diff --git a/docs/plans/2026-07-04-tool-cli-args-agent-ergonomics.md b/docs/plans/2026-07-04-tool-cli-args-agent-ergonomics.md deleted file mode 100644 index f4e3d54012..0000000000 --- a/docs/plans/2026-07-04-tool-cli-args-agent-ergonomics.md +++ /dev/null @@ -1,842 +0,0 @@ -# `tracedecay tool` CLI arguments, reimagined for the AI-agent consumer - -> **Archived record — not implementation authority.** This document preserves -> historical intent and evidence. Current requirements come only from the -> `docs/plans/tracedecay-v2/` hierarchy. Exact tests and counts, source-string -> checks, branch/commit/worktree choreography, snapshots, receipts, -> attestations, PR packets, and gate matrices below are not rebuild -> instructions; validate current parser, runtime, and product behavior directly. - -Date: 2026-07-04 -Branch: `codex/cli-args-stdin` (PR #286, "Support stdin for tool args") -Status: implemented on `codex/cli-args-stdin` — the branch now includes the -JSON-first CLI contract, validation gate, `--dry-run`, corrective errors, -help/skill/steering updates, per-key repairs, and hermetic eval coverage. - ---- - -## 1. Context and scope - -`tracedecay tool [args...]` invokes any of the ~100 MCP tools from the -shell. Its consumers, in rough order of real traffic: - -1. **AI coding agents** shelling out because MCP is unavailable: the server - errored/timed out/disconnected, the host never configured it, or the - context (subagent, hook, CI script) has shell access but no MCP client. - Every plugin skill, steering hook, and prompt rule routes agents here - (`src/hooks/steering.rs:137-143`, `plugin/skills/using-the-cli/SKILL.md`, - `plugin/rules/tracedecay.mdc:35`, `src/agents/mod.rs:816`, - `src/agents/hermes/templates/skill.md:15`). -2. **Generated machine glue** — the Hermes plugin's `tools.py` - (`src/agents/hermes/templates.rs:233-272`) dispatches every tool call - through this CLI. -3. **Humans** debugging, exploring, or scripting ad hoc. - -PR #286 added stdin support to the `--args` whole-payload escape hatch -(`--args -`, `--args @-`, bare file path, back-compat `@file`), plus per-key -`@path`/`@-` file/stdin values with memoized stdin, and a two-scenario -hermetic eval corpus. That work is committed on this branch -(`src/tool_command.rs:1-30` module doc; `f4af7ce4`, `846081c8`, `14ae43de`). - -This plan steps back and asks the prior question the PR did not: **what -should the agent-facing argument surface be at all?** It evaluates the -abstraction first (Section 2), documents the current mechanics precisely -(Section 3), ranks the observed agent-facing frictions with empirical -evidence from the dev binary (Section 4), proposes the design (Section 5), -specifies the discoverability changes so the taught model matches the parser -(Section 6), defines a detailed eval plan to prove the change with real -Sonnet and Codex sessions (Section 7), and phases the rollout (Section 8). - -Everything below cites the state of this branch at commit `14ae43de`. All -empirical outputs come from this worktree's debug binary run against an -isolated `TRACEDECAY_DATA_DIR` and a throwaway indexed project (Appendix A). - ---- - -## 2. The abstraction question: should agents get a `--key value` surface at all? - -Before polishing the argument parser, decide what the agent-facing contract -is. Four candidate models, evaluated against the code as it exists. - -### 2.0 The 30-second version of how it works today - -`tracedecay tool` is declared in clap as a name plus a raw trailing vector -(`src/cli.rs:101-114`, `trailing_var_arg = true, allow_hyphen_values = -true`) — clap does no parsing of tool args. A hand-rolled parser -(`src/tool_command.rs:351-483`) walks the tool's JSON Schema -(`src/mcp/tools/definitions.rs`) to convert `--key value` strings into a -`serde_json::Value` object, with per-type coercion (`coerce_value`, -`src/tool_command.rs:514-560`). The finished object is then handed — -verbatim — to the daemon over a Unix socket as a standard MCP -`tools/call` JSON-RPC request (`dispatch_daemon_tool` → -`call_default_tool` → `call_tool`, `src/daemon.rs:1177-1244`), falling -back to an in-process dispatch of the same registry when the daemon is -down (`src/tool_command.rs:212-267`). - -So the CLI is *already* a thin MCP client. The only thing the arg surface -does is **construct the `params.arguments` JSON object from argv**. That -framing is what makes the abstraction question sharp: the per-key grammar -is a lossy re-encoding of a JSON object into shell words, immediately -decoded back into JSON. - -### 2.1 Option 1 — status quo: `--key value` with schema coercion as the agent path - -The taught model everywhere today ("every tool is also a shell command: -`tracedecay tool --key value`"). For an agent this means learning a -second parameter language on top of the MCP schema it already knows: - -- booleans need explicit values (`--include-code true`; bare `--include-code` - eats the next token — Appendix A.3/A.4); -- `--key=value` is not accepted, unlike every clap subcommand in the same - binary (Appendix A.1/A.2); -- arrays are repetition or comma-splitting (`finalize_arrays`, - `src/tool_command.rs:587-617`) — which **destroys** inline JSON for - array-of-array/object params (Appendix A.7); -- nested objects and enum constraints are not expressible/validated at all - (`coerce_value` falls through to string, enums pass unchecked — - Appendix A.8); -- unknown keys are silently forwarded and silently ignored by handlers - (Appendix A.6) — a typo produces wrong behavior with no error; -- any value containing quotes/newlines is a shell-quoting minefield in the - one-shot command an agent must emit. - -Fixing all of that is possible (Section 5.4 keeps a subset), but the end -state is still: an agent that knows `{"path": ..., "replacements": -[[...]]}` must transliterate it into a different grammar, correctly, in one -shot, under shell quoting. Every rule added to make that grammar richer is -one more rule the agent must have internalized *before* the failure moment -in which it reaches for the CLI. **Rejected as the agent-primary path.** - -### 2.2 Option 2 — JSON-first agent path: `--key value` is human sugar; agents pass the MCP arguments object - -Treat the argument surface as two contracts: - -- **Agent contract**: `tracedecay tool --args ` where the - payload **is exactly the MCP `arguments` object** the agent would have - sent over MCP — inline for short quote-free payloads, stdin (`--args -`, - typically a quoted heredoc) or a file for everything else. Nothing new to - learn beyond one sentence, because the agent already knows every tool's - schema; MCP knowledge transfers byte-for-byte. -- **Human contract**: `--key value` (plus positional-required-string - binding) stays for interactive use, where flags genuinely beat typing - JSON. - -Evidence this is the natural machine interface, from this repo itself: - -- The Hermes plugin's generated `tools.py` — the one *programmatic* consumer - of `tracedecay tool` in the codebase — never uses `--key value`. It - serializes the arguments dict and passes `--json --args `, - spilling to `--args @tempfile` above ~100 KB to dodge Linux's 128 KiB - per-argv-string cap (`src/agents/hermes/templates.rs:89-98,260-272`; - pinned by `tests/agent_suite/agent_test.rs:1117-1120`). -- `memory curate --llm-ops ` established the same whole-payload - convention for LLM-constructed JSON (`src/commands.rs:46-66`). -- PR #286 already built the payload plumbing: inline / `-` / bare path / - `@file` / `@-`, memoized stdin (`resolve_args_payload`, - `src/tool_command.rs:491-509`). - -Shell quoting — the classic objection to JSON-on-the-CLI — is solved by the -stdin form with a quoted heredoc, a pattern agents already use daily for -`git commit -m "$(cat <<'EOF' ...)"`: - -```bash -tracedecay tool multi_str_replace --args - <<'JSON' -{"path":"src/lib.rs","replacements":[["old, with 'quotes'","new $body"]]} -JSON -``` - -No escaping, no argv cap, arbitrary newlines, byte-exact MCP parity. -**Recommended.** The rest of this plan is the working-out of this option. - -### 2.3 Option 3 — raw daemon/MCP passthrough: the CLI accepts a JSON-RPC frame - -The maximal-parity idea: `tracedecay tool` (or a new `tracedecay rpc`) -reads a whole `tools/call` JSON-RPC request from stdin, forwards it to the -daemon socket, prints the response. Research findings: - -- The daemon already speaks exactly this protocol: a handshake line then - line-delimited JSON-RPC, with `tools/call` handled server-side - (`src/daemon.rs:2145-2184`); `tracedecay serve` proxies full MCP stdio to - it (`proxy_stdio_to_default_daemon`, `src/daemon.rs:1168-1174`; - `src/cli.rs:289-291`). -- But the CLI *already is* this passthrough for the part that carries - information: `call_tool` builds the envelope — `jsonrpc`, `id`, `method: - "tools/call"`, `params.name` — from values the CLI already has - (`src/daemon.rs:1183-1193`). The only agent-authored content in the frame - is `params.arguments`, which is precisely what `--args` transports. -- A raw-frame mode would *add* agent-visible failure modes (wrong method - string, malformed envelope, id bookkeeping) while *removing* things the - CLI quietly does correctly: the handshake carries client identity, - profile root, global-DB path, and project routing - (`DaemonHandshake::for_current_client`, `src/daemon.rs:195-231`; - project resolution in `DaemonToolDispatch::project_scoped`, - `src/tool_command.rs:181-194`), and `dispatch_daemon_tool` falls back to - in-process execution when the daemon socket is absent - (`src/tool_command.rs:269-296`) — on Windows there is no socket at all - (`src/daemon.rs` `#[cfg(not(unix))]` stubs). An agent hand-writing frames - would have to replicate an undocumented, version-coupled handshake or - lose all of that. - -Option 3 therefore collapses into Option 2: **passthrough of the -`arguments` object, not the envelope**. The envelope is boilerplate the CLI -should keep owning. Rejected as a separate surface; one nicety survives as -`--dry-run` (Section 5.3), which prints the fully-resolved arguments object -(and optionally the frame) without dispatching — giving scripts and evals -the "show me the request" affordance without a second protocol. - -### 2.4 Option 4 — does the agent need this surface at all? - -When does an agent actually land here? - -- **MCP transport failure mid-session** — the steering text injected into - every Codex session names this exact moment and prescribes the CLI - (`src/hooks/steering.rs:137-143`); the `using-the-cli` skill is the - Claude-side equivalent (its trigger description names failures, - timeouts, disconnected/unconfigured servers). -- **Contexts that never had MCP** — subagents and hooks with shell but no - MCP client (`plugin/skills/using-the-cli/SKILL.md:50-52`), CI scripts, - and hosts where tracedecay's MCP server isn't registered. These are - by-design consumers, not failure recovery. -- **Permission-denied MCP** in restricted harness configurations where - `Bash` is allowed but the MCP tool isn't. - -Alternative remedies considered: - -- *Re-establish MCP* (restart server, ToolSearch reload): host-level and - frequently outside the agent's control; the skill already covers the - "deferred but healthy" case separately (`SKILL.md:63-69`). Not a - substitute for the genuinely-no-MCP contexts. -- *`tracedecay serve` as ad hoc MCP*: an agent could spawn it and speak MCP - over stdio, but a one-shot shell tool cannot reasonably hold a - bidirectional initialize/call/shutdown conversation. Impractical. -- *Only expose plumbing (`--print-request`) and let the agent pipe frames - itself*: strictly worse than Option 3 for the same reasons. - -Conclusion: the fallback surface must exist, agents are its primary -consumers, and the **only part of it agents should have to think about is -the arguments object they already know**. The per-key grammar continues to -exist for humans — but every agent-facing document, hook, and error message -should converge on the JSON path. - -### 2.5 Tensions resolved explicitly - -| Tension | Resolution | -|---|---| -| Human vs agent ergonomics | Two documented contracts on one command: `--key value` for humans, `--args` JSON for agents/scripts. Neither is deprecated; they are *taught to different audiences*. | -| MCP parity vs CLI convention | Parity wins for agents: the payload is the MCP `arguments` object, so nothing new to learn. The CLI-only conventions (`--json`, `--project`, `--dry-run`) are transport concerns, not argument concerns. | -| Forgiving vs unambiguous parsing | Per-key parsing stays forgiving for humans but gains a single validation gate (Section 5.2) that turns every silent divergence into a corrective error. JSON path is unambiguous by construction. | -| Back-compat vs one-clean-rule | All currently-working invocations keep working (Section 8). The one intentional break: unknown keys and invalid enum values stop being silently ignored — that silence is the bug. | -| Altitude: schema coercion in the CLI duplicates the MCP layer | Correct diagnosis: `coerce_value` re-derives types the schema already declares, and handlers re-validate (or fail to). The fix is not more coercion but one schema-driven validation pass over the *final JSON object*, shared by both paths, next to the schemas it validates. | - ---- - -## 3. Current state, precisely - -### 3.1 Parse pipeline - -`run()` (`src/tool_command.rs:81-132`): resolve tool name via -`canonical_tool_name` (strip `tracedecay_`, dash→underscore, alias -`query`→`search`; `src/tool_command.rs:49,148-156`) → `parse_invocation` → -help or dispatch. - -`parse_invocation_with_stdin` (`src/tool_command.rs:351-483`), one pass over -the raw arg vector: - -- Reserved flags: `-h/--help` short-circuits; `--json` sets raw output; - `--project` takes a value (`:387-395`). -- `--args ` (`:396-412`): `resolve_args_payload` - (`:491-509`) resolves inline JSON (leading `{`/`[`) verbatim, `-` → - memoized stdin (`:330-349`), `@file`/`@-` via `resolve_at_file`, anything - else as a bare file path. Must parse to a JSON **object**; mutually - exclusive with any other tool flag or positional (`:426-435`). -- Any other `--flag` (`:414-421`): key = kebab→snake, next token is the - value (`take_value`, `:620-624`), `@`-prefixed values are read from - file/stdin (`resolve_at_file`, `:630-642`), then `coerce_value` - (`:514-560`) coerces by schema type: string pass-through; boolean accepts - `true/1/yes/on`/`false/0/no/off` else errors; integer/number parse with - whole-number-stays-integer care; **`array` → returns the raw string**; - **anything else (incl. `object`) → returns the raw string**. Repeated - flags accumulate into an array (`merge_value`, `:569-581`). -- Non-flag tokens are positionals, bound in-order to *required* properties - not already set (`:439-466`); leftovers error. -- Missing required params error (`:468-478`). -- `finalize_arrays` (`:480,587-617`): for every schema property of type - `array` whose collected value is a single string, **split on commas** if - the string contains any, else wrap as a one-element array. No JSON - detection, no `items`-type awareness. - -Notable absences: no `--key=value` handling (the whole token becomes an -unknown key), no unknown-key rejection (missing `prop_schema` just means -"coerce as string and forward"), no enum validation anywhere, no -object-typed value parsing. - -### 3.2 Dispatch - -The parsed object goes to the daemon as MCP `tools/call` -(`src/daemon.rs:1177-1244`), or in-process through the same registry when -the socket is unavailable (`src/tool_command.rs:227-296`). Neither the -daemon (`src/daemon.rs:2181-2184` checks only that `params.name` exists) -nor the handlers validate arguments against the schema; handlers `.get()` -fields and default what's missing. Output: joined `content[*].text` blocks, -or raw JSON with `--json` (`:298-326`). - -### 3.3 Schema surface the parser must cover - -From `src/mcp/tools/definitions.rs` (100 tools listed by the binary): - -- ~23 array-typed params; most are `array` (comma-split works), - but at least four are arrays of arrays/objects: `multi_str_replace. - replacements` (`:1726-1748`, `[[old,new],…]`), and message/query arrays - on `lcm_expand_query` (`:2955`), `lcm_preflight` (`:3009`), - `lcm_compress` (`:3100`). -- 30 `enum` declarations (e.g. `gini.metric` - `["complexity","lines","fan_in","fan_out","members"]`, `:1792-1811`). -- Nested-object params: `project_selector` on `search`/`context`/etc. - (`project_selector_object`, `:946-965`). -- Long multi-line strings: `str_replace.old_str/new_str`, - `replace_symbol.new_source`, `insert_at.content`, `ast_grep_rewrite. - pattern/rewrite` (`:3231-3259`), `diagnose.cargo_output`, - `fact_store` text. - -### 3.4 What the agent is taught, and where it diverges from the parser - -| Surface | What it says | Divergence | -|---|---|---| -| Codex steering, injected every session (`src/hooks/steering.rs:137-143`) | "every tool is also a shell command: `tracedecay tool --key value`" | Teaches the grammar with the most traps; never mentions `--args`/stdin. | -| `using-the-cli` skill (`plugin/skills/using-the-cli/SKILL.md:18-23`) | `--key value` first; `--args`/`@`/stdin as a parenthetical | JSON path presented as an afterthought, not the machine path. | -| Arg catalog (`plugin/skills/using-the-cli/references/tool-arg-catalog.md:56`) | `multi_str_replace` required flags: `--path`, `--replacements` (`[[old,new],…]`) | **Actively teaches a shape the parser destroys** — comma-splitting mangles the JSON (Appendix A.7). Also stale (`body` documented as `--node-id (or --symbol)`; schema is `symbol` + `limit`, `definitions.rs:3290-3312`). | -| Per-tool `--help` (`render_tool_cli_help`, `src/mcp/tools/mod.rs:85-160`) | `--replacements array required "Array of [old_str, new_str] pairs"` | Invites inline per-key JSON that will be mangled; **does not print enum values** (Appendix A.8 help output); does not say array/object params need `--args`. | -| `tracedecay tool --help` trailer (`src/cli/help.rs:86-106`) | Correctly says `--args @file.json … required for array/object parameters` | The one place that states the rule — but it's on the *subcommand* help, which the discovery flow (list → per-tool `--help`) skips right past. | -| Bare list footer (`src/tool_command.rs:693-697`) vs per-tool footer (`mod.rs:150-157`) vs subcommand trailer | Three different reserved-flag/stdin footnotes | Wording drift; per-tool footer still leads with the `@` sigil model, list footer with the new whole-payload model. | -| ~10 other skills + Cursor rule + Hermes template + install prompt rules (`plugin/skills/*/SKILL.md`, `plugin/rules/tracedecay.mdc:35`, `src/agents/hermes/templates/skill.md:15`, `src/agents/mod.rs:816`) | All repeat `--key value` verbatim | The grammar is ossified in a dozen prose surfaces — and beyond the repo, into users' own CLAUDE.md files. Whatever we teach next should be *stable*, which favors the schema-parity JSON contract over flag ergonomics. | - -The net mental model handed to an agent — "alternating `--key value` -flags, kebab-case" — is **correct for scalar-only tools and wrong for -exactly the tools whose payloads are hardest to construct**, with the -authoritative reference (the catalog) actively wrong for -`multi_str_replace`. - ---- - -## 4. Usability analysis for an AI-agent consumer - -An LLM invoking a CLI constructs the entire command in one shot from -steering + `--help` + prior knowledge; it cannot tab-complete or -experiment cheaply; it generalizes one convention across all 100 tools; and -its recovery loop is exactly as good as the error text. Ranked friction -points, each grounded in an empirical run (Appendix A) or code: - -**F1 — Silent wrong behavior (worst class: no error to learn from).** -- Typo'd/unknown optional flag: `tool search --query gamma --limt 2` runs - successfully with the default limit; `--limt` is forwarded and ignored - (A.6; `src/tool_command.rs:414-421` — no schema check). -- Invalid enum: `tool gini --metric bogus` returns a computed result - labelled `metric: bogus` (A.8) — 30 enum params, zero validation. -- Object-typed param per-key: `--project-selector '{"project_id":"x"}'` - arrives as a *string*; handlers ignore it (`coerce_value` fall-through, - `:558`). - -**F2 — The taught shape for array-of-JSON params fails, with a -non-corrective error.** `tool multi_str_replace --path lib.rs ---replacements '[["alpha","gamma"]]'` — the exact catalog shape — dies with -`each replacement must be an array of exactly 2 strings` (A.7), a -*handler* error produced after comma-splitting mangled the JSON, hinting -nothing about `--args`. The one-shot construction fails and the retry has -no signpost. - -**F3 — GNU `=` form rejected, confusingly.** `--query=foo` → -``flag `--query=foo` requires a value`` (A.1); worse, `--query=foo --json` -consumes `--json` as the unknown key's value and then reports `missing -required parameter --query` (A.2). Clap accepts `=` everywhere else in the -binary, so an agent's prior from `tracedecay sync --path=X` actively -misleads it. - -**F4 — Boolean flags aren't presence flags.** `--include-code` alone at -end: ``requires a value``; mid-command it swallows the next token — -`--include-code --json` at least errors with `expected a boolean -(true/false), got '--json'` (A.3/A.4), which is corrective, but the -`requires a value` variant never states the fix (`pass true or false`). - -**F5 — Shell quoting of inline JSON.** Any `--args '{...}'` or per-key -value containing a single quote forces the `'"'"'` dance; multi-line -bodies (replacement text, ast-grep patterns, cargo output) are effectively -impossible inline. The escape (`@file`, `@-`, `--args -`) exists and is -good — but it's taught as a footnote (Section 3.4) rather than as *the* -agent form, and nothing in an error message ever points to it. - -**F6 — `--help` is insufficient for one-shot construction on the hard -tools.** No enum values (A.8 help), no `items` shape for arrays, no -example, no statement that array/object params require `--args`. For -`multi_str_replace`, help + catalog steer the agent straight into F2. - -**F7 — Single-dash and positional misbinding.** `-query foo` silently -binds `-query` to the required `query` and errors about leftover `foo` -(A.5) — the message points at the wrong token. `allow_hyphen_values` -(`src/cli.rs:112`) means clap can't catch it. - -**F8 — Minor consistency debt.** `--json` (raw envelope) vs per-tool -`--format json` (markdown/JSON payload switch) is a two-knob surprise; -three drifting footers (Section 3.4); usage errors print as -`Error: config error: …` (`TraceDecayError::Config` display), mislabeling -user-input problems as configuration problems. - -What already works well and must be preserved: required-param enforcement -with good message shape (``missing required parameter `--query` for tool -`search` ``), name normalization (prefix/dash/alias, -`:148-156`), repetition-for-arrays, `@file`/`@-` per-key, the whole-payload -`--args` family with stdin memoization, positional binding for quick human -queries, and grouped discovery via bare `tracedecay tool` (`:647-698`). - ---- - -## 5. Reimagined design - -### 5.1 The one generalizable rule - -> **The arguments of `tracedecay tool ` are the tool's MCP -> `arguments` object.** Pass it whole with `--args` (inline JSON, `-` for -> stdin — use a quoted heredoc, or a file path). Or, for quick scalar -> calls, spell top-level fields as `--key value` flags; values are -> interpreted by the tool's schema, and anything that isn't a scalar is -> JSON. - -Everything an agent needs beyond its existing MCP knowledge is that one -paragraph. How each argument kind maps: - -| Kind | Agent form (taught) | Human form (kept) | -|---|---|---| -| Whole payload | `--args -` + `<<'JSON'` heredoc; `--args '{…}'` inline when short and quote-free; `--args payload.json` | same | -| Scalar string / integer / number | inside `--args` | `--key value`, `--key=value` (new), positional for required strings | -| Boolean | inside `--args` | `--key true|false` (unchanged; corrective error gains the exact fix text) | -| Enum | inside `--args` | `--key value`, now validated with allowed values in the error | -| Array of strings | inside `--args` | repeat `--key a --key b`, or `--key a,b`, or `--key '["a","b"]'` (new: JSON accepted) | -| Array of arrays/objects | inside `--args` (the only sane form) | `--key ''` (new: parsed as JSON because schema type is array; comma-split only applies when the value doesn't parse as JSON) | -| Nested object | inside `--args` | `--key '{"…":…}'` (new: parsed as JSON because schema type is object) | -| Multi-line string | inside the heredoc payload | `--key @file` / `--key @-` (unchanged) | -| Payload > 128 KiB argv cap | `--args -` or `--args file` | same | - -### 5.2 One validation gate, shared by both paths - -New function in `src/tool_command.rs` (name suggestion: -`validate_tool_args(def: &ToolDefinition, args: &Map) -> -Result<()>`), called at the end of `parse_invocation_with_stdin` on the -final object — whether it came from `--args` or from per-key collection -(insert after `finalize_arrays`, `:480`, and on the `--args` branch, -`:426-435`). It walks `input_schema` once and enforces: - -1. **Unknown keys** → error listing the unknown key, a did-you-mean - suggestion (nearest by edit distance over property names), and the - valid keys. Catches F1-typos on *both* paths (an `--args` payload with - a misspelled key gets the same protection MCP hosts give). -2. **Enum membership** → error with the allowed values verbatim (F1-enums). -3. **Type agreement** on the final JSON (string vs array vs object vs - number/boolean) → corrective error naming the expected JSON type and - showing the `--args -` heredoc form for non-scalars (F2 backstop). -4. **Required presence** → keep the existing message (`:468-478`), now also - enforced for `--args` payloads (today a payload missing required keys - goes to the handler and fails handler-side or silently). - -Implementation notes: hand-roll the walker (~100 lines; the schemas use -only `type`/`enum`/`items`/`required`/`properties`) rather than adding a -`jsonschema` crate dependency; validate against the same -`get_tool_definitions()` the dispatch uses so conditionally-advertised -tools (`ast_grep_rewrite` retention, `definitions.rs:340`) stay -consistent; treat schemas without `properties` as opaque (skip validation) -so profile-scoped/dynamic tools cannot be bricked by a stale walker. - -This is the altitude fix: validation happens once, on the final JSON, -next to the schema — not scattered through string coercion, and not -duplicated per-handler. It also makes the CLI *stricter than the daemon*, -which is correct: the daemon trusts validated MCP clients -(`src/daemon.rs:2181-2184`); the CLI's caller is the thing that needs the -teaching. - -### 5.3 `--dry-run` (reserved flag) - -Parse + validate + print the final arguments object as pretty JSON to -stdout, exit 0 (or the corrective error, exit ≠ 0) — no daemon, no -handler, no side effects. One `if` in `run()` after `parse_invocation` -(`src/tool_command.rs:102-113`), one field in `ParsedInvocation` -(`:137-142`). Value: agents can self-check destructive edit-tool payloads -before applying; evals get a deterministic, side-effect-free probe of -"did the agent construct the right object" (Section 7.4); humans get -"show me what would be sent". This subsumes the `--print-request` idea -from Option 3 — if desired later, `--dry-run --json` can print the full -`tools/call` frame, but the arguments object is the useful part. - -### 5.4 Per-key repairs (human path, kept deliberately small) - -In `parse_invocation_with_stdin`: - -1. **`--key=value`** (F3): in the `flag if flag.starts_with("--")` arm - (`:414`), split on the first `=` before kebab→snake conversion; the - remainder is the value (no `take_value`). `--args=…`, `--project=…` - likewise in the reserved-flag matches. -2. **JSON-typed per-key values** (F2, F1-objects): in `coerce_value` - (`:514-560`), for schema type `array` or `object`, first attempt - `serde_json::from_str`; accept if the parsed type matches the schema - type; otherwise fall back to current behavior (string, later - comma-split for arrays) so `--keywords auth,login` keeps working. - `finalize_arrays` (`:587-617`) then skips values that are already - arrays (it does today, `:612`). -3. **Corrective boolean/missing-value errors** (F4): `take_value` error - becomes ``flag `--include-code` requires a value — pass `--include-code - true` or `--include-code false``` for booleans (thread the schema type - through, or special-case in the caller); generic flags get ``flag `--x` - requires a value — write `--x ` or `--x=` ``. -4. **Single-dash guard** (F7): a positional starting with a single `-` and - matching a known property name (after dash→underscore) errors with - ``did you mean `--query`?`` instead of binding as a positional. - -Explicitly *not* doing: presence-style booleans (`--include-code` alone). -With `allow_hyphen_values` and positionals in play, presence booleans are -ambiguous (`--before src/x.rs` on `insert_at` — flag-then-positional or -flag-with-value?); the corrective error is the safe fix. - -### 5.5 The corrective-error contract - -Every rejection must tell the agent exactly how to fix the call — the -error message is the CLI's tab-completion. The contract to implement and -test (messages abbreviated; all end by pointing at `--help` only when the -fix isn't already fully stated): - -| Rejection | Today (`src/tool_command.rs`) | Contract | -|---|---|---| -| Unknown tool (`:94-99`) | names the tool, points at list | + nearest-name suggestion (`did you mean 'dead_code'?`) | -| Unknown key (new) | *silent* (F1) | ``unknown parameter `--limt` for `search` — did you mean `--limit`? Valid: --query (required), --limit, --format, --project-id, --project-path, --project-selector`` | -| Invalid enum (new) | *silent* (F1) | ``--metric: `bogus` is not one of: complexity, lines, fan_in, fan_out, members`` | -| Array/object param given a non-JSON scalar (new) | comma-split mangle → handler error (F2) | ``--replacements expects a JSON array. Pass JSON: --replacements '[["old","new"]]' — or the whole payload via stdin: tracedecay tool multi_str_replace --args - <<'JSON' … JSON`` | -| `--key=value` (F3) | ``flag `--query=foo` requires a value`` | *accepted* (5.4.1); until then: ``write `--query foo` or `--query=foo` `` | -| Bare boolean (F4) | ``requires a value`` | ``--include-code requires true or false, e.g. `--include-code true` `` | -| Boolean swallowed a flag (`:522-531`) | states expected/got (good) | keep; append the `true/false` example | -| Missing flag value (`:620-624`) | ``flag `--x` requires a value`` | + `--x ` example (5.4.3) | -| Missing required (`:468-478`) | good | keep; append one-line usage: ``e.g. tracedecay tool search --query ""`` | -| `--args` invalid JSON (`:403-406`) | serde error, positioned | + ``if the payload contains quotes or newlines, pipe it: --args - <<'JSON' … JSON`` | -| `--args` non-object (`:407-411`) | ``must be a JSON object`` | + ``the same object you would pass as MCP arguments, e.g. {"query":"…"}`` | -| `--args` + other flags (`:426-435`) | states exclusivity | + ``either put everything in --args, or use only --key value flags`` | -| `--args` unreadable path (`:503-508`) | states the three forms (good) | keep | -| `@file` missing (`:636-638`) | ``failed to read @path: `` | + note the path is cwd-relative; suggest `--args -` for literals that begin with `@` | -| Unexpected positional (`:456-465`) | suggests flags + help (good) | + if it starts with `-`, the 5.4.4 did-you-mean | -| stdin read failure (`:342-345`) | io error | keep | - -Cosmetic but worthwhile: introduce a distinct display prefix for these -(`usage error:` rather than `config error:`) — either a new -`TraceDecayError` variant or message-prefix convention (F8). - -### 5.6 Alternative considered and rejected: full clap-native dynamic subcommands - -Generate a real clap `Command` per tool at startup (schema → typed -`Arg`s), getting `=` handling, unknown-flag errors, and `--help` for free. -Rejected: clap's dynamic builder would re-encode JSON Schema into clap's -type system (losing enums-with-values-in-errors unless hand-fed, -struggling with array-of-array items), boot-time cost on a "hot-ish path" -that deliberately skips even the reinstall scan (`src/main.rs:790-812`), -and it polishes exactly the surface Section 2 demoted — while `--args` -passthrough, positionals, and `@` values would still need the hand-rolled -layer. The 100-line validation walker buys the same agent-visible wins at -a fraction of the risk, and keeps one parser instead of two. - -### 5.7 Files and functions touched (implementer map) - -| Change | Where | -|---|---| -| `validate_tool_args` + call sites (both paths) | `src/tool_command.rs` (new fn; hook at `:426-435` and after `:480`) | -| `--dry-run` flag + `ParsedInvocation.dry_run` + early return | `src/tool_command.rs:81-142,387-395` | -| `--key=value` split; boolean/missing-value error text; single-dash guard | `src/tool_command.rs:387-424,514-560,620-624` | -| JSON-typed per-key values for array/object | `src/tool_command.rs:514-560` (`coerce_value`), `:587-617` (`finalize_arrays` no-op on real arrays — already true) | -| Error-contract wording | same file; unit tests in `src/tool_command/tests.rs` (one test per table row) | -| Help: enum values, items shape, generated `--args -` example for tools with non-scalar params, unified footer | `src/mcp/tools/mod.rs:85-160` (`render_tool_cli_help`); list footer `src/tool_command.rs:693-697`; trailer `src/cli/help.rs:76-106` | -| Skill/catalog/steering rewrite | Section 6 | -| Eval corpus + harness extensions | Section 7 | -| Contract tests pinning taught text ↔ parser | `tests/agent_suite/agent_test.rs` (exists for hermes `tools.py`, `:1117-1120`) + the shared/plugin skill contract tests PR #286 already exercises | - ---- - -## 6. Discoverability: make the taught model identical to the parser - -The principle: **an agent should see the same one-paragraph contract in -every place it can learn from — steering, skill, catalog, `--help`, and -error messages — and that contract should be Section 5.1 verbatim.** - -1. **Codex/Cursor steering** (`src/hooks/steering.rs:137-143`, mirrored - in `src/agents/mod.rs:816` and `src/agents/hermes/templates/skill.md:15`): - replace "`tracedecay tool --key value`" with: - > every tool is also a shell command: `tracedecay tool --args - > ''` — the same JSON arguments object as the MCP tool; pipe it - > via `--args -` (heredoc) when it has quotes/newlines. `tracedecay - > tool` lists tools; `tracedecay tool --help` shows parameters. - Keep it to ~2 lines; steering is paid for in every session. -2. **`using-the-cli` SKILL.md**: invert the Invocation section — JSON-first - with the heredoc example as the canonical form; `--key value` follows - as "quick scalar calls"; document `--dry-run` for pre-flighting edit - tools; keep the discovery flow and retrieval sections as-is. -3. **`tool-arg-catalog.md`**: fix the actively-wrong rows *immediately* - (in PR #286): `multi_str_replace` → ``--args -`` heredoc example; - `body` → `--symbol`. Restructure each row to show *the MCP argument - names* (which are the `--key` names modulo kebab-case) plus one - ready-to-copy example per hard-shape tool. Add a top "Invocation - grammar" that is Section 5.1's paragraph. -4. **Per-tool `--help`** (`render_tool_cli_help`): append enum values - (`one of: complexity | lines | …`) and array item shapes - (`array of [old, new] string pairs` derived from `items`); for any tool - with an array/object param, emit a generated example: - ``` - Example: - tracedecay tool multi_str_replace --args - <<'JSON' - {"path": "", "replacements": [["", ""]]} - JSON - ``` - (constructed mechanically from `properties` + `required` — placeholders - from the property names). Unify the three footers to the same two - lines (payload forms; `@`/`@-` per-key). -5. **Other skills / Cursor rule / prompt rules** (the dozen `--key value` - citations in Section 3.4): mechanical rewrite to "`tracedecay tool - ` (see `tracedecay:using-the-cli`)" — stop repeating the grammar - in surfaces that can drift; the skill is the single source. -6. **Contract tests**: extend the plugin/shared skill contract tests (run - in PR #286's test list) to assert the steering string, SKILL.md, and - catalog all contain the `--args -` form and do **not** teach per-key - for `replacements`, so the taught model cannot silently drift from the - parser again. - ---- - -## 7. Eval plan - -Goal: measure, with real Sonnet and Codex sessions in the hermetic -harness, whether an agent that must fall back to the CLI can construct -correct tool calls across the hard shapes — and whether it self-corrects -when it can't — before and after the changes. - -### 7.1 Harness (existing, verified) - -`evals/hermetic/run.sh` builds this worktree's binary, stages it at a -non-cargo path, installs the plugin into an isolated -`CLAUDE_CONFIG_DIR`/`CODEX_HOME`/`TRACEDECAY_DATA_DIR`, indexes a target -project, then runs each corpus line via `claude -p … --model sonnet` -(`run.sh:244-247`) or `codex exec --json` (`run.sh:252-258`) and scores -the isolated transcript with `score.py`. A scenario passes when every -`expected_tools` fragment appears among MCP tool names, every -`expected_cli` fragment appears among captured shell command strings, and -no `anti_tools` appear (`score.py:203-231`; `evals/hermetic/README.md:87-110`). -The existing two-scenario corpus (`corpora/tool-args-ergonomics.jsonl`) -already demonstrates the MCP-first vs CLI-fallback pattern; it stays -untouched as a continuity check. - -Key property exploited below: for Claude, the Bash `tool_use` input -contains the **full command text including heredoc bodies** -(`score.py:137-143`), so fragment matching sees inside `--args -` -payloads; Codex command strings are captured equivalently -(`score.py:147-167`). - -### 7.2 Harness extensions (small, additive) - -1. **`verify_cmd`** (per-scenario, optional): a shell command run by - `run.sh` in the scenario's `project_dir` *after* the agent session, - with the env's staged binary first on PATH; its exit status is passed - to `score.py` (new `--verify-status` arg) and folded into `pass` as - `verify_pass`. This is how edit-tool scenarios assert *effect* (file - actually contains the replacement) rather than command shape. -2. **Attempt counting**: `score.py` gains `tool_cmd_attempts` = number of - captured commands containing `tracedecay tool ` - (per-scenario `attempt_tool` field), and `self_corrected` = - `pass && tool_cmd_attempts > 1`. This turns "did the corrective error - teach the retry" into a metric without a smarter judge. -3. **Reset between reps**: `run.sh run` gains `--reps N` (re-run the - corpus N times, appending to `results.jsonl` with a `rep` field); - `verify_cmd` scenarios provide a `setup_cmd` to restore fixture state - (e.g. `git checkout -- lib.rs` in the fixture project) run before each - rep. - -Corpus schema additions documented in `evals/hermetic/README.md:87-110`. - -### 7.3 Fixture - -A tiny dedicated fixture project (3–4 files, committed under -`evals/hermetic/fixtures/tool-args/`, copied into the env and indexed by -`run.sh index`) rather than the tracedecay repo itself: edit scenarios -must mutate files deterministically, and `verify_cmd`/`setup_cmd` need -stable content. One file carries a function whose body contains a comma, -both quote characters, and a `$` — the quoting gauntlet. A second -registered project (one file) is indexed to exercise `project_selector`. -A >128 KiB `cargo-output.txt` fixture feeds the argv-cap scenario. - -### 7.4 Corpus: `evals/hermetic/corpora/tool-args-agent-path.jsonl` - -All prompts begin from the same fiction the existing corpus uses -("Assume the TraceDecay MCP server is unavailable; use the tracedecay CLI -fallback"), include `providers: ["sonnet","codex"]`, and anti-tools ban -raw DB access (`sqlite3`, `.tracedecay/`). Fragment expectations are -chosen to be *shape-agnostic* where multiple correct forms exist (a -fragment like `"fan_in"` matches per-key, inline JSON, and heredoc alike); -effects are verified where the tool has one. - -| id | Forces | Prompt sketch | Pass signal | -|---|---|---|---| -| `ap-array-of-pairs` | array of `[old,new]` pairs | apply two replacements in `lib.rs`, one new string containing `', '` and `$x` | `expected_cli: ["tracedecay tool multi_str_replace"]`; `verify_cmd`: grep the file for both new strings; `attempt_tool: multi_str_replace` | -| `ap-multiline-string` | multi-line string param | insert a 5-line doc comment (contains both quote types) above a named function via `insert_at` | `expected_cli: ["tracedecay tool insert_at"]`; `verify_cmd`: grep for a sentinel line | -| `ap-nested-object` | object param | search for symbol `zeta` in *the other registered project* using a project selector | `expected_cli: ["project_selector"]` or `["project-path"]` (either correct spelling); success text mentions the hit | -| `ap-enum-param` | enum | "compute inequality of fan in per file" (phrasing tempts `fanin`/`fan-in`) | `expected_cli: ["tool gini","fan_in"]` | -| `ap-whole-payload-stdin` | argv cap + stdin | diagnose the provided >128 KiB `cargo-output.txt` — "pipe the file, do not paste it inline" | `expected_cli: ["tool diagnose","--args"]`; passing `@`/`-`/file all count (fragment `--args`) | -| `ap-typo-recovery` | unknown-key correction | "search for `gamma` capping results with the `max_results` option" (real param: `limit`) | `expected_cli: ["--limit"]` or `["\"limit\""]`; **baseline expectation: fail silently** (agent uses `--max-results`, sees success, never corrects); after: corrective error → `self_corrected` | -| `ap-help-one-shot` | discoverability | construct a `fact_store` add for a given decision text using only `--help` (catalog withheld by prompt) | `expected_cli: ["tool fact_store"]`; `verify_cmd`: `tracedecay tool fact_store --args '{"action":"search","query":…}' --json | grep `; `tool_cmd_attempts ≤ 3` | -| `ap-dry-run-preflight` *(candidate arm only, Phase 1+)* | `--dry-run` | validate a `multi_str_replace` payload **without applying it**, then apply | `expected_cli: ["--dry-run"]`; `verify_cmd` checks final content | - -Eight scenarios × 2 agents. `ap-dry-run-preflight` is excluded from the -baseline arm (flag doesn't exist there); it establishes the Phase-1 -affordance is discoverable from help alone. - -### 7.5 Protocol: baseline vs after - -Two arms, identical corpus, identical fixture, same models: - -- **Arm A (baseline)**: binary + plugin from `master` (pre-#286 docs), via - `run.sh setup` on a master checkout. -- **Arm B (candidate)**: this branch (per phase: B0 = docs/steering only; - B1 = + validation/`--dry-run`; B2 = + help generation). - -Per arm: `setup` → `index` (fixture + second project) → `run --corpus -tool-args-agent-path.jsonl --reps 3` for `--agent claude --model sonnet` -and `--agent codex`. 8 scenarios × 2 agents × 3 reps × 2 arms ≈ 96 short -sessions — same manual, cost-gated posture as the existing harness (no -CI; `evals/memory/run_real_model.py` sets the precedent for explicit cost -consent). Record per-arm `summary.md` pass rates and mean -`tool_cmd_attempts`; store both as durable facts per the README's -post-merge protocol (`evals/hermetic/README.md:144-158`). - -**Success bar:** Arm B ≥ Arm A on every scenario (majority-of-3 reps); -the four trap scenarios (`array-of-pairs`, `multiline`, `enum`, -`typo-recovery`) move from expected-fail/flaky to ≥ 2/3 pass per agent; -mean attempts on hard shapes ≤ 2; zero silent-failure passes (a -`verify_cmd` failing while fragments pass counts as fail — that -combination *is* the silent-failure detector). - -### 7.6 Hypotheses (falsifiable, grounded in Section 4) - -| Scenario | Baseline prediction (why) | After prediction | -|---|---|---| -| `ap-array-of-pairs` | Mostly fail or multi-attempt: catalog/help steer to per-key `--replacements` (F2); handler error doesn't mention `--args`; some agents recover by inventing `--args`, then fight quoting (F5) | 1–2 attempts via heredoc; corrective error catches per-key strays | -| `ap-multiline-string` | Flaky: inline quoting breaks (F5); some agents write a temp file + `@file` (fine, passes) | heredoc first-shot | -| `ap-nested-object` | Fail: per-key object arrives as string, silently ignored (F1) → wrong-project results; fragments may pass while `verify` fails — recorded as silent failure | JSON path or corrective type error | -| `ap-enum-param` | Split: `fan_in` guessable from description text, but `fan-in`/`fanin` silently accepted (F1) → wrong output, no correction | corrective enum error → self-correct ≤ 2 attempts | -| `ap-whole-payload-stdin` | Mixed: argv-cap unknown to some agents; inline attempt may hit E2BIG with an opaque OS error | taught stdin form, first-shot | -| `ap-typo-recovery` | Fail silently (F1): `--max-results` ignored, agent reports success | unknown-key error names `--limit` → self-correct | -| `ap-help-one-shot` | Multi-attempt: help lacks enum/shape info (F6) | ≤ 3 attempts with generated example in help | - -If Arm B0 (docs only) already clears most of the bar, that is a -finding: the parser was adequate and the *teaching* was the bug — Phases -1–2 then stand on the remaining deltas (`typo-recovery` and -`enum` cannot pass B0; they need the validation gate). - ---- - -## 8. Rollout - -Phased, lowest-risk first; every phase independently shippable and -re-evaluated (Section 7.5 arms map to phases). - -**Phase 0 — reshape PR #286 (docs + evals, zero parser changes).** -The branch's parser work (payload conventions, stdin memoization, tests) -is correct and stays. The PR grows: the Section 6 rewrites of -`using-the-cli` SKILL.md, `tool-arg-catalog.md` (fixing the actively-wrong -`multi_str_replace`/`body` rows), steering strings -(`src/hooks/steering.rs`, `src/agents/mod.rs:816`, hermes template), and -`src/cli/help.rs` trailer; the new corpus + fixtures + `verify_cmd`/ -`attempts`/`--reps` harness extensions; contract-test updates pinning the -new taught text. What PR #286 *becomes*: "the CLI's agent contract is the -MCP arguments object over `--args`/stdin; docs, steering, and evals now -say so" — a docs-and-measurement PR on top of already-landed plumbing. -Back-compat: total (prose + additive harness changes only). - -**Phase 1 — validation gate + corrective errors + `--dry-run`.** -`validate_tool_args`, the Section 5.5 error contract, `--dry-run`; unit -tests per contract row; re-run Arm B1. Back-compat break, intentional and -narrow: unknown keys and invalid enums now error (previously silent). -Hermes `tools.py` is safe (it sends schema-exact dicts, and injected keys -like `messages`/`storage_scope`/`hermes_home` exist in the LCM/memory -schemas — verify against `PROFILE_SCOPED_LCM_TOOLS` handling in -`src/tool_command.rs:50-78` during implementation; if any injected key is -absent from a schema, add it to the schema rather than weakening the -gate). Call the break out in the changelog. - -**Phase 2 — help generation.** Enum values, item shapes, generated -`--args -` example, unified footers (`render_tool_cli_help`, -`src/mcp/tools/mod.rs:85-160`). Re-run Arm B2 (expect `ap-help-one-shot` -delta). No behavior change. - -**Phase 3 (optional, human polish) — per-key repairs.** `--key=value`, -JSON-typed per-key array/object values, single-dash guard (Section 5.4). -Lowest urgency: by now agents are on the JSON path; this serves humans. -Each is individually testable in `src/tool_command/tests.rs`. - -Explicit non-goals: no new top-level command, no raw JSON-RPC mode, no -clap dynamic subcommands, no removal of positionals/`--key value`/ -`@file`, no MCP protocol changes. - ---- - -## 9. Open questions and risks - -1. **Strictness vs unknown consumers.** Phase 1's unknown-key/enum - rejection could break third-party scripts relying on silently-ignored - params. Judged acceptable (that silence is a latent bug), but if - telemetry or issue reports say otherwise, the fallback is - downgrade-to-warning on stderr for one release. Decide at Phase 1 - review. -2. **Schemas that intentionally accept extra keys.** The validation gate - assumes `properties` is exhaustive. Audit for handlers reading - undeclared keys (the Hermes-injected `messages`/`hermes_home` pattern - is the known case); fix schemas, not the gate. Risk: one missed case - bricks a working integration — mitigated by running the full - `agent_suite` and hermes plugin tests in Phase 1. -3. **Steering token budget.** The 2-line steering rewrite is - cost-neutral, but adding a heredoc example to session-injected text is - not free at fleet scale. Current stance: example lives in `--help` and - the skill, one-sentence rule in steering. Revisit if `ap-*` scenarios - show agents not finding the heredoc form from steering alone. -4. **Eval judge fidelity.** Fragment matching can false-pass (command - emitted but failed) — `verify_cmd` closes this for edit tools, but - read-only scenarios (`ap-enum-param`, `ap-nested-object`) still lean on - fragments; a transcript-level LLM judge is out of scope (README's - stated philosophy: the harness guarantees isolation, not sophisticated - grading). Accepted; attempts + fragments + effects triangulate well - enough for a before/after signal at N=3. -5. **Windows.** No daemon socket; dispatch already falls back in-process - (`src/tool_command.rs:206-209,227-267`). stdin/heredoc guidance holds - in POSIX-ish shells agents use; PowerShell heredoc syntax differs — - the docs should show the `--args payload.json` form as the - portable alternative. Low priority; agent fleet is overwhelmingly - POSIX. -6. **`--json` vs `--format json`** (F8): consolidating the two knobs is - out of scope here (it spans handler output shaping); document the - distinction in the skill for now, and consider folding `--format` into - the help footer text. -7. **Error-variant taxonomy.** Whether to add a `TraceDecayError::Usage` - variant or keep `Config` with reworded messages — cosmetic; decide in - Phase 1 code review. -8. **External ossification.** Users' own CLAUDE.md files and memory facts - teach `--key value` (this repo's owner included). Nothing we ship - un-teaches those; the corrective errors are the safety net that - retrains stale habits in one round-trip. That is precisely why the - error contract, not the happy path, is the highest-leverage surface. - ---- - -## Appendix A — empirical evidence log - -Environment: this worktree's `target/debug/tracedecay` (0.0.29, commit -`14ae43de`), `TRACEDECAY_DATA_DIR` isolated to a scratch dir, throwaway -project `tiny-proj/lib.rs` (`fn alpha() {}\nfn beta() { alpha(); }`) -indexed via `tracedecay init` (1 file, 3 nodes). Boilerplate note lines -elided. - -| # | Command | Output (verbatim, trimmed) | -|---|---|---| -| A.1 | `tool search --query=foo` | ``Error: config error: flag `--query=foo` requires a value`` | -| A.2 | `tool search --query=foo --json` | ``Error: config error: missing required parameter `--query` for tool `search` `` (the unknown key `query=foo` consumed `--json` as its value, then both vanished) | -| A.3 | `tool context "how" --include-code` | ``Error: config error: flag `--include-code` requires a value`` | -| A.4 | `tool context "how" --include-code --json` | ``Error: config error: --include-code: expected a boolean (true/false), got `--json` `` | -| A.5 | `tool search -query foo` | ``Error: config error: unexpected positional argument(s): foo — use --key value flags or run `tracedecay tool search --help` `` (`-query` silently bound to `query`) | -| A.6 | `tool search --query gamma --limt 2 --json` | Succeeds; returns results with default limit — `--limt` silently forwarded and ignored | -| A.7 | `tool multi_str_replace --path lib.rs --replacements '[["alpha","gamma"]]'` (the catalog-taught shape) | ``Error: config error: each replacement must be an array of exactly 2 strings`` — comma-split mangled the JSON before the handler saw it; file untouched. Same edit via `--args '{"path":"lib.rs","replacements":[["alpha","gamma"]]}'` parses correctly (handler then rightly rejects the ambiguous 2-site match) | -| A.8 | `tool gini --metric bogus` | Succeeds: `gini: 0 … metric: bogus` — invalid enum accepted end-to-end. `tool gini --help` shows `--metric string optional Metric to measure inequality for (default: complexity)` — allowed values not shown | diff --git a/docs/plans/2026-08-08-v2-rc-recovery-design.md b/docs/plans/2026-08-08-v2-rc-recovery-design.md deleted file mode 100644 index 972fa98667..0000000000 --- a/docs/plans/2026-08-08-v2-rc-recovery-design.md +++ /dev/null @@ -1,194 +0,0 @@ -# TraceDecay V2 RC Recovery Design - -> **RECONCILED INTO NEXT.md (2026-08-13).** Its live remainder is carried -> by `docs/plans/tracedecay-v2/NEXT.md`, which is the delivery authority. -> This document is retained as the design record for the recovery, not as -> implementation authority. Its implementation-plan sibling, -> `docs/superpowers/plans/2026-08-08-v2-rc-recovery.md`, was stamped at the -> same time; this one was missed. - -**Status:** Approved on 2026-08-08 - -## Purpose - -Finish the latest Claude Code root session's interrupted V2 delivery in place, -preserve the useful implementation already present on -`codex/tracedecay-total-redesign-plan`, and produce a release-candidate branch -whose product surfaces are wired through real production journeys. - -The recovery is not a new roadmap. The sole roadmap precedence remains -`docs/plans/tracedecay-v2/00-plan-set-index.md`; this design reconciles the -unfinished checkout with that authority. - -## Recovered State - -The latest Claude root session was -`99dc84b5-f5ec-4ebb-8b96-318e1b20f871`. It referenced 140 ordinary subagents -and one 36-agent code-review workflow. Every referenced agent transcript, -metadata record, and task output was recovered. The root session stopped after -Claude hit its monthly quota and never produced its requested final checkpoint. - -The checkout contains two local commits beyond the remote integration floor and -an unstaged implementation spanning Work, workflow fan-out, worktrees, -observability, host integration, privacy, LSP, retained context, SDKs, and the -dashboard. A late queue run established that workspace compilation succeeds, -but the broad root library run still had 98 failures. Several green results in -older Claude logs were invalid because shell pipelines masked Cargo failures; -only direct exit status and non-vacuous test counts are accepted below. - -## Product Outcome - -The RC branch must provide the promised V2 product through the production -daemon and supported host journeys. Contracts, fakes, generated clients, and -dashboard components do not count as delivered unless a production caller can -exercise them and the relevant failure states remain typed. - -RC readiness means: - -- canonical Rust authorities own every wire shape; -- Work and workflow operations are admitted, dispatched, persisted, observed, - and retrievable through their promised surfaces; -- SDK, MCP, HTTP, CLI, host, and dashboard availability claims match mounted - production behavior; -- host installation and execution preserve operator state and isolate test - state; -- privacy, identity, staleness, denial, rollback, and replay boundaries fail - closed with falsifiable tests; -- generated artifacts are regenerated only from canonical authorities; -- focused and aggregate verification reports zero unclassified failures. - -## Recovery Strategy - -Preserve and finish Claude's dirty tree in place. Each dirty module is handled -in one of three ways: - -1. complete its real production journey and retain it; -2. fold it into the canonical authority that already owns the behavior; or -3. delete it when no V2 acceptance requirement or shipped compatibility - obligation justifies it. - -The recovery will not checkpoint the entire dirty tree as a mixed WIP commit, -reset it to the remote branch, or build parallel shadow authorities. - -## Delivery Slices - -### 1. Correctness and security foundation - -Repair the load-bearing defects before mounting additional callers: - -- make work-synthesis replay byte-stable by persisting the complete admitted - result atomically rather than recomputing source and draft state; -- bind a run's deadline and topology to durable run identity rather than - lexical attempt ordering or caller self-attestation; -- clear the environment of spawned provider processes and restore only the - admitted snapshot; -- return truthful fresh-store reset, Doctor observation, retained-source, - graph-generation, and ownership states instead of fabricated defaults; -- preserve typed absent, unsupported, denied, stale, and unavailable outcomes. - -### 2. Work and workflow runtime - -Complete the canonical Work journey: - -- mount all 26 Work operations through definitions, binding, dispatch, - application ownership, and production handlers; -- resolve and pin topology from registered control-plane authority before a - provider starts; -- provide real worktree inventory and cleanup adapters with partial, stale, - foreign, denial, reconcile, and rollback behavior; -- enforce fan-out fences and `max_parallel` through the real run-control path; -- persist checkpoints only when a production consumer retrieves or hands them - off; otherwise remove the unused contract; -- emit topology/run/attempt/handoff events and project them through a - generation-bound read model. - -### 3. Shared transports and hosts - -Expose only mounted capabilities: - -- finish Context Scout and the V2 SDK operations that have canonical schemas - and production executors; operations without a real journey remain typed - unavailable rather than fabricated; -- wire required remote enrollment, status, replay, backup, restore, and - failover operations through their promised CLI/MCP/SDK/dashboard surfaces; -- make LSP advisory registrations, snapshots, and denied outcomes derive from - real daemon authority; -- finish Claude's existing Kiro CLI lifecycle lane without redesigning the - integration: isolate the child environment and working directory, preserve - peer configuration, prove rollback, and reconcile its docs; do not add a - Kiro Power, OpenVSX extension, or new bundle architecture; -- parse and sanitize structured provider metadata before any GitHub, fact, or - session sink sees it. - -### 4. Contracts and dashboard - -After Rust request/result shapes settle: - -- export dashboard schemas and regenerate TypeScript contracts; -- regenerate Rust and TypeScript SDKs from the same registry; -- update fixtures to embed `WorkTopologyPolicyV1` rather than the obsolete - `topology_policy_digest`; -- mount the recovered Observatory views and Work topology accounting in real - navigation and data routes; -- bind joined dashboard data to one generation and label capped denominators as - partial rather than exact; -- add DOM journey tests for user-visible V2 behavior. Automated functional UI - coverage remains required; manual screen-reader polish is not an RC blocker. - -### 5. Cutover, cleanup, and release evidence - -Complete cutovers instead of keeping branch-local compatibility: - -- remove the root application compatibility shim after migrating its callers; -- split newly created or materially touched hand-written modules that exceed - the repository's 1,000-line ceiling; -- remove dead flags, aliases, test-only production ports, stale docs, and - unmounted claims; -- run formatting, compiler, lint, contract, dashboard, host, integration, - aggregate Rust, and current CI checks with direct exit-status evidence; -- record release evidence and the exact remaining operator actions. - -## Execution Model - -Implementation uses test-driven slices. Every behavioral change begins with a -focused failing test, records the expected failure, adds the minimum production -change, and records the passing result. Generated artifacts are the explicit -exception: their generator and drift check are the test. - -Each slice has one implementation owner at a time because all agents share this -dirty checkout. Terra workers own cohesive integration slices, Luna workers own -small mechanical tests, fixtures, and generated-artifact work, and Sol workers -own subtle concurrency, replay, identity, and security boundaries. Workers must -re-read files before editing, stage only their declared ownership, preserve peer -changes, and create conventional commits. A separate worker reviews every slice -for spec compliance and code quality before the next dependent slice begins. - -## Verification - -Focused development checks are followed by: - -- `cargo fmt --all -- --check`; -- `cargo check --workspace --all-targets --all-features`; -- repository clippy policy with warnings denied; -- non-vacuous focused Rust tests and then the full workspace suite; -- dashboard contract generation/check, typecheck, tests, and production build; -- SDK generation and conformance checks; -- supported-host bundle, install/update/uninstall, and isolation journeys; -- end-to-end Work, workflow, retained-context, remote, LSP, and dashboard - journeys; -- current GitHub CI for the final pushed commit. - -Failures are fixed at their root. Assertions are not weakened, timeouts are not -raised to hide races, and tests are not ignored or filtered into vacuous greens. - -## Human and Operator Gates - -The code branch can be RC-ready while clearly recording these external actions: - -- npm trusted-publisher/OIDC configuration, which the user will provide; -- designated live semantic evaluation/profile runs; -- machine-specific Doctor/Cursor/Kiro journeys where the real host is required; -- the planned large-store garbage-collection observation. - -No RC tag or package publication occurs until those required external gates are -classified and the user authorizes publication. diff --git a/docs/plans/tracedecay-v2/00-plan-set-index.md b/docs/plans/tracedecay-v2/00-plan-set-index.md index 013b8e835b..e55841b892 100644 --- a/docs/plans/tracedecay-v2/00-plan-set-index.md +++ b/docs/plans/tracedecay-v2/00-plan-set-index.md @@ -7,9 +7,8 @@ tests, and normal CI remain active delivery work. The repository is not green. This file is the sole authority for V2 precedence, rejected mechanisms, delivery order, and acceptance. Numbered plans own semantic product behavior, failure semantics, fresh-store cutover, and direct acceptance; they are not independent -queues and do not require one delivery branch per document. `NEXT.md` tracks -current outcomes and blockers only. Historical gap ledgers and contract-spine -artifacts are records, not parallel authorities. +queues and do not require one delivery branch per document. Historical gap +ledgers and contract-spine artifacts are records, not parallel authorities. The `TraceDecay V2` roadmap name is independent of contract/schema versioning. Only an actually independently released public wire/API protocol may retain an diff --git a/docs/plans/tracedecay-v2/24-canonical-task-plan-graph-and-multi-agent-executor.md b/docs/plans/tracedecay-v2/24-canonical-task-plan-graph-and-multi-agent-executor.md index 0de9710a17..f6369637a7 100644 --- a/docs/plans/tracedecay-v2/24-canonical-task-plan-graph-and-multi-agent-executor.md +++ b/docs/plans/tracedecay-v2/24-canonical-task-plan-graph-and-multi-agent-executor.md @@ -75,7 +75,7 @@ placement, workflow definitions, automation execution, expertise/calibration, fan-out/synthesis/recovery, and host/LSP handoff. Session-derived tasks, an independent Kanban database, or dashboard-owned task authority remain rejected. -Roadmap Markdown, `NEXT.md`, PR sequences, contributor checklists, and +Roadmap Markdown, PR sequences, contributor checklists, and completion ledgers are documentation and Git evidence only. The workflow runtime never parses, imports, schedules, or executes them. Product work enters through explicit application commands or an authorized product-data import. diff --git a/docs/plans/tracedecay-v2/39-embedded-grafeo-graph-database.md b/docs/plans/tracedecay-v2/39-embedded-grafeo-graph-database.md index 109cf0e672..825d3b6d4d 100644 --- a/docs/plans/tracedecay-v2/39-embedded-grafeo-graph-database.md +++ b/docs/plans/tracedecay-v2/39-embedded-grafeo-graph-database.md @@ -7,8 +7,7 @@ > the daemon consumes it through > `src/daemon/code_index_scheduler/graph_activation.rs`; and the SQLite graph > authority was deleted in `79683eb527`. Treat the unchecked boxes as -> historical planning, not open work. Residual graph work is tracked in -> `NEXT.md` (Grafeo memory-relations restart/isolation journey). +> historical planning, not open work. **Goal:** Replace custom adjacency structures and graph-shaped SQLite storage with one embedded Grafeo runtime boundary while retaining SQLite only for genuinely relational, transactional, and content-bearing records. @@ -883,7 +882,6 @@ git diff --check - Modify: `scripts/tool-sweep.sh` - Modify: `.github/workflows/ci.yml` - Modify: `docs/plans/tracedecay-v2/33-end-to-end-performance-optimization.md` -- Modify: `docs/plans/tracedecay-v2/NEXT.md` **Interfaces:** - Consumes: final graph-db-backed production journeys. diff --git a/docs/plans/tracedecay-v2/NEXT.md b/docs/plans/tracedecay-v2/NEXT.md deleted file mode 100644 index 400af95094..0000000000 --- a/docs/plans/tracedecay-v2/NEXT.md +++ /dev/null @@ -1,88 +0,0 @@ -# V2 current outcomes - -`00-plan-set-index.md` remains sole roadmap/acceptance authority. -This file records outcomes only. Last reconciled: 2026-09-17 (post-#707 merge to master). - -Branch: `master` (PR #707 merged 2026-09-17 as `e13a35319f1c5de99e310a32d180dab3f54a802a`; tip has moved further). -Workspace: 38 crates under `crates/` (virtual root; counted from workspace -`members` in the root `Cargo.toml`). - -## Outcomes since 2026-08-19 - -- Usecase code-index generation retention (journal + receipt store) lives in - `tracedecay-code-index-retention`, not `tracedecay-application/src/retention`. -- GitHub read path: fail-closed REST protocol envelope in - `crates/tracedecay-application/src/advisory/github_runtime/protocol.rs` - (`Retry-After` / `Link`). One static GraphQL query is unchanged. -- Canonical clock routing: usecases wall-clock reads go through - `tracedecay-contracts` `clock` (`now_micros` / `try_now_micros` in - `crates/tracedecay-contracts/src/clock.rs`). -- Hook-identity canonicalization: `envelope_identity_hash16` in - `crates/tracedecay-hooks/src/lib.rs` (`HookHostV1` aliases domain - `NativeHostIdentityV1`). -- `tracedecay-runtime-core` no longer depends on `tracedecay-lsp`. -- Root decomp: `tracedecay-code-index-runtime` owns the scheduler previously - at `src/daemon/code_index_scheduler/`. Also extracted: `tracedecay-mcp`, - `tracedecay-session-temporal-store`, `tracedecay-maintenance`, - `tracedecay-source-edit`, `tracedecay-host-admission`, - `tracedecay-daemon-protocol`, `tracedecay-daemon-control`, - `tracedecay-automation-runtime`. - `tracedecay-cli` absorbs the work, workflow, remote, and upgrade verbs. -- Daemon lifecycle: start a dead installed unit after update - (`crates/tracedecay-daemon-control/src/service.rs`); treat `WouldBlock` - connect as saturation; doctor names a stopped-and-disabled installed unit - (`crates/tracedecay/src/doctor.rs`). -- Domain move-ups: file-document / extraction / lineage records left domain - for code-index; root-only graph shapes moved to the root crate. - `review_labels` is deleted — no current vocabulary; Plan 26 archival - mentions only. -- Machine compile cache is kache, not sccache. - -## 2026-08-29 landing wave - -Already landed on the redesign tip (now merged to `master` via PR #707) when this file was -reconciled: - -- Scheduler cluster repair: early publish with serving-seat wait, lock-park - remount, graph-off memory-pressure rebuilds, bounded activation, ready - abstain, lock-free reconcile slot, publication identity, text-head reopen, - and label-move CAS. -- Streaming sealed seat: committed-WAL recovery streams instead of - materializing. -- Grafeo checkpoints are crash-atomic (out-of-place generation + authenticated - header flip). Catalog format-version guard is pinned; torn-vector checkpoint - recovery reopens serving search. -- `GrafeoDB::close` skips the checkpoint when the container is already current. -- Durable Ready receipt is reattached after remount. -- Status reads serve the cached background process sample. -- Transport phases are spanned (daemon wire, LSP outbound, automation/MCP). - -## 2026-08-30 landing wave - -- Retention convergence landed with a binary handoff; the store was growing - 64.9→83.3 GiB per 2 h on the old binary. -- Deferred-bind proxy (#752). -- `semantic activate` journey shipped with typed revision refusal (#753). -- Vector-commit fix (#754): peak RSS 3044→236 MiB, wall 184→24 s at 120k×768. -- Skew-crash poll-frame fix; the daemon DoS is closed. -- Sealed decode cut: 55.3→29.6 s, RSS 2678→1875 MiB. -- Publication gate split: sealed-store build no longer blocks retrieval; - verify-once markers fixed the double 84 s verify. -- Typed-park plus self-heal for owner-privacy roots. -- Storage-status blocking fixes: 431 ms warm. -- Session journal paging. -- Redundancy sanitizer coordinate fix. -- Fact-store category and telemetry fixes. -- ANN wiring landed, held on exact-flat pending the latency verdict. -- Seconds-as-micros sweep. -- Wave review verdict: sound; 92 local failures proven environmental. - -## Unverified on HEAD - -These were remaining work on earlier handoffs and were not re-proven here: - -- Physical-daemon memory/session/LCM restart across CLI, MCP, HTTP, and SDKs. -- Operator doctor plus a clean Cursor agents/in-composer install → upgrade. -- Incremental indexing matrix (save, rename, delete, ref switch, overflow, - cancel, restart). -- npm trusted-publisher OIDC (operator-owned). diff --git a/docs/plans/tracedecay-v2/README.md b/docs/plans/tracedecay-v2/README.md index 81e6296293..286588a3f5 100644 --- a/docs/plans/tracedecay-v2/README.md +++ b/docs/plans/tracedecay-v2/README.md @@ -2,13 +2,11 @@ Status: active product rewrite. Temporal capture is complete. Retrieval, production integration, dashboard journeys, stable active contracts, direct -tests, and normal CI remain active delivery work. [NEXT.md](NEXT.md) records -the current product outcomes and blockers. +tests, and normal CI remain active delivery work. [00-plan-set-index.md](00-plan-set-index.md) is the sole precedence, rejection, -delivery, and acceptance authority. [NEXT.md](NEXT.md) tracks current outcomes -and blockers only. These are contributor documents only and never product -runtime input. +delivery, and acceptance authority. These are contributor documents only and +never product runtime input. Numbered plans define component behavior and boundaries, not separate crate-first work queues. @@ -161,10 +159,9 @@ removes duplicate authorities and follows inventories, registries, matrices, and generated declarations remain non-product and never justify implementation or CI by themselves. -The current executable slice is always [NEXT.md](NEXT.md). This roadmap is -contributor documentation, never daemon input, workflow input, product state, -or a source of completion truth. Integration completes only after direct -product tests and normal cross-platform CI are stable. +This roadmap is contributor documentation, never daemon input, workflow input, +product state, or a source of completion truth. Integration completes only +after direct product tests and normal cross-platform CI are stable. ## Release diff --git a/docs/plans/tracedecay-v2/audits/ci-triage-plan-2026-08-14.md b/docs/plans/tracedecay-v2/audits/ci-triage-plan-2026-08-14.md deleted file mode 100644 index 9edb15c18c..0000000000 --- a/docs/plans/tracedecay-v2/audits/ci-triage-plan-2026-08-14.md +++ /dev/null @@ -1,874 +0,0 @@ -# CI failure classification — run 31788759294 - -Scope: `codex/tracedecay-total-redesign-plan`, failed head `c962cd627`, local head -`2ff144f83`. Analysis is read-only. No Cargo command or test was run. - -## Executive result - -The supplied count of 484 does not match the log. - -- 490 unique names occur in literal `test ... FAILED` lines. -- Nextest adds 11 unique failures with no matching libtest failure line: - one `ABRT` (stack overflow) and ten `TMT` identities. -- The complete extracted inventory is therefore **501 distinct identities**. -- 24 of the 490 libtest identities passed on retry in every job where they - failed; 477 identities remained terminally failed/aborted/timed out in at - least one job. -- Literal libtest identities by job: Dashboard 17, macOS 423, Linux 390. - macOS/Linux overlap is 340. Windows ran zero tests: its build shard failed - first, and the Test Windows aggregator reported `skipped`. - -The primary partition below is exhaustive and sums to 501. Some diagnostic -families overlap the primary partition (for example a test can log both a busy -WAL checkpoint and an application-surface timeout); those correlations are -listed separately and must not be double-counted as independent failures. - -## Method - -1. Removed the log's literal `^[[...m` ANSI encoding. -2. Extracted every `test ... FAILED` identity. -3. Parsed nextest `FAIL`, `ABRT`, and `TMT` status lines and added identities - not emitted by libtest. -4. For each identity, inspected its retry block and captured the panic/error - text. Appendix A contains one compact evidence line for every identity. -5. Compared the failed head and local head, inspected the named reform - commits, and checked current production/test symbols. - -## Primary class table - -| Class | Count | Verdict | Single-concern task | -|---|---:|---|---| -| C01 retry-only failures | 24 | ENV/INFRA | Preserve as flake evidence; reproduce under equivalent load before changing code. | -| C02 Kiro live-handler fixture | 1 | STALE-TEST | Make the fixture provide the restored live-handler daemon boundary or assert the intentional fail-open diagnostic. | -| C03 semantic byte pins | 2 | STALE-TEST | Repin both workload hashes to the one authoritative 2170-chunk corpus. | -| C04 fact-store first touch | 1 | STALE-TEST | Replace removed `fact_store` dispatcher use with the canonical exact tool and daemon-owned profile setup. | -| C05 read-only ResetRequired authority | 1 | STALE-TEST | Expose the actual error, then assert the settled typed reset contract without pinning a superseded authority string. | -| C06 skill/tool discovery coverage | 1 | PRODUCT-BUG | Teach or explicitly internalize all 79 uncovered public tools; replace source-string coverage with behavioral discovery evidence. | -| C07a transcript concurrent CAS | 1 | PRODUCT-BUG | Restore one-winner compare-and-swap for concurrent full transcript batches. | -| C07b transcript summary projection fixtures | 2 | STALE-TEST | Publish summaries through the current verified lineage path or update counts to the intentional non-projection contract. | -| C08 worktree guard authority lifetime | 2 | STALE-TEST | Explicitly close or reuse the first runtime authority before the registry read; retain canonical-root assertions. | -| C09 LCM foreign-session canary | 1 | STALE-TEST | Use a non-secret canary; `sk-proj-...` is correctly rejected by privacy admission before lineage is exercised. | -| C10 update second-writer roster | 1 | STALE-TEST | Align the Cline expectation with the current stock-host canonical component set. | -| C11 socket Git stack overflow | 1 | PRODUCT-BUG | Isolate the recursive preview/apply route causing deterministic SIGABRT. | -| C12 session-registry convergence fixture | 4 | LIKELY-FIXED-BY `42bbaeb6a` + `f88610f33` | Confirm the four directly edited convergence tests under construction-time daemon scope. | -| C13 semantic evaluator paging timeouts | 7 | LIKELY-FIXED-BY `f8fec7b55` + `f282b313d` | Recheck the seven evaluator TMT identities after paged vector commit admission. | -| C14 missing database authority | 22 | STALE-TEST | Convert remaining fixtures to explicit daemon or exclusive-maintenance scopes; never widen ambient test authority. | -| C15 daemon/maintenance overlap | 7 | STALE-TEST | Remove nested incompatible scopes from fixture construction. | -| C16 macOS daemon clean early exit | 54 | ENV/INFRA | Isolate test daemons from nextest/process-group signals and record the terminating signal. | -| C17 configuration reset propagation | 4 | STALE-TEST | Seed the final configuration schema or assert ResetRequired; do not expect in-place migration. | -| C18 daemon disconnect/project retirement | 10 | PRODUCT-BUG | Keep the selected server alive through response settlement and close only after in-flight joins. | -| C19 application surface never mounts | 6 | PRODUCT-BUG | Find the owner that leaves the runtime in `mounting` for the full retry budget. | -| C21 SQLite lease expiry | 9 | PRODUCT-BUG | Renew/shorten bounded transactions so Linux load cannot expire live work. | -| C22 unverifiable summary timestamp | 4 | STALE-TEST | Add authoritative raw-source timestamps to the compression fixtures. | -| C23 memory graph publication conflict | 9 | PRODUCT-BUG | Make curator graph publication idempotent against its own verified head. | -| C24 WAL/graph lock lifecycle | 3 | PRODUCT-BUG | Drain readers/leases before checkpoint, reopen, or close. | -| C25 runtime timeout/deadlock | 22 | PRODUCT-BUG | Split by owner and fix missing completion/cancellation joins; do not raise timeouts. | -| C26 Dashboard response drift | 17 | STALE-TEST | Regenerate/align Dashboard fixtures to current typed envelopes and CAS fields. | -| C-P-AUTOMATION | 12 | PRODUCT-BUG | Repair automation execution/ledger/runtime failures. | -| C-P-DAEMON | 28 | PRODUCT-BUG | Repair daemon ownership, readiness, cancellation, and scheduler failures. | -| C-P-HOST | 19 | PRODUCT-BUG | Repair host mutation/rollback and privacy-admission behavior. | -| C-P-LCM | 7 | PRODUCT-BUG | Repair LCM privacy receipt and retained transport failures. | -| C-P-MCP | 21 | PRODUCT-BUG | Repair MCP dispatch, settlement, and process-tree behavior. | -| C-P-MEMORY | 4 | PRODUCT-BUG | Repair fact identity/lineage behavior. | -| C-P-MISC | 19 | PRODUCT-BUG | Repair isolated typed runtime defects not covered above. | -| C-P-SEARCH | 4 | PRODUCT-BUG | Repair remaining code-index/search activation failures. | -| C-P-SESSION | 1 | PRODUCT-BUG | Repair the remaining session ingestion failure. | -| C-P-STORAGE | 10 | PRODUCT-BUG | Repair storage identity, lock, and durable-state failures. | -| C-P-WORK | 5 | PRODUCT-BUG | Repair work/workflow routing and settlement failures. | -| C-S-AUTOMATION | 16 | STALE-TEST | Align automation DTO/status/count fixtures. | -| C-S-DAEMON | 23 | STALE-TEST | Align daemon contract assertions where the new typed state is intentional. | -| C-S-HOST | 35 | STALE-TEST | Align host/plugin/hook fixture contracts and generated assets. | -| C-S-LCM | 11 | STALE-TEST | Align LCM privacy, redaction, and retained-envelope fixtures. | -| C-S-MCP | 30 | STALE-TEST | Align tool names, schemas, parsers, and affected-test result DTOs. | -| C-S-MEMORY | 1 | STALE-TEST | Align the remaining fact-store expectation. | -| C-S-MISC | 14 | STALE-TEST | Align isolated renamed status/error expectations. | -| C-S-SEARCH | 5 | STALE-TEST | Align remaining search/eval pins and expected states. | -| C-S-SESSION | 5 | STALE-TEST | Align session/temporal fixture counts and hashes. | -| C-S-STORAGE | 10 | STALE-TEST | Align final-schema and storage error expectations. | -| C-S-WORK | 5 | STALE-TEST | Align work/workflow contract fixtures. | - -## Hypothesis verification - -### Known intentional reforms - -All six named reform commits are ancestors of the failed head, not post-run -fixes: - -- `c5c0a7663` live Hermes/Kiro handlers: ancestor of `c962cd627`. -- `2ec565ad5` unbound-observer/profile-minting behavior: ancestor. -- `e7a740457` 2170-chunk workload retarget: ancestor. -- `39629c4f5` daemon-pin corpus alignment: ancestor. -- `132b5b0ac` typed ResetRequired settlement: ancestor. -- `79406945a` canonical isolated-path spelling fix: ancestor. - -Therefore C02-C05 are expectation/fixture drift at the failed head; they are -not fixed merely because those commits exist locally. - -Evidence: - -- C02 actual: Kiro exits 0 with `{}` but emits - `local counter reset daemon call failed`; the test invokes a restored live - handler without a daemon and still requires empty stderr. -- C03 both tests compare actual - `sha256:068e...5610` against stale `sha256:a8e1...1347`. -- C04 fails before initialization with `unknown tool: 'fact_store'`; the - removed broad dispatcher cannot prove first-touch behavior. -- C05 still checks `authority == "graph store"` and does not print the actual - typed error, while the settlement reform preserves the owning authority's - ResetRequired state. If adding diagnostic context shows a non-ResetRequired - variant, reclassify this one test as PRODUCT-BUG rather than weakening it. - -### Skill coverage - -C06 is not one missing tool. The panic lists **79** uncovered tools: - -`tracedecay_github_stack_signal_expand`, `tracedecay_stack_snapshot`, -the five native-integration lifecycle tools, three multi-root tools, five -feedback tools, `tracedecay_affected_tests`, six fact-store read/reason tools, -`tracedecay_memory_status`, `tracedecay_session_refresh`, -`tracedecay_rename_symbol`, `tracedecay_observatory_read`, 32 work tools, -16 workflow-definition/run/handoff tools, and five worktree cleanup/inventory -tools. - -Exact uncovered set: - -- Stack/native/multi-root: - `tracedecay_github_stack_signal_expand`, `tracedecay_stack_snapshot`, - `tracedecay_preflight_native_integration`, - `tracedecay_approve_native_integration`, - `tracedecay_apply_native_integration`, - `tracedecay_native_integration_status`, - `tracedecay_cancel_native_integration`, - `tracedecay_multi_root_scope_set_read`, - `tracedecay_multi_root_scope_set_compare_and_swap`, - `tracedecay_multi_root_execute`. -- Feedback/memory/session: - `tracedecay_feedback_diagnostics`, `tracedecay_feedback_get`, - `tracedecay_feedback_expand`, `tracedecay_feedback_list`, - `tracedecay_feedback_impact`, `tracedecay_affected_tests`, - `tracedecay_fact_store_probe`, `tracedecay_fact_store_related`, - `tracedecay_fact_store_reason`, `tracedecay_fact_store_contradict`, - `tracedecay_fact_store_get`, `tracedecay_fact_store_list`, - `tracedecay_memory_status`, `tracedecay_session_refresh`, - `tracedecay_rename_symbol`, `tracedecay_observatory_read`. -- Work: - `tracedecay_work_generate_proposal`, `tracedecay_work_create`, - `tracedecay_work_review_proposal`, `tracedecay_work_accept_proposal`, - `tracedecay_work_admit_execution`, `tracedecay_work_start_attempt`, - `tracedecay_work_synthesize`, `tracedecay_work_attempt_status`, - `tracedecay_work_cancel_attempt`, `tracedecay_work_resume_attempts`, - `tracedecay_work_retry_attempt`, `tracedecay_work_list_attempts`, - `tracedecay_work_execution_history`, `tracedecay_work_hydrate_artifacts`, - `tracedecay_work_retrieve_evidence`, `tracedecay_work_views`, - `tracedecay_work_experience`, `tracedecay_work_compare_proposal`, - `tracedecay_work_prepare_graph_mutation`, - `tracedecay_work_mutate_graph`, `tracedecay_work_topology`, - `tracedecay_work_topology_metrics`, - `tracedecay_work_prepare_duplicate_adjudication`, - `tracedecay_work_adjudicate_duplicate`, - `tracedecay_work_adjudicate_leak`, `tracedecay_work_pause_run`, - `tracedecay_work_resume_run`, `tracedecay_work_run_control`, - `tracedecay_work_placement_preflight`, - `tracedecay_work_admit_placement`, - `tracedecay_work_placement_status`, - `tracedecay_work_release_placement`. -- Workflow: - `tracedecay_workflow_register_definition`, - `tracedecay_workflow_activate_definition`, - `tracedecay_workflow_retire_definition`, - `tracedecay_workflow_reject_definition`, - `tracedecay_workflow_validate_definition`, - `tracedecay_workflow_get_definition`, - `tracedecay_workflow_list_definitions`, - `tracedecay_workflow_definition_history`, - `tracedecay_workflow_diff_definition`, - `tracedecay_workflow_handoff_issue`, - `tracedecay_workflow_handoff_redeem`, - `tracedecay_workflow_start_run`, `tracedecay_workflow_pause_run`, - `tracedecay_workflow_resume_run`, `tracedecay_workflow_cancel_run`, - `tracedecay_workflow_get_run`. -- Worktree: - `tracedecay_worktree_inventory`, - `tracedecay_worktree_cleanup_inspect`, - `tracedecay_worktree_cleanup_confirm`, - `tracedecay_worktree_cleanup_remove`, - `tracedecay_worktree_cleanup_reconcile`. - -This is a real discoverability gap, although the current test's body-string -scan is not acceptable as the final behavioral acceptance mechanism. - -### macOS path hypothesis is disproved - -At failed head `c962cd627`, `under_isolated_root` already: - -1. tries raw `starts_with`; -2. canonicalizes the root through the deepest existing ancestor; -3. canonicalizes the candidate path the same way; -4. compares the canonical spellings. - -That is exactly the `/var` -> `/private/var` settlement from `79406945a`. -Moreover, the errors are not macOS-only: - -- missing managed-daemon/maintenance authority: 90 failed attempts across - 26 tests (macOS 48, Linux 42); -- daemon/maintenance overlap: 28 failed attempts across seven tests - (macOS 14, Linux 14). - -The remaining issue is fixture scope ownership, not path spelling. The -post-run `f88610f33` diff confirms this diagnosis by keeping a -`DaemonDatabaseScope` alive in the four convergence fixtures. - -### Environment/lifecycle families - -| Signature | Raw occurrences | Distinct tests | OS conclusion | Classification | -|---|---:|---:|---|---| -| daemon exits status 0 before socket | 107 | 54 | macOS only | ENV/INFRA signal leakage | -| WAL checkpoint `busy=1` | 34 | 15 | macOS + Linux | secondary lifecycle symptom | -| `application.surface.unavailable` | 95 | 17 | macOS + Linux | PRODUCT-BUG unless a stale reset fixture precedes it | -| configuration persisted shape reset | 16 | 4 | macOS + Linux | STALE-TEST fixture | -| daemon closed connection | 17 | 6 | macOS + Linux | PRODUCT-BUG | -| retained server retired | 14 | 12 | macOS + Linux | PRODUCT-BUG | -| child did not exit within 20s | 5 | 3 | Linux only | PRODUCT-BUG/process leak | -| SQLite transaction lease expired | 26 | 16 | Linux only | PRODUCT-BUG for terminal failures; retry-pass subset is ENV/INFRA | - -`run_foreground_unix` has no ordinary status-0 pre-socket return for these -fresh-home fixtures: after excluding the fresh-home-inapplicable account -deletion resume, a clean return comes from its Ctrl-C/SIGTERM select branch. -The macOS-only, empty-stderr pattern therefore points to test-runner/process- -group signal leakage, not a product startup rejection. - -The WAL line is often only logged by the daemon while the test later fails for -another reason. `checkpoint_result` truthfully returns busy/incomplete; the -dispatch task is lifecycle drainage, not weakening the error. - -### Named residual families - -- Transcript concurrent full batches: both writers return `Ok(())`; this is a - real split-brain/CAS defect. -- Transcript late-cursor and stale-higher tests: raw rows commit but summary - count remains zero. Their synthetic summary bypasses the current immutable - lineage publication contract; align the fixture before declaring a storage - regression. -- Worktree canonical-root pair: both fail before their canonical-root - assertions because a prior `TraceDecay` authority is still incompatible - with `HostAdmissionTestRuntimeV1::profile`; these fixtures use `drop` instead - of the explicit consuming `TraceDecay::close`. -- LCM canary: `FOREIGN_CANARY` is - `sk-proj-lineage-foreign-canary-1234567890`, intentionally secret-shaped; - privacy admission blocks setup before foreign-lineage disclosure is tested. -- Update second-writer: `host_owns_canonical_component_set` derives from all - stock host kinds with non-empty default components; Cline is now in that - canonical set, while the test still puts it with Zed/Roo/Kilo. -- Nextest-only abort: - `daemon::tests::socket::socket_git_preview_apply_replay_and_pre_admission_problems_are_canonical` - deterministically stack-overflows and SIGABRTs on both macOS and Linux. - -## Post-run commits that plausibly remove failures - -- `42bbaeb6a`: production now schedules historical schema convergence instead - of leaving it pending forever. -- `f88610f33`: maintenance mode is fixed at registry construction and the four - convergence tests now hold daemon database scope for the fixture lifetime. -- `5a00ee063`..`f282b313d`: semantic evaluation now exposes source errors, - isolates source projection, uses an identity receipt, pages durable vector - commits, and admits page-local change-set digests. -- `2ff144f83`: rustfmt-only checkpoint; it plausibly clears the Format job. - -These are **likely-fixed**, not verified: this task forbids Cargo runs. - -## Non-test CI blockers - -1. **Windows compile — PRODUCT-BUG, still open.** - `crates/tracedecay-usecases/src/retention/code_index_generations/scope_quarantine.rs:422` - calls `creation_time()` through the wrong metadata extension trait on - Windows (`cap_fs_ext::Metadata` implements `MetadataExt`, not - `OsMetadataExt`). The Windows test job consequently ran zero tests. -2. **Clippy — PRODUCT-BUG, still open.** - - `crates/tracedecay-policy/src/work_loop.rs:515`: 8 arguments. - - `crates/tracedecay-store/src/memory/project_memory/mod.rs:116`: 9 arguments. - - `crates/tracedecay-store/src/memory/project_memory/curation/operations.rs:226`: - redundant closure. - No post-run commit touches those files. -3. **Format — LIKELY-FIXED-BY `2ff144f83`.** - The failed paths (`paths_and_io.rs`, `semantic_evaluation.rs`, - `hook_cmd.rs`, `grafeo_restart_acceptance.rs`) are in the post-run rustfmt - delta. - -## Ordered fix dispatch - -1. Windows compile (`creation_time`) so the Windows shard can execute. -2. C11 deterministic stack overflow; it is a process abort, not assertion drift. -3. C16 macOS test-daemon signal isolation; it masks 54 identities. -4. Recheck C12 on the four directly changed convergence tests. -5. Recheck C13 semantic TMT tests after `f8fec7b55`/`f282b313d`. -6. C14/C15 fixture authority cutover; keep explicit production scopes. -7. C18/C19/C24/C25 daemon settlement, mounting, checkpoint, and join defects. -8. C21 Linux transaction lease expiry. -9. C07a transcript CAS. -10. C23 memory graph publication conflict. -11. C08 worktree authority lifetime. -12. C06 public-tool discovery/skill coverage. -13. C17/C22/C09 stale reset/timestamp/canary fixtures. -14. C02-C05 and C10 narrow stale reform expectations. -15. C26 Dashboard typed contract/CAS fixture alignment. -16. C-S-MCP and C-S-HOST generated tool/host contract alignment. -17. Remaining subsystem product lanes, then remaining stale-test lanes. -18. Clippy cleanup. -19. Run narrow non-vacuous checks per lane, then a fresh complete main-CI run. - -## Class membership index - -Appendix A is the affected-test list. The following selectors make the primary -partition reproducible: - -- C01 contains these 24 retry-only identities: - `agents::context_scout_model::tests::configured_model_measures_usage_when_backend_omits_token_counts`, - `agents::context_scout_model::tests::denied_backend_surfaces_denied_not_unavailable`, - `agents::context_scout_model::tests::disconnected_backend_surfaces_disconnect_not_unavailable`, - `agents::context_scout_model::tests::production_adapter_sends_only_bounded_candidates_and_retains_usage`, - `daemon::code_index_scheduler::tests::registry_feeds_publications_and_bounded_freshness_reads`, - `daemon::service::invocation::tests::lsp_tests::lsp_disconnect_expiry_settles_unacknowledged_outbound_as_dropped`, - `daemon::tests::bootstrap::remote_account_deletion_joins_admitted_open_before_enumeration_and_reconciles_restart`, - `exact_sql::tests::authority::long_lease_transaction_renews_its_lease_after_successful_bounded_steps`, - `global_db::upsert_session_message_preserves_oversized_text_losslessly`, - `jobs::concurrent_manual_job_triggers_do_not_double_execute`, - `lcm_payload::delete_external_payload_rejects_referenced_payload_without_hash_verification`, - `lcm_payload::replay_and_successor_reuse_reject_mutated_payload_global_authority`, - `lcm_payload::summary_publication_binds_external_payload_manifest_and_sanitization_receipt`, - `lcm_query::expand::expand_returns_sliced_raw_summary_and_payload_content_with_ranges`, - `lcm_query::status::status_reports_payload_gc_run_metadata_after_apply`, - `lcm_raw::transcript_ingest_preserves_lossless_raw_content`, - `persistence_failure_cannot_be_rewritten_as_a_clean_terminal`, - `runtime::lcm::payload::rollback_tests::direct_store_failure_rolls_back_metadata_and_payload_file`, - `session_temporal_benchmark::tests::fixture_refresh_persists_progress_before_measurement`, - `tool_daemon_test::cursor_after_shell_missing_daemon_exits_promptly_without_children`, - `tool_daemon_test::status_json_requests_compact_daemon_payload_noninteractively`, - `tool_daemon_test::tool_cli_invokes_mcp_tool_through_daemon_socket`, - `tool_daemon_test::tool_cli_rejects_truncated_json_rpc_response_without_hanging`, - `tool_daemon_test::tool_cli_skips_daemon_notifications_until_matching_response`. -- C02-C11 are the exact named tests in their table descriptions. -- C12 is the four `session_registry::tests` entries directly changed by - `f88610f33`: daemon admission, duplicate attach, convergence checkpoint, and - degraded convergence. -- C13 is the six nextest-only `candidate_output::tests` TMT entries plus - `packaged_assets::tests::packaged_evaluator_runs_against_an_unrelated_project`. -- C14-C24 select Appendix entries by their literal evidence phrase: - missing database authority, scope overlap, status-0 daemon exit, - configuration reset, daemon-close/retired-server, application-surface - unavailable, transaction expiry, unverifiable timestamp, memory graph - publication conflict, and WAL/graph lock respectively. Earlier exact classes - win when an identity matches more than one selector. -- C25 selects remaining terminal timeout/deadline/`Elapsed` identities. -- C26 is every Dashboard identity. -- Remaining identities are partitioned by subsystem prefix and then by - evidence: expected-value/schema/name/DTO drift is `C-S-*`; runtime errors, - conflicts, unavailable states, lock failures, and invariant violations are - `C-P-*`. - -## Appendix A — all failing identities and compact error snippets - -Format: `identity [jobs] — first useful panic/error text`. ``, ``, -and `` normalize unstable values. `nextest` marks failures that had no -`test ... FAILED` line. - -- `advanced_workflow_journey_test::mounted_fan_out_recovers_then_synthesizes_and_hands_off` [Linux/macOS] — advanced Work TaskSession journey requires the byte-pinned FastEmbed fixture in TRACEDECAY_DISTRIBUTION_FASTEMBED_FIXTURE -- `agent_cmd::tests::codex_core_rollback_restores_generated_agent_exports_byte_for_byte` [Linux/macOS] — called `Result::unwrap()` on an `Err` value: NativeUpdateRequired -- `agent_cmd::tests::codex_native_removed_retry_cleans_receipt_owned_source` [Linux/macOS] — called `Result::unwrap()` on an `Err` value: bundle ownership marker conflicts or is ambiguous -- `agent_cmd::tests::kimi_canonical_component_set_fails_before_direct_host_mutation` [Linux/macOS] — assertion failed: error does not contain `host capability is unsupported` -- `agent_cmd::tests::kiro_context_mcp_apply_converges_without_rollback` [macOS] — Kiro Install apply did not converge after an interrupted host-bundle operation -- `agent_cmd::tests::opencode_core_rollback_restores_every_registration_side_effect` [Linux/macOS] — `StalePreview` at host component registration -- `agents::codex::mcp_registry::tests::add_and_remove_preserve_an_operator_owned_peer_server` [macOS] — host add changed Codex-owned peer state and left host state unaccepted -- `agents::context_scout_model::tests::configured_model_measures_usage_when_backend_omits_token_counts` [Linux] — `DeadlineExceeded` -- `agents::context_scout_model::tests::denied_backend_surfaces_denied_not_unavailable` [Linux] — `Err(DeadlineExceeded)` vs `Err(Denied)` -- `agents::context_scout_model::tests::disconnected_backend_surfaces_disconnect_not_unavailable` [Linux] — `Err(DeadlineExceeded)` vs `Err(Disconnected)` -- `agents::context_scout_model::tests::production_adapter_sends_only_bounded_candidates_and_retains_usage` [Linux] — `DeadlineExceeded` -- `agents::copilot::tests::add_and_remove_preserve_an_operator_owned_peer_server` [macOS] — host add changed peer MCP servers and left host state unaccepted -- `agents::host_cli::tests::env_shebang_interpreter_is_resolved_before_ambient_path_is_cleared` [macOS] — interpreter command bytes differ despite the same rendered command -- `agents::kiro::tests::add_and_remove_preserve_an_operator_owned_peer_server` [macOS] — forced host add changed peer MCP servers and left host state unaccepted -- `agents::kiro::tests::rollback_refuses_a_foreign_registry_write_after_cli_apply` [macOS] — fake native add returned `StorageFailure` -- `agents::path_normalize_tests::path_lookup_preserves_non_unicode_parent_components` [macOS] — `Illegal byte sequence` -- `analytics_api::tests::diagnostics_summary_aggregates_real_hook_completed_rows_safely` [Linux/macOS] — `"unavailable"` vs `"measured"` -- `api::automation_outcomes_endpoint_returns_live_read_only_outcomes` [Dashboard] — expected activated skill outcome; response contains facts but no skills -- `api::holographic_dashboard_endpoints_return_seeded_payloads` [Dashboard] — graph-assist coverage is null, expected `"complete"` -- `api::holographic_fact_detail_returns_full_content_and_entities` [Dashboard] — expected `linked_entities` -- `api::lcm_endpoints_cover_seeded_fts_and_like_fallback` [Dashboard] — null vs true -- `api::lcm_project_store_wins_over_global_accounting_override` [Dashboard] — null vs `"profile_sharded"` -- `api::lcm_serves_project_session_store_without_global_override` [Dashboard] — null vs `"profile_sharded"` -- `application_surface::tests::catalog_bound_compatibility_tools_resolve_before_retained_dispatch` [Linux/macOS] — live catalog-bound compatibility-tool set differs from expected set -- `authentic_callback_to_all_delivery_surfaces` [Linux/macOS] — LSP surface absent -- `authority_tests::a_selected_project_answers_feedback_and_work_reads_and_nothing_else_by_post` [Linux/macOS] — `None` vs `Some(Work)` -- `authority_tests::application_routes_are_active_project_only` [Linux/macOS] — HTTP 404 vs 204 -- `authority_tests::dashboard_user_job_run_without_automation_authority_fails_closed` [Linux/macOS] — HTTP 500 vs 503 -- `authority_tests::graph_overview_returns_the_canonical_dashboard_envelope` [Linux/macOS] — `"unknown"` vs `"ready"` -- `authority_tests::memory_status_returns_the_canonical_dashboard_envelope` [Linux/macOS] — `"error"` vs `"ready"` -- `automation::automation_run_artifact_api_serves_verified_sidecar_payloads` [Dashboard] — malformed automation ledger `started_at` -- `automation::final_self_improvement_smoke_covers_autonomous_curation_and_skill_deployment` [Dashboard] — settings patch lacks `expected_revision_id` -- `automation::jobs::effect_receipt::tests::file_write_failure_after_open_is_a_bound_partial_effect` [macOS] — post-open failure was not a partial effect -- `automation_jobs::dashboard_user_job_history_appears_only_after_retained_settlement` [Dashboard] — user-job backend never reached admitted execution -- `automation_skills::managed_skills_are_dashboard_controllable_with_direct_activation` [Dashboard] — disable did not export to Claude -- `backend::classifies_backend_failures_for_retry_policy` [Linux/macOS] — `Disconnected` vs `Unavailable` -- `backend::failure_disposition_heals_stale_recorded_retryability` [Linux/macOS] — `Some(Disconnected)` vs `Some(Unavailable)` -- `cancellation_before_relational_cas_keeps_the_prior_head_current` [macOS] — `Conflict` -- `cancelling_generic_tool_reaps_child_and_closes_request` [Linux/macOS] — daemon exited status 0 before accepting connections -- `candidate_output::tests::candidate_bytes_match_direct_production_calls` [nextest] — TMT after 360s -- `candidate_output::tests::direct_outputs_cover_train_and_validation` [nextest] — TMT after 360s -- `candidate_output::tests::evaluation_rejects_optional_stage_status_that_disagrees_with_profile` [nextest] — TMT after 360s -- `candidate_output::tests::published_corpus_maps_production_source_occurrences` [Linux/macOS] — `Unavailable(ReadFailed)` vs `Complete` -- `candidate_output::tests::query_phrase_and_historical_queries_reach_their_checked_in_anchors` [Linux/macOS] — checked-in historical anchor absent -- `candidate_output::tests::rerank_profiles_remain_pending_when_no_rerank_measurement_ran` [nextest] — TMT after 360s -- `candidate_output::tests::resource_evidence_enforces_state_budgets_and_exact_catalog` [nextest] — TMT after 360s -- `candidate_output::tests::semantic_profiles_do_not_claim_a_comparison_when_only_fallback_ran` [nextest] — TMT after 360s -- `claude_plugin_bundle_test::claude_agents_allow_only_live_read_only_mcp_tools` [Linux/macOS] — automation-auditor grants `tracedecay_analytics`, whose live `readOnlyHint` is not true -- `claude_plugin_schema_test::claude_bundle_hooks_config_matches_the_claude_hooks_schema` [Linux/macOS] — Claude hooks schema rejects `PostCompact` -- `cli_args_contract_test::arg_catalog_table_flags_exist_in_tool_schemas` [Linux/macOS] — catalog documents `--keywords` absent from context schema and documents removed `fact_store` -- `cli_args_contract_test::managed_skill_guidance_matches_automatic_activation` [Linux/macOS] — managed-skill approval/review-queue guidance is stale -- `cli_mcp_and_http_dispatch_the_same_callable_contracts` [Linux/macOS] — `github_stack_signal_expand` lacks parity-golden accounting -- `cli_non_interactive_test::automation_config_enable_writes_canonical_project_setting_noninteractively` [Linux/macOS] — application surface unavailable, then not-found/not-authorized -- `cli_non_interactive_test::automation_config_set_rejects_unimplemented_external_backend` [Linux/macOS] — stderr lacks `unknown automation backend` -- `cli_non_interactive_test::automation_config_set_writes_complete_canonical_project_setting_noninteractively` [Linux/macOS] — application surface unavailable, then not-found/not-authorized -- `cli_non_interactive_test::branch_add_tracks_the_branch_on_the_single_project_store` [Linux/macOS] — code-index scheduler unavailable for branch activation -- `cli_non_interactive_test::branch_gc_preserves_profile_shard_without_repository_evidence` [Linux/macOS] — configuration persisted shape requires reset -- `cli_non_interactive_test::branch_list_reads_profile_sharded_branch_meta` [Linux/macOS] — fixture project never mounted -- `cli_non_interactive_test::branch_remove_deletes_branch_db_from_profile_shard` [Linux/macOS] — configuration persisted shape requires reset -- `cli_non_interactive_test::branch_remove_deletes_branch_local_memory_without_cutover_receipt` [Linux/macOS] — configuration persisted shape requires reset -- `cli_non_interactive_test::branch_removeall_deletes_profile_shard_branch_dbs` [Linux/macOS] — configuration persisted shape requires reset -- `cli_non_interactive_test::fact_store_curate_records_backend_disabled_skip_and_preserves_read_only_inspection` [Linux/macOS] — null vs `"memory_curator"` -- `cli_non_interactive_test::gitignore_reads_effective_config_for_primary_and_linked_worktrees` [Linux] — application surface unavailable while reading effective configuration -- `cli_non_interactive_test::init_skips_gitignore_prompt_when_stdin_not_a_terminal` [Linux/macOS] — initialization stderr no longer matches the expected noninteractive text -- `cli_non_interactive_test::install_codex_automation_enables_daemon_owned_project_configuration_noninteractively` [Linux/macOS] — `Text file busy` -- `cli_non_interactive_test::list_all_reports_orphan_manifest_reconstructable_store` [macOS] — daemon exited status 0 before accepting connections -- `cli_non_interactive_test::list_all_reports_profile_sharded_store_without_stale_label` [macOS] — daemon exited status 0 before accepting connections -- `cli_non_interactive_test::list_all_uses_registry_profile_shard_when_enrollment_marker_missing` [macOS] — daemon exited status 0 before accepting connections -- `cli_non_interactive_test::projects_context_resolves_linked_worktree_path_by_git_common_dir` [macOS] — daemon exited status 0 before accepting connections -- `cli_non_interactive_test::projects_context_resolves_project_id_and_path` [macOS] — daemon exited status 0 before accepting connections -- `cli_non_interactive_test::projects_list_json_reads_global_registry` [macOS] — daemon exited status 0 before accepting connections -- `cli_non_interactive_test::projects_search_text_matches_registered_alias` [macOS] — daemon exited status 0 before accepting connections -- `cli_non_interactive_test::sessions_search_omits_absent_optional_filters_and_preserves_provider` [macOS] — daemon exited status 0 before accepting connections -- `cli_non_interactive_test::sessions_unfinished_lists_workflow_state_evidence` [Linux/macOS] — daemon exited status 0 before accepting connections -- `cli_non_interactive_test::status_reports_uninitialized_project_without_creating_it` [macOS] — daemon exited status 0 before accepting connections -- `cli_non_interactive_test::status_surfaces_split_identity_conflict_without_suggesting_init` [Linux/macOS] — daemon exited status 0 before accepting connections -- `cli_non_interactive_test::storage_report_prints_registered_store_size_and_unregistered_backlog` [Linux/macOS] — report JSON is empty/EOF -- `cli_non_interactive_test::storage_report_uses_active_daemon_authority_without_hanging` [macOS] — daemon exited status 0 before accepting connections -- `cli_non_interactive_test::wipe_all_removes_profile_sharded_store_and_global_row` [macOS] — shard `tracedecay.db` remains -- `cli_non_interactive_test::wipe_all_removes_registry_backed_profile_shard_without_enrollment_marker` [macOS] — registry-backed profile shard remains -- `code_diagnostics::code_diagnostics_dashboard_api_exposes_engines_and_applies_settings` [Dashboard] — HTTP 503 vs 200 -- `codex_compaction::codex_post_compact_hook_commits_app_server_summary_through_daemon_effect` [Linux/macOS] — daemon exited status 0 before accepting connections -- `codex_goals::codex_workflow_lifecycle_secret_content_is_sanitized_before_persistence` [Linux/macOS] — secret-bearing goal was not sanitized before persistence -- `codex_response_items::codex_goal_response_item_is_cataloged_as_context` [Linux/macOS] — LCM privacy sanitizer receipt construction failed -- `codex_response_items::codex_response_item_skips_developer_messages_and_keeps_reasoning_summaries` [Linux/macOS] — LCM privacy sanitizer receipt construction failed -- `codex_usage::codex_structured_events_produce_full_row_mix` [Linux/macOS] — LCM privacy sanitizer receipt construction failed -- `commands::storage::wipe_target_tests::registered_project_paths_preserve_non_unicode_roots` [macOS] — expected non-Unicode path absent -- `compare_reports_unmeasured_semantic_and_rerank_stages_as_pending` [Linux/macOS] — `"fail"` vs `"pending"` -- `config::tests::runtime_configuration_cutover::resolve_runtime_configuration_pins_registered_project_when_cache_is_cold` [Linux/macOS] — configuration ResetRequired: no canonical revision -- `config::validation_rejects_zero_scheduler_tick_secs` [Linux/macOS] — error no longer contains `scheduler_tick_secs` -- `cursor::cursor_pre_compact_without_native_payload_is_read_only_and_unavailable` [Linux/macOS] — daemon exited status 0 before accepting connections -- `cursor::cursor_transcript_ingest_retries_after_mid_batch_db_failure` [Linux/macOS] — required projection-audit trigger missing -- `cursor_composer::composer_envelope_todo_secret_is_sanitized_before_persistence` [Linux/macOS] — secret-bearing todo was not sanitized -- `cursor_native_extension_receipt_matches_embedded_assets` [Linux/macOS] — `Corrupt` vs `Current` -- `daemon::automation_effect::journal::tests::cancellation_observed_under_lock_leaves_foreign_reservation_pending` [Linux/macOS] — curation receipt missing `accepted_operations` -- `daemon::automation_effect::journal::tests::durable_journal_rejects_swapped_partial_receipts_before_write` [Linux/macOS] — curation receipt missing `accepted_operations` -- `daemon::automation_effect::journal::tests::durable_journal_reports_changed_project_owner_as_a_conflict` [Linux/macOS] — prepared-effect binding now rejected as inconsistent -- `daemon::automation_effect::journal::tests::durable_journal_reports_changed_scope_identity_as_a_conflict` [Linux/macOS] — recovery problem now rejected as inconsistent -- `daemon::automation_effect::journal::tests::durable_journal_reports_changed_task_identity_as_a_conflict` [Linux/macOS] — recovery problem now rejected as inconsistent -- `daemon::automation_effect::journal::tests::foreign_reservation_recovery_persists_exact_partial_terminal` [Linux/macOS] — curation receipt missing `accepted_operations` -- `daemon::automation_effect::journal::tests::pending_index_survives_physical_reopen_and_closes_after_terminal` [Linux/macOS] — curation receipt missing `accepted_operations` -- `daemon::automation_effect::journal::tests::physical_reopen_rejects_a_corrupt_swapped_terminal` [Linux/macOS] — corrupt fixture rejected during construction as inconsistent -- `daemon::automation_effect::journal::tests::project_open_repairs_corrupt_append_intent_at_clean_eof_without_pending_journals` [Linux/macOS] — corrupt append-intent path missing -- `daemon::automation_effect::journal::tests::reserved_admission_conflict_preserves_recovery_index` [Linux/macOS] — prepared-effect binding rejected as inconsistent -- `daemon::automation_effect::journal::tests::reserved_read_removes_an_orphan_terminal_sidecar` [Linux/macOS] — expected reservation read error did not occur -- `daemon::automation_effect::journal::tests::retained_projector_panic_finishes_recovery_before_releasing_task_lock` [nextest] — TMT after 360s -- `daemon::automation_effect::journal::tests::terminal_admission_conflict_preserves_existing_cleanup_authority` [Linux/macOS] — prepared-effect binding rejected as inconsistent -- `daemon::automation_effect::projection::tests::all_noop_curation_projects_accepted_effects_without_mutation_or_anchors` [Linux/macOS] — canonical all-noop receipt contains another owner's fact -- `daemon::branch_admin::tests::profile_bootstrap_preserves_future_spool_reset_without_retry_mapping` [Linux/macOS] — profile identity root permissions are not 0700 -- `daemon::code_index_scheduler::activation_tests::cold_mount_defers_sealed_decode_and_truth_verification_to_the_retained_owner` [macOS] — retained owner did not activate the sealed generation -- `daemon::code_index_scheduler::branch_generations::tests::mounted_store_diffs_two_clean_exact_commit_generations` [Linux/macOS] — exact-generation read timed out -- `daemon::code_index_scheduler::ignored_dependencies_tests::cancellation_tests::admitted_source_read_observes_live_cancellation_between_chunks` [macOS] — unexpected `IgnoredDependency(SymlinkEscape)` -- `daemon::code_index_scheduler::tests::registry_feeds_publications_and_bounded_freshness_reads` [Linux] — initial publication timed out -- `daemon::git_transactions::native::tests::apply_rematerializes_exact_commit_input_after_executor_restart` [Linux/macOS] — preview is `Unsupported` -- `daemon::git_transactions::native::tests::files_ref_backend_exposes_no_destination_publication_window` [Linux/macOS] — typed preview is `Unsupported` -- `daemon::git_transactions::native::tests::native_blockers_never_mint_a_preview_from_stale_caller_state` [Linux/macOS] — stale materialization did not return `StalePreview` -- `daemon::git_transactions::native::tests::snapshot_capture_agrees_across_symlink_repository_root_aliases` [Linux/macOS] — drifted preview CAS did not report stale preview -- `daemon::http_application_tests::daemon_http_authenticated_operations_cancel_and_resume_through_canonical_owner` [Linux/macOS] — cancel returned HTTP 503 -- `daemon::invocation_executor::controlled_invocation_tests::in_process_effect_without_settlement_returns_reset_required` [Linux/macOS] — authoritative join timed out -- `daemon::lcm_effects::tests::codex_and_cursor_daemon_adapters_commit_exact_authoritative_summaries` [Linux/macOS] — `"needs_summary"` vs `"ok"` -- `daemon::lcm_effects::tests::compression_producer_apply_read_and_rollback_stay_one_authority` [Linux/macOS] — relation reads reconstructed a pending projection -- `daemon::production_harness::configuration_idempotency_journey_test::configuration_set_has_cli_mcp_http_sdk_parity_and_replays_after_restart` [Linux/macOS] — first CLI configuration effect is not-found/not-authorized -- `daemon::production_harness::configuration_idempotency_journey_test::credential_effect_uses_the_durable_request_operation_digest` [Linux/macOS] — typed tool payload is invalid JSON -- `daemon::production_harness::configuration_idempotency_journey_test::user_profile_configuration_batch_has_cli_dashboard_parity_after_restart` [Linux/macOS] — session relation graph database remains locked on restart -- `daemon::production_harness::generation_retention_test::linked_worktree_scope_retention_crash_replay_and_pure_inventory_journey` [Linux/macOS] — distribution FastEmbed fixture missing -- `daemon::production_harness::generation_retention_test::mounted_daemon_maintenance_retains_activation_lease_and_converges_after_restart` [Linux/macOS] — committed query-only profile did not expose known-empty retention authority -- `daemon::production_harness::semantic_activation_journey_test::public_semantic_activation_rollback_and_exact_retry_preserve_graph_authority` [Linux/macOS] — distribution FastEmbed fixture missing -- `daemon::project_open_owners::code_index_reads::ignored_dependency_admission_tests::writable_binding_returns_only_after_exact_scope_generation_is_warm_and_serving` [Linux/macOS] — code graph projection had not completed activation -- `daemon::query_authority_provider::tests::activation_tests::committed_query_routes_install_and_rollback_as_one_revision` [macOS] — active vector generation is `None` -- `daemon::retained_owner::memory_target::tests::selected_project_opens_its_exact_read_only_store_not_the_active_store` [macOS] — `Unavailable` -- `daemon::retained_owner::session::retained_effect_tests::retained_begin_and_join_report_partial_effect_and_restart_recovers_same_operation` [Linux/macOS] — manifest digest mismatch -- `daemon::retained_owner::session::retained_effect_tests::retained_cancel_reports_partial_effect_with_canonical_cancelled_receipt` [Linux/macOS] — manifest digest mismatch -- `daemon::scheduler::combined_effect::tests::conflicting_reflector_abandons_only_the_fresh_skill_reservation` [macOS] — 0 vs 1 -- `daemon::scheduler::combined_effect::tests::conflicting_skill_abandons_only_the_fresh_reflector_reservation` [macOS] — 0 vs 1 -- `daemon::scheduler::combined_effect::tests::partial_replay_reuses_prior_scheduler_skip_without_current_publication` [macOS] — duplicate project authority -- `daemon::service::invocation::tests::dispatch_tests::feedback_handles_fail_closed_without_an_owner` [Linux/macOS] — absent owner returns `Unavailable`, not expected application problem shape -- `daemon::service::invocation::tests::dispatch_tests::multi_root_payloads_are_not_served_by_the_per_project_service` [Linux/macOS] — response is not `InvalidRequest` -- `daemon::service::invocation::tests::lsp_tests::lsp_disconnect_expiry_settles_unacknowledged_outbound_as_dropped` [Linux] — observability persistence deadline -- `daemon::service::invocation::tests::project_lifecycle_tests::recovery_quiescence_retires_only_the_selected_projects_lsp_owners` [Linux/macOS] — authorized-root scope set is invalid -- `daemon::service::project_runtime::observability_tests::each_producer_lifetime_uses_a_disjoint_ordered_stream` [Linux/macOS] — delivery settlement recorder already running -- `daemon::service::project_runtime::observability_tests::exact_profile_routing_collapses_linked_roots_without_crossing_profiles` [Linux/macOS] — expected distinct brain IDs but both are `brain.test-runtime` -- `daemon::session_sync::tests::cancel_in_alias_activation_gap_mirrors_primary_terminal_receipt` [Linux/macOS] — project cannot bind a foreign session shard -- `daemon::store_runtime::session_registry::code_graph::seals::tests::project_replay_pool_serializes_same_digest_from_distinct_sources` [macOS] — `Conflict` -- `daemon::store_runtime::session_registry::code_graph::seals::tests::replay_seal_publish_preserves_foreign_existing_destination` [Linux/macOS] — `Corrupt` vs `Conflict` -- `daemon::store_runtime::session_registry::project_memory_relation_graph_contract_tests::registered_memory_relation_graph_survives_restart_and_isolates_topologies` [Linux/macOS] — mounted graph reconciliation did not settle -- `daemon::store_runtime::session_registry::remote_recovery::publication::tests::failed_phase_transition_preserves_the_previous_active_fence` [Linux/macOS] — rollback unexpectedly required -- `daemon::store_runtime::session_registry::remote_recovery::publication::tests::stale_mounted_runtime_rejects_the_replacement_identity` [Linux/macOS] — error lacks `mounted identity` -- `daemon::store_runtime::session_registry::remote_recovery::publication::tests::unverified_destination_is_quarantined_before_retained_rollback_is_restored` [Linux/macOS] — expected file missing -- `daemon::store_runtime::session_registry::tests::background_convergence_commits_the_durable_authority_checkpoint` [Linux/macOS] — missing managed-daemon/maintenance authority -- `daemon::store_runtime::session_registry::tests::background_convergence_failure_remains_observable_as_degraded` [Linux/macOS] — missing managed-daemon/maintenance authority -- `daemon::store_runtime::session_registry::tests::cached_project_sessions_reject_conflicting_enrollment_authority` [Linux/macOS] — missing managed-daemon/maintenance authority -- `daemon::store_runtime::session_registry::tests::daemon_admission_returns_while_historical_convergence_is_blocked` [Linux/macOS] — missing managed-daemon/maintenance authority -- `daemon::store_runtime::session_registry::tests::duplicate_project_attaches_schedule_one_historical_convergence` [Linux/macOS] — missing managed-daemon/maintenance authority -- `daemon::store_runtime::session_registry::tests::existing_profile_memory_uses_final_schema_and_canonical_linked_lineage` [Linux/macOS] — missing managed-daemon/maintenance authority -- `daemon::store_runtime::session_registry::tests::profile_sessions_mount_rejects_incompatible_schema_through_registered_runtime` [Linux/macOS] — missing managed-daemon/maintenance authority -- `daemon::store_runtime::session_registry::tests::profile_sessions_mount_uses_the_durable_profile_identity_and_profile_pin` [Linux/macOS] — missing managed-daemon/maintenance authority -- `daemon::store_runtime::session_registry::tests::project_sessions_mount_uses_typed_enrollment_and_is_idempotent` [Linux/macOS] — missing managed-daemon/maintenance authority -- `daemon::store_runtime::session_registry::tests::read_only_worktree_mount_never_recreates_a_deleted_database` [Linux/macOS] — missing managed-daemon/maintenance authority -- `daemon::tests::bootstrap::account_tombstone_denies_projectless_memory_and_profile_automation` [macOS] — account deletion fails with authority unavailable -- `daemon::tests::bootstrap::daemon_restart_resumes_account_tombstone_without_ordinary_admission` [macOS] — missing managed-daemon/maintenance authority -- `daemon::tests::bootstrap::direct_tool_cache_miss_returns_warming_while_project_opens_in_background` [Linux/macOS] — bounded warming response timed out -- `daemon::tests::bootstrap::linked_route_reuses_primary_authority_while_shadow_writer_is_held` [Linux/macOS] — routes do not resolve one retained server -- `daemon::tests::bootstrap::mcp_bootstrap_catalog_bypasses_project_writer_gate` [Linux/macOS] — initialize waits on writer gate -- `daemon::tests::bootstrap::portable_broker_bootstrap_bypasses_project_writer_gate` [Linux/macOS] — portable initialize waits on writer gate -- `daemon::tests::bootstrap::production_composition_dashboard_persists_project_settings_over_http` [Linux/macOS] — settings patch lacks `idempotency_key` -- `daemon::tests::bootstrap::production_composition_harness_dispatches_application_invocations_in_process` [Linux/macOS] — `tracedecay_storage_status` exceeds absolute deadline -- `daemon::tests::bootstrap::production_composition_harness_reads_retained_profile_analytics_authority` [Linux/macOS] — retained analytics event absent -- `daemon::tests::bootstrap::production_composition_harness_shutdown_allows_immediate_profile_reopen` [Linux/macOS] — session relation graph remains locked -- `daemon::tests::bootstrap::production_composition_harness_wires_cross_project_resolver` [Linux/macOS] — top-level `project_path` rejected as a selector -- `daemon::tests::bootstrap::production_composition_mounts_core_query_without_optional_stage_evaluation` [Linux/macOS] — core query authority never becomes ready -- `daemon::tests::bootstrap::project_open_shutdown_retains_noncooperative_task_until_retry_joins_it` [Linux/macOS] — zero-deadline shutdown unexpectedly succeeds -- `daemon::tests::bootstrap::remote_account_deletion_joins_admitted_open_before_enumeration_and_reconciles_restart` [Linux] — account tombstone persistence timed out -- `daemon::tests::bootstrap::unenrolled_ambient_directory_is_rejected_before_project_warmup` [macOS] — authority error masks missing-enrollment error -- `daemon::tests::bootstrap::unenrolled_leaf_is_rejected_from_cache_and_direct_open` [macOS] — authority error masks missing-enrollment error -- `daemon::tests::handshake::daemon_refreshes_once_only_after_generation_change` [Linux/macOS] — socket client lacks managed-daemon/maintenance authority -- `daemon::tests::handshake::initialized_ack_preserves_pending_catalog_refresh_notification` [Linux/macOS] — socket client lacks managed-daemon/maintenance authority -- `daemon::tests::invocation_ownership::committed_project_invocation_routes_mounted_operations` [Linux/macOS] — Work route returns not-found/not-authorized -- `daemon::tests::multi_root_journey::authenticated_multi_root_journey_reaches_scope_set_storage` [Linux/macOS] — journey thread panics -- `daemon::tests::ownership::fresh_committed_project_open_mounts_feedback_before_lsp` [Linux/macOS] — committed Git identity has no feedback cycle -- `daemon::tests::ownership::released_automation_tombstone_allows_one_eventual_replacement` [Linux/macOS] — replacement owner-key mismatch panic -- `daemon::tests::remote_project_recovery::recovery_quiesces_only_a_and_remounts_its_retry_route` [Linux/macOS] — graph runtime close conflicts -- `daemon::tests::replay::client_identity_startup_replays_retained_profile_receipts` [macOS] — 0 vs 1 -- `daemon::tests::replay::projectless_user_session_setup_failure_returns_json_rpc_error` [Linux/macOS] — authenticated profile mismatch -- `daemon::tests::rmcp_route::portable_production_route_selects_rmcp_after_initialize` [Linux/macOS] — cancelled route does not terminate -- `daemon::tests::rmcp_route::production_rmcp_cancels_registered_and_pre_registration_requests` [Linux/macOS] — cancelled requests do not terminate -- `daemon::tests::rmcp_route::selected_target_rmcp_flushes_response_and_full_disconnect_cancels_target` [Linux/macOS] — daemon and maintenance scopes overlap -- `daemon::tests::rmcp_route::unix_production_route_selects_rmcp_only_after_initialize` [Linux/macOS] — cancelled route does not terminate -- `daemon::tests::runtime_identity::concurrent_same_identity_worktrees_keep_exact_server_and_scheduler_bindings` [Linux/macOS] — follow-up sees code graph unavailable instead of retaining linked route -- `daemon::tests::scheduler_config::cached_project_reconciles_cli_enabled_automation_without_cache_probe` [Linux/macOS] — scheduler key remains when expected absent -- `daemon::tests::scheduler_config::daemon_scheduler_discovery_without_work_does_not_wait_for_writer_gate` [Linux/macOS] — read-only scheduler discovery waits on writer gate -- `daemon::tests::scheduler_config::daemon_scheduler_skips_stale_owner_key_after_rekey` [Linux/macOS] — scheduler starts under a stale owner key -- `daemon::tests::scheduler_config::disabled_scheduler_reconcile_cannot_acknowledge_an_owner_that_then_exits` [Linux/macOS] — `RunningNotified` vs `Started` -- `daemon::tests::scheduler_config::fresh_v2_project_starts_the_required_automation_scheduler` [Linux/macOS] — scheduler key absent -- `daemon::tests::scheduler_config::profile_reconcile_broadcasts_to_cached_projects_without_opening_uncached_projects` [macOS] — projectless profile reconcile sees authenticated profile mismatch -- `daemon::tests::socket::daemon_linked_worktree_route_repairs_primary_identity_and_keeps_alias` [Linux/macOS] — linked-root path remains canonical instead of primary path -- `daemon::tests::socket::socket_client_requires_user_storage_scope_without_project` [Linux/macOS] — projectless handshake message changed -- `daemon::tests::socket::socket_git_preview_apply_replay_and_pre_admission_problems_are_canonical` [nextest] — stack overflow; SIGABRT on both attempts -- `daemon::tests::socket::user_session_read_bypasses_unregistered_project_route` [Linux/macOS] — user-session read times out -- `dashboard_project_settings_commit_through_the_daemon_control_plane` [Linux/macOS] — daemon exited status 0 before accepting connections -- `dashboard_user_settings_replay_through_application_restart` [Linux/macOS] — daemon exited status 0 before accepting connections -- `direct_lifecycle_entry_points_retain_production_authority` [Linux/macOS] — graph database locked by another process -- `dropped_reservation_releases_its_fence_after_caller_cancellation` [macOS] — `Conflict` -- `duplicate_receipt_corrections_choose_the_latest_revision_across_anchors_and_fragment_order` [Linux/macOS] — `IncompatibleFragments` -- `embedded_component_sets_complete_lifecycle_for_all_supported_hosts` [Linux/macOS] — `ArtifactContentMismatch` -- `every_cursor_carrying_code_operation_mints_and_spends_a_continuation` [Linux/macOS] — code-symbol search remains `application.surface.unavailable` after ~60s -- `every_journey_operation_binds_to_cli_and_mcp_and_withholds_http` [Linux/macOS] — operation count 11 vs 6 -- `exact_project_profile_identity_reuses_one_persistent_handle` [macOS] — `Conflict` -- `exact_search_does_not_wait_for_semantic_projection` [Linux/macOS] — `tracedecay init` rejects stale `--quiet` -- `exact_sql::tests::authority::long_lease_transaction_renews_its_lease_after_successful_bounded_steps` [macOS] — `TransactionExpired` -- `exact_verified_generation_lease_blocks_retirement_until_activation_releases_it` [macOS] — `Conflict` -- `expired_deadline_does_not_open_or_close_a_registered_store` [macOS] — `Conflict` -- `explorer::explorer_query_coordinates_real_sources_without_inventing_a_merge` [Dashboard] — `"unavailable"` vs `"ready"` -- `explorer::explorer_session_routes_reuse_lcm_size_and_read_context_authority` [Dashboard] — `"unknown"` vs `"ready"` -- `fact_merge_hydration::contradictions_are_recorded_explicitly_in_lineage` [Linux/macOS] — `FactNotFound` returned where storage error expected -- `fact_merge_hydration::failed_fact_batch_rolls_back_identity_assertion_anchor_and_lineage` [Linux/macOS] — `FactNotFound` returned after staged writes -- `fact_merge_hydration_test::contradictions_are_recorded_explicitly_in_lineage` [Linux/macOS] — `FactNotFound` returned where storage error expected -- `fact_merge_hydration_test::failed_fact_batch_rolls_back_identity_assertion_anchor_and_lineage` [Linux/macOS] — `FactNotFound` returned after staged writes -- `fixture_authority_test::committing_a_fixture_tree_never_stages_enrollment_state` [Linux/macOS] — daemon and maintenance scopes overlap -- `fixture_authority_test::enrolled_layout_comes_from_the_opened_graph` [Linux/macOS] — daemon and maintenance scopes overlap -- `fixture_authority_test::one_profile_serves_two_projects_with_distinct_stores` [Linux/macOS] — daemon and maintenance scopes overlap -- `generic_tool_accepts_slow_byte_stream` [Linux/macOS] — daemon exited status 0 before accepting connections -- `generic_tool_accepts_split_json_rpc_frame` [macOS] — daemon exited status 0 before accepting connections -- `generic_tool_handles_concurrent_requests_without_crosstalk` [Linux/macOS] — daemon exited status 0 before accepting connections -- `generic_tool_preserves_late_reply_within_response_grace` [Linux/macOS] — daemon exited status 0 before accepting connections -- `generic_tool_rejects_semantic_truncation_envelope_without_output` [macOS] — daemon exited status 0 before accepting connections -- `generic_tool_rejects_truncated_frame_without_output` [Linux/macOS] — daemon exited status 0 before accepting connections -- `generic_tool_rejects_unrepresentable_deadline` [macOS] — daemon exited status 0 before accepting connections -- `git_index_transactions::tests::configured_merge_diff_and_filter_drivers_are_preview_only` [Linux/macOS] — external drivers still present -- `global_db::open_at_upgrades_existing_global_db_with_analytics_events_table` [Linux/macOS] — session-temporal ResetRequired replaces upgrade -- `global_db::open_at_upgrades_existing_sessions_table_with_parent_columns` [Linux/macOS] — session-temporal ResetRequired replaces upgrade -- `global_db::search_session_messages_git_scoped_by_branch_with_hyphen_term` [Linux/macOS] — ProjectSessions authority required -- `global_db::upsert_session_message_externalizes_tool_payload_without_indexing_body_or_metadata` [Linux/macOS] — session-message upsert returns false -- `global_db::upsert_session_message_preserves_oversized_text_losslessly` [Linux] — session-message upsert returns false -- `global_registry_test::project_tokens_saved_schema_and_queries_still_work` [macOS] — byte-distinct path vectors render identically but compare unequal -- `graph_store_survives_reopen_and_preserves_superseded_generations` [Linux/macOS] — externalized state serialized before sealing -- `hook_lifecycle_lease_test::native_hook_captures_only_bound_transport_spool_records` [Linux/macOS] — captured hook count 0 vs 1 -- `hook_replay_test::replayed_provider_hooks_record_attributed_rows_and_bridge_to_analytics_events` [Linux/macOS] — Codex prompt hook exits 1 -- `hooks::codex::tests::codex_session_context_resolves_global_only_and_preserves_nudge` [Linux/macOS] — global-only repo reports `Generic` vs `Initialized` -- `hooks::tests::daemon_tool_json_returns_project_warming_without_retrying` [Linux/macOS] — hook daemon call retries warming until timeout -- `host_admission_test::host_ingress_binds_provenance_to_authoritative_project_and_replays_stably` [Linux/macOS] — observation remains `AcceptedForReplay/Pending` -- `host_admission_test::registered_profile_runtime_is_required_and_mismatch_never_falls_back` [Linux/macOS] — expected committed persisted outcome absent -- `host_admission_test::registered_project_runtime_is_exact_and_revocation_never_falls_back` [Linux/macOS] — expected committed persisted outcome absent -- `immediate_concurrent_and_repeated_opens_publish_one_callable_owner` [Linux/macOS] — test-results surface remains unavailable after ~60s -- `interrupted_convergence_serves_the_prior_snapshot_and_replays_identically` [macOS] — `Conflict` -- `jobs::concurrent_manual_job_triggers_do_not_double_execute` [macOS] — automation lock parent missing -- `jobs::user_job_delivers_output_to_file_and_records_ledger` [Linux/macOS] — null output path -- `labeled_byte_record_entities_reach_a_verified_head` [macOS] — `Conflict` -- `lcm_bridge::generated_skill_mirrors_session_context_retrieval_contract` [Linux/macOS] — generated skill lacks `begin` marker -- `lcm_bridge::generated_tools_bridge_preserves_message_kwargs_in_json_args` [Linux/macOS] — generated subprocess bridge changes message kwargs -- `lcm_compression::frontier::late_summary_projection_failure_rolls_back_payload_files_and_canonical_rows` [Linux/macOS] — `SummarySourceUnavailable(unverifiable_timestamp)` -- `lcm_compression::overflow::overflow_recovery_keeps_preserved_objective_scaffold_when_evicting_tail` [Linux/macOS] — session-message upsert returns false -- `lcm_compression::replay::idless_compression_replay_does_not_reingest_existing_raw_messages` [Linux/macOS] — `SummarySourceUnavailable(unverifiable_timestamp)` -- `lcm_compression::tool_transactions::bounded_leaf_chunk_backs_off_before_multi_tool_transaction` [Linux/macOS] — `SummarySourceUnavailable(unverifiable_timestamp)` -- `lcm_compression::tool_transactions::budget_and_overflow_replay_never_split_tool_transaction` [Linux/macOS] — `SummarySourceUnavailable(unverifiable_timestamp)` -- `lcm_compression::tool_transactions::fresh_tail_boundary_keeps_multi_tool_transaction_atomic_and_shrinking` [Linux/macOS] — `SummarySourceUnavailable(unverifiable_timestamp)` -- `lcm_dag::summary_expansion_marks_external_raw_sources_without_silent_empty_content` [Linux] — SQLite transaction lease expired -- `lcm_payload::api_alias_assignments_redact_apikey_and_apitoken` [Linux/macOS] — privacy sanitizer receipt construction failed -- `lcm_payload::delete_external_payload_rejects_referenced_payload_without_hash_verification` [Linux] — SQLite transaction lease expired -- `lcm_payload::denies_cross_session_payload_expansion` [Linux] — SQLite transaction lease expired -- `lcm_payload::denies_expansion_after_message_updates_to_new_payload_ref` [Linux] — SQLite transaction lease expired -- `lcm_payload::externalizes_large_tool_payload_with_recoverable_ref` [Linux] — SQLite transaction lease expired -- `lcm_payload::lcm_ingest_uses_the_canonical_privacy_detector_without_local_policy` [Linux/macOS] — redacted content lacks `canonicallcmcanary` -- `lcm_payload::private_key_redaction_cannot_be_disabled_by_local_metadata` [Linux/macOS] — privacy sanitizer receipt construction failed -- `lcm_payload::quoted_password_assignment_redacts_full_quoted_value` [Linux/macOS] — canonical credential redaction marker absent -- `lcm_payload::replay_and_successor_reuse_reject_mutated_payload_global_authority` [Linux] — SQLite transaction lease expired -- `lcm_payload::sensitive_redaction_is_canonical_lossy_and_not_indexed` [Linux/macOS] — canonical credential redaction marker absent -- `lcm_payload::summary_publication_binds_external_payload_manifest_and_sanitization_receipt` [Linux] — SQLite transaction lease expired -- `lcm_query::describe::describe_gives_session_overview_without_full_payload_bodies` [Linux] — SQLite transaction lease expired -- `lcm_query::describe::describe_node_and_external_payload_return_metadata_without_body_leaks` [Linux] — SQLite transaction lease expired -- `lcm_query::expand::expand_cross_session_external_row_can_hydrate_payload_via_two_step_expand` [Linux] — SQLite transaction lease expired -- `lcm_query::expand::expand_returns_sliced_raw_summary_and_payload_content_with_ranges` [Linux] — SQLite transaction lease expired -- `lcm_query::status::status_reports_payload_gc_run_metadata_after_apply` [Linux] — SQLite transaction lease expired -- `lcm_query::status::status_reports_schema_frontier_payload_and_debt_counts` [Linux] — SQLite transaction lease expired -- `lcm_raw::transcript_ingest_preserves_lossless_raw_content` [Linux] — SQLite transaction lease expired -- `lcm_summary_lineage_review::immutable_summary_lineage_rejects_foreign_session_canary_sources_without_disclosure` [Linux/macOS] — secret-shaped canary session-message upsert returns false -- `mcp::scope::tests::dotdot_request_resolves_the_same_worktree_scope_as_daemon_authority` [Linux/macOS] — dotdot spelling resolves a different scope identity -- `mcp::scope::tests::symlink_request_resolves_the_same_worktree_scope_as_daemon_authority` [Linux/macOS] — symlink spelling resolves a different scope identity -- `mcp::server::connection::cancellable_queue_tests::cancellation_during_route_resolution_reaches_selected_live_target` [macOS] — cancellation response times out -- `mcp::server::hook_boundary_failure_matrix_tests::matrix_backpressure_overflow_rejects_before_reconcile_without_pending_growth` [Linux] — count 2 vs 1 -- `mcp::server::hook_boundary_failure_matrix_tests::matrix_daemon_unavailable_without_broker_skips_reconcile_and_frontier` [Linux] — unavailable path opens reconcile sink -- `mcp::server::hook_boundary_failure_matrix_tests::matrix_identical_notifications_are_distinct_without_frontier_corruption` [Linux] — count 3 vs 2 -- `mcp::server::hook_boundary_failure_matrix_tests::matrix_unavailable_then_success_keeps_sticky_retained_failure_frontier` [Linux] — count 2 vs 1 -- `mcp::server::host_admission_tests::add_branch_at_replay_rejects_stale_branch_after_switch` [Linux] — count 1 vs 0 -- `mcp::server::host_admission_tests::add_branch_at_replay_rejects_stale_root_after_adversarial_replace` [Linux] — stale root writes -- `mcp::server::host_admission_tests::add_branch_at_restart_replay_rejects_common_dir_drift` [Linux] — count 1 vs 0 -- `mcp::server::host_admission_tests::add_branch_at_restart_replay_rejects_symlink_swap` [Linux] — count 1 vs 0 -- `mcp::server::host_admission_tests::add_branch_replay_rejects_stale_branch_after_delayed_switch` [Linux] — count 1 vs 0 -- `mcp::server::host_admission_tests::add_branch_restart_replay_rejects_stale_branch_after_switch` [Linux] — count 1 vs 0 -- `mcp::server::host_admission_tests::cancelled_canonical_attempt_is_recovered_and_replayed` [Linux] — count 0 vs 1 -- `mcp::server::host_admission_tests::commit_before_ack_replays_once_and_acknowledges_exact_duplicate` [Linux] — count 2 vs 1 -- `mcp::server::host_admission_tests::durable_route_survives_unavailable_effect_for_same_connection_retry` [Linux] — `Committed` vs `Unavailable` -- `mcp::server::host_admission_tests::malformed_semantic_payload_is_explicit_and_quarantined_across_reopen` [Linux] — effect was attempted unexpectedly -- `mcp::server::host_admission_tests::malformed_source_does_not_starve_valid_sibling_source` [Linux] — count 2 vs 1 -- `mcp::server::host_admission_tests::oversized_event_is_rejected_before_canonical_attempt` [Linux] — effect was attempted unexpectedly -- `mcp::server::host_admission_tests::quarantine_releases_active_capacity_then_full_fails_closed` [Linux] — count 1 vs 0 -- `mcp::server::host_admission_tests::sync_current_branch_replay_rejects_stale_branch_after_delayed_switch` [Linux] — count 1 vs 0 -- `mcp::server::host_admission_tests::sync_current_branch_restart_replay_rejects_stale_branch_after_switch` [Linux] — count 1 vs 0 -- `mcp::server::host_admission_tests::unsupported_payload_version_is_retryable_and_retained_across_reopen` [Linux] — effect was attempted unexpectedly -- `mcp::server::lcm_claude_recall_tests::lcm_expand_query_returns_every_matching_claude_message` [Linux/macOS] — retained transport unavailable -- `mcp::server::lcm_claude_recall_tests::lcm_expand_reads_every_live_raw_message_store_id` [Linux/macOS] — retained transport unavailable -- `mcp::server::lcm_claude_recall_tests::lcm_grep_finds_a_term_stored_in_exactly_one_message` [Linux/macOS] — retained transport unavailable -- `mcp::server::lcm_claude_recall_tests::lcm_grep_returns_every_matching_claude_message` [Linux/macOS] — retained transport unavailable -- `mcp::tools::definitions::tests::catalog_filter_preserves_non_catalog_tools_and_filters_catalog_bindings` [Linux/macOS] — legacy production tools are no longer discoverable -- `mcp::tools::handlers::analysis::unmounted_files::rust::tests::a_non_utf8_file_name_is_reported_without_panicking` [macOS] — `Illegal byte sequence` -- `mcp::tools::handlers::configuration_dispatch_tests::available_configuration_effect_reaches_canonical_executor` [Linux/macOS] — effect admission does not win settlement before daemon invocation -- `mcp::tools::handlers::configuration_dispatch_tests::every_other_configuration_effect_reaches_the_authoritative_daemon_executor` [Linux/macOS] — configuration-unset does not claim settlement -- `mcp::tools::handlers::context_scout_control_dispatch_tests::context_scout_pause_and_resume_preserve_caller_idempotency_keys` [Linux/macOS] — context-scout pause does not claim settlement -- `mcp::tools::handlers::dispatch_tests::a_warm_call_is_unaffected_by_the_ceiling` [Linux/macOS] — warm context sees code index unavailable -- `mcp::tools::handlers::dispatch_tests::graph_reader_selector_dispatch_policy_is_allowlisted` [Linux/macOS] — required selector keys are null -- `mcp::tools::handlers::dispatch_tests::graph_tools_reject_blank_node_ids_and_zero_depth_with_typed_errors` [Linux/macOS] — blank node ID maps to noncanonical occurrence instead of expected typed error -- `mcp::tools::handlers::dispatch_tests::unavailable_user_lcm_effect_is_rejected_before_profile_store_open` [Linux/macOS] — error does not contain expected `unknown` -- `mcp::tools::handlers::dispatch_tests::user_lcm_doctor_reports_a_missing_store_without_opening_it` [Linux/macOS] — profile retained authority unavailable -- `mcp::tools::handlers::retained_timeout_dispatch_tests::fact_store_curate_forwards_only_bounds_and_preserves_canonical_success` [Linux/macOS] — zero-second dispatch ceiling -- `mcp::tools::handlers::retained_timeout_dispatch_tests::fact_store_curate_pre_commit_cancellation_does_not_mutate` [Linux/macOS] — zero-second dispatch ceiling -- `mcp::tools::handlers::retained_timeout_dispatch_tests::fact_store_curate_rejects_a_partial_receipt_from_another_scope` [Linux/macOS] — zero-second dispatch ceiling -- `mcp::tools::handlers::workflow::affected_tests_tests::cancellation_retains_results_completed_before_the_later_test` [Linux/macOS] — `"invalid_test_identity"` vs `"cargo"` -- `mcp::tools::handlers::workflow::affected_tests_tests::directly_changed_test_file_dispatches_each_full_test_identity` [Linux/macOS] — dispatched test list is null -- `mcp::tools::handlers::workflow::affected_tests_tests::nested_source_module_dispatches_the_crate_relative_test_identity` [Linux/macOS] — dispatched test list is null -- `mcp::tools::handlers::workflow::affected_tests_tests::reported_passing_and_failing_tests_complete_with_observed_results` [Linux/macOS] — completion state is null -- `mcp::tools::handlers::workflow::affected_tests_tests::timed_out_test_runner_returns_a_terminal_receipt` [Linux/macOS] — `"invalid_test_identity"` vs `"cargo"` -- `mcp::tools::handlers::workflow::affected_tests_tests::vacuous_or_nonzero_test_output_is_a_failed_terminal` [Linux/macOS] — `"invalid_test_identity"` vs `"cargo"` -- `mcp::tools::handlers::workflow::test_runner::tests::deadline_terminates_and_reaps_the_complete_test_process_tree` [Linux] — child marker missing -- `mcp::tools::plugin_conformance_tests::plugin_tool_mentions_resolve_to_registered_tools` [Linux/macOS] — plugin mentions removed session-refresh/work tools -- `mcp::tools::plugin_conformance_tests::readme_mcp_allowlist_matches_read_only_tools` [Linux/macOS] — README allowlist differs from live `readOnlyHint=true` set -- `mcp::tools::plugin_conformance_tests::registered_tools_are_referenced_by_the_plugin_bundle` [Linux/macOS] — many registered tools unreferenced by Cursor plugin -- `mcp_cli_serve_test::explicit_initialized_path_ignores_initialize_roots` [macOS] — daemon exited status 0 before accepting connections -- `mcp_cli_serve_test::initialize_roots_auto_initializes_unindexed_git_repo` [Linux/macOS] — daemon exited status 0 before accepting connections / child timeout -- `mcp_cli_serve_test::initialize_roots_decode_file_uri_localhost_and_percent_escapes` [macOS] — daemon exited status 0 before accepting connections -- `mcp_cli_serve_test::no_explicit_path_auto_initializes_unindexed_git_cwd` [Linux/macOS] — daemon exited status 0 before accepting connections / child timeout -- `mcp_cli_serve_test::no_explicit_path_prefers_discovered_cwd_over_initialize_roots` [Linux/macOS] — daemon exited status 0 before accepting connections / child timeout -- `mcp_cli_serve_test::no_explicit_path_prefers_initialize_roots_over_global_fallback` [macOS] — daemon exited status 0 before accepting connections -- `mcp_cli_serve_test::no_explicit_path_without_roots_still_uses_global_fallback` [macOS] — daemon exited status 0 before accepting connections -- `mcp_cli_serve_test::serve_daemon_proxy_reports_daemon_disconnect_as_json_rpc_error` [nextest] — TMT after 360s -- `mcp_cli_serve_test::serve_stdio_smokes_automation_run_artifact_view` [macOS] — daemon exited status 0 before accepting connections -- `mcp_cli_serve_test::serve_stdio_smokes_managed_skill_list_and_view` [macOS] — daemon exited status 0 before accepting connections -- `mcp_cli_serve_test::serve_with_reachable_daemon_proxies_before_opening_explicit_project` [macOS] — serve does not connect before project resolution -- `mcp_cli_serve_test::serve_without_daemon_socket_reports_daemon_unavailable` [macOS] — command unexpectedly succeeds -- `mcp_cli_serve_test::unexpanded_template_path_prefers_initialize_roots_over_discovered_cwd` [macOS] — daemon exited status 0 before accepting connections -- `mcp_configuration_write_persists_and_rejects_stale_cas` [Linux/macOS] — daemon exited status 0 before accepting connections -- `mcp_handler_test::admin_test::project_registry_tools_missing_registry_carries_stable_shape` [Linux/macOS] — registered selection unresolved before dispatch -- `mcp_handler_test::lcm_test::lcm_expand_query_context_max_tokens_is_independent_of_max_tokens` [Linux/macOS] — retained transport unavailable -- `mcp_handler_test::lcm_test::lcm_grep_rejects_invalid_scope` [Linux/macOS] — generic invalid retained request replaces expected argument error -- `mcp_handler_test::lcm_test::lcm_grep_rejects_invalid_scope_without_searching_all_sessions` [Linux/macOS] — generic invalid retained request replaces expected argument error -- `mcp_handler_test::lcm_test::lcm_load_session_missing_store_uses_typed_empty_messages_without_creating_sessions_db` [Linux/macOS] — messages null vs empty array -- `mcp_handler_test::lcm_test::lcm_read_only_tools_return_not_ingested_without_creating_sessions_db` [Linux/macOS] — retained transport unavailable -- `mcp_handler_test::lcm_test::lcm_status_cli_bridge_accepts_json_args` [Linux/macOS] — retained authority unavailable -- `mcp_handler_test::retrieve_truncation_test::retrieve_tool_reports_missing_and_expired_handles_actionably` [Linux/macOS] — `Option::unwrap()` on `None` -- `mcp_handler_test::schema_test::exact_memory_tool_definitions_exclude_legacy_payload_aliases` [Linux/macOS] — schema type is `["number","null"]`, expected `"number"` -- `mcp_handler_test::schema_test::schema_required_arguments_match_representative_handler_parsers` [Linux/macOS] — route availability masks missing-parameter parser error -- `mcp_handler_test::session_search_test::message_search_rejects_all_registered_with_project_selector` [Linux/macOS] — unresolved registered selection masks selector rejection -- `mcp_handler_test::session_search_test::message_search_rejects_invalid_scope` [Linux/macOS] — generic invalid retained request replaces expected error -- `mcp_handler_test::session_search_test::message_search_rejects_unsupported_project_scope` [Linux/macOS] — call unexpectedly succeeds -- `mcp_handler_test::status_runtime_test::test_status` [Linux/macOS] — branch diagnostics absent -- `memory_curation::automatic_fact_receipt_endpoints_expose_terminal_applied_and_quarantined_receipts` [Dashboard] — count 2 vs 1 -- `memory_curation::retained_admin_journey_commits_add_update_feedback_and_remove` [Dashboard] — tombstone read does not preserve expected feedback lineage -- `memory_curation::retained_mutations_deny_foreign_project_scope_without_a_receipt` [Dashboard] — `"runtime"` vs `"application"` -- `memory_curator::backend_failures::memory_curator_runner_ledgers_malformed_backend_output` [Linux/macOS] — count 0 vs 1 -- `memory_curator::backend_failures::memory_curator_runner_records_noop_fallback_when_backend_run_task_fails` [Linux/macOS] — verified memory graph publication conflicted -- `memory_curator::manual_trigger::manual_memory_curator_runs_when_scheduling_and_task_are_disabled` [Linux/macOS] — count 0 vs 1 -- `memory_curator::memory_curator_persists_transient_transient_success_retry_receipt` [Linux/macOS] — verified memory graph publication conflicted -- `memory_curator::memory_curator_quarantines_legacy_output_after_bounded_repair_exhaustion` [Linux/macOS] — count 0 vs 2 -- `memory_curator::memory_curator_repairs_then_applies_validated_ops_and_records_ledger` [Linux/macOS] — verified memory graph publication conflicted -- `memory_curator::memory_curator_runner_applies_validated_ops_under_apply_policy` [Linux/macOS] — verified memory graph publication conflicted -- `memory_curator::memory_curator_runner_artifacts_block_handoff_without_validation_examples` [Linux/macOS] — verified memory graph publication conflicted -- `memory_curator::memory_curator_runner_artifacts_mark_handoff_ready_for_accepted_only_examples` [Linux/macOS] — verified memory graph publication conflicted -- `memory_curator::memory_curator_runner_auto_applies_validated_operations` [Linux/macOS] — verified memory graph publication conflicted -- `memory_curator::memory_curator_stops_before_backend_or_apply_when_caller_is_interrupted` [Linux/macOS] — error lacks `interrupted` -- `memory_curator::pagination::memory_curator_resumes_from_the_durable_next_page_cursor` [Linux/macOS] — verified memory graph publication conflicted -- `memory_curator::scheduler_memory_curator_applies_validated_ops_automatically` [Linux/macOS] — verified memory graph publication conflicted -- `memory_eval_test::eval_memory_feedback_trust` [Linux/macOS] — daemon exited status 0 before accepting connections -- `memory_eval_test::eval_memory_multiturn_continuity` [Linux/macOS] — daemon exited status 0 before accepting connections -- `memory_eval_test::eval_memory_no_pollution` [Linux/macOS] — daemon exited status 0 before accepting connections -- `memory_eval_test::eval_memory_ranking_feedback_promotes` [Linux/macOS] — daemon exited status 0 before accepting connections -- `memory_eval_test::eval_memory_ranking_morphology` [Linux/macOS] — daemon exited status 0 before accepting connections -- `memory_eval_test::eval_memory_ranking_retrieval_reinforcement` [Linux/macOS] — daemon exited status 0 before accepting connections -- `memory_eval_test::eval_memory_ranking_supersession` [Linux/macOS] — daemon exited status 0 before accepting connections -- `memory_eval_test::eval_memory_ranking_trust_bias` [Linux/macOS] — daemon exited status 0 before accepting connections -- `memory_eval_test::eval_memory_secret_rejection` [Linux/macOS] — daemon exited status 0 before accepting connections -- `memory_eval_test::eval_memory_skip_local` [Linux/macOS] — daemon exited status 0 before accepting connections -- `memory_eval_test::eval_memory_supersede_without_dup` [Linux/macOS] — daemon exited status 0 before accepting connections -- `multi_connection_test::split_brain_is_rejected_and_unavailable_daemon_fails_closed_until_restart` [Linux/macOS] — daemon exited status 0 before opening socket -- `multi_connection_test::twelve_mcp_cli_and_hook_clients_share_one_daemon_profile_store_owner` [Linux/macOS] — daemon exited status 0 before opening socket -- `native_host_event_fixtures_execute_provider_admission_paths` [Linux/macOS] — Hermes supported stdout is `{}` instead of empty -- `observation_store::legacy_idempotency_column_rows_migrate_before_reads_and_writes` [Linux/macOS] — session-temporal ResetRequired replaces migration -- `observation_workflow_projection::workflow_projection_rolls_back_rebuilds_restarts_and_audits` [Linux/macOS] — injected projection failure remains `RetryDeferred` -- `operation_family_executes_through_cli_mcp_and_http` [Linux/macOS] — retained project server retired during init -- `packaged_assets::tests::packaged_evaluator_runs_against_an_unrelated_project` [nextest] — TMT after 360s -- `packaged_host_ingest_delivers_a_registered_advisory_cycle` [macOS] — advisory cycle fails `feedback-document-outside-root` -- `persisted_topology_wakes_idle_rollup_after_unrelated_queue_tail` [Linux/macOS] — topology source does not bypass five-minute idle poll -- `persistence_failure_cannot_be_rewritten_as_a_clean_terminal` [Linux] — count 0 vs 2 -- `post_retention_corrections_join_retained_bounded_evidence_exactly` [Linux/macOS] — `IncompatibleFragments` -- `pr_autotrack_test::discovery_classifies_same_repo_and_fork_pull_heads` [Linux/macOS] — daemon and maintenance scopes overlap -- `pr_autotrack_test::failed_discovery_is_not_reported_as_an_empty_success` [Linux/macOS] — daemon and maintenance scopes overlap -- `pr_autotrack_test::reconciliation_without_scheduler_fails_before_git_or_state_mutation` [Linux/macOS] — daemon and maintenance scopes overlap -- `pre_cancelled_application_has_cli_mcp_http_parity` [macOS] — daemon exited status 0 before accepting connections -- `primitive_config_markdown_json_parity` [Linux/macOS] — daemon exited status 0 before accepting connections -- `production_lsp_negotiates_and_projects_canonical_context` [Linux/macOS] — daemon exited status 0 before accepting connections -- `production_primitive_code_routes_have_cli_mcp_http_parity` [Linux/macOS] — daemon exited status 0 before accepting connections -- `production_primitive_reads_agree_across_mcp_http_and_cli` [Linux/macOS] — daemon authority wait timed out -- `production_project_open_serves_a_paginated_symbol_graph_read` [Linux/macOS] — symbol search remains unavailable after ~60s -- `profile_memory_scope_uses_exact_profile_authority` [macOS] — `Conflict` -- `profile_sessions_scope_uses_exact_profile_authority` [macOS] — `Conflict` -- `profile_storage_reset_test::branch_open_rejects_a_mismatched_maintenance_profile` [Linux/macOS] — error text changed from `branch snapshot open` to `branch open` -- `profile_storage_reset_test::incompatible_profile_store_requires_reset_without_in_place_changes` [Linux/macOS] — ResetRequired authority/reason differs -- `profile_storage_reset_test::trace_decay_open_branch_uses_shared_profile_store` [Linux/macOS] — branch tracking is absent -- `project_open_application_boundary` [Linux/macOS] — daemon exited status 0 before accepting connections -- `project_session_and_code_scopes_keep_distinct_locator_authority` [macOS] — `Conflict` -- `projects::project_scoped_plugin_routes_read_selected_project_store` [Dashboard] — selected-project graph coverage contract differs -- `public_executable_routes_are_served_by_the_production_daemon` [macOS] — daemon exited status 0 before publishing authority -- `report_tests::baseline_report_is_self_validating_but_not_activation_evidence` [Linux/macOS] — activation now requires a passing direct evaluation -- `research_anchors::authorized_resolution_rejects_unknown_fields_and_incoherent_states` [Linux/macOS] — incoherent wire value decodes -- `research_anchors::canonical_retrieval_anchor_rejects_payload_unknown_fields_and_claimed_ids` [Linux/macOS] — unknown-field diagnostic changed -- `reset_required_is_retained_until_an_explicit_reopen` [macOS] — `Conflict` -- `reset_required_shape_is_recreated_fresh_and_republished_from_the_manifest` [macOS] — `Conflict` -- `restart_reverification_installs_once_and_steady_reads_need_no_authority` [macOS] — `Conflict` -- `retained_surfaces::sdk::results::automation::curation::tests::all_noop_receipt_retains_acceptance_without_fabricating_mutations_or_anchors` [Linux/macOS] — noncanonical ManifestDigest -- `retained_surfaces::sdk::results::tests::automation_terminal_selects_only_its_exact_result_variant` [Linux/macOS] — terminal DTO matches no retained-surface variant -- `retention::orphan_stores::tests::unregistered_collection_rejects_profile_contained_data_root_symlink` [Linux/macOS] — `InspectFailed` vs `OutsideProfile` -- `retention::storage_report::tests::an_unreadable_store_reports_size_with_unsampled_free_pages` [Linux/macOS] — offline snapshot lacks managed-daemon/maintenance authority -- `retention::storage_report::tests::full_profile_report_creates_no_entries_under_the_profile_root` [Linux/macOS] — offline snapshot lacks managed-daemon/maintenance authority -- `retention::storage_report::tests::full_profile_total_includes_session_and_generation_files` [Linux/macOS] — offline snapshot lacks managed-daemon/maintenance authority -- `retention::storage_report::tests::report_sizes_every_registered_store_and_counts_unregistered_dirs` [Linux/macOS] — offline snapshot lacks managed-daemon/maintenance authority -- `retention::storage_report::tests::sizing_a_store_copies_nothing_and_leaves_no_scratch` [Linux/macOS] — offline snapshot lacks managed-daemon/maintenance authority -- `retention::storage_report::tests::storage_report_preserves_the_exact_live_global_database_family` [Linux/macOS] — offline snapshot lacks managed-daemon/maintenance authority -- `retention::storage_report::tests::unregistered_sizing_does_not_follow_symlinks` [Linux/macOS] — offline snapshot lacks managed-daemon/maintenance authority -- `retention_cleanup_failure_keeps_cancel_fence_until_retry` [nextest] — TMT after 360s -- `run_ledger::run_ledger_limit_and_malformed_lines_are_handled` [Linux/macOS] — malformed ledger row lacks status -- `run_ledger::run_ledger_rejects_legacy_rfc3339_with_subsecond_micros` [Linux/macOS] — legacy timestamp is no longer rejected -- `runtime::git_correlation::backfill::bounded::native::resume::tests::non_utf8_local_ref_is_sealed_without_fabricated_branch_text` [macOS] — child command fails -- `runtime::git_correlation::backfill::bounded::tests::non_utf8_canonical_worktree_resumes_exactly_then_fails_typed_publish` [macOS] — `Illegal byte sequence` -- `runtime::hermes::tests::hermes_reader_supports_non_utf8_database_paths` [macOS] — SQLite cannot open non-Unicode path -- `runtime::lcm::payload::rollback_tests::direct_store_failure_rolls_back_metadata_and_payload_file` [Linux] — `TransactionExpired` -- `runtime::opencode::tests::durable_sql_frontier_reaches_rows_beyond_a_poisoned_pass_after_restart` [Linux] — count 0 vs 1 -- `runtime::opencode::tests::steady_state_restart_keeps_high_water_without_per_row_durability_reads` [Linux] — count 2 vs 1 -- `runtime::opencode::tests::wal_part_append_replaces_the_durable_message_with_complete_content` [Linux] — unwraps missing value -- `scheduler::config_validates_scheduler_idle_and_lock_bounds` [Linux/macOS] — validation no longer mentions `min_idle_secs` -- `selected_project_source_route_survives_physical_daemon_restart` [macOS] — daemon exited status 0 before accepting connections -- `serve_template_path_test::literal_template_without_daemon_fails_closed_before_mcp_handshake` [macOS] — command unexpectedly succeeds -- `session_ingest_tests::cancelled_user_pass_reports_partial_coverage` [Linux/macOS] — deferred units 11 vs 9 -- `session_reflector::session_reflector_runner_skips_when_task_is_disabled` [Linux/macOS] — count 1 vs 0 -- `session_temporal_benchmark::tests::contract_matches_checked_in_artifacts` [Linux/macOS] — implementation hash mismatch -- `session_temporal_benchmark::tests::fixture_refresh_persists_progress_before_measurement` [Linux/macOS] — root refresh deadline exceeded -- `session_temporal_benchmark::tests::fresh_benchmark_db_provisions_key_for_rank_and_hydration` [Linux/macOS] — root refresh deadline exceeded -- `shutdown_terminal_linearizes_after_concurrent_admission` [Linux] — observability shutdown deadline -- `skill_lint_cursor_test::cursor_skill_references_resolve` [Linux/macOS] — six references name removed session-refresh/work tools -- `skill_targets_test::lifecycle_export_sweep_deploys_and_retracts_across_detected_agents` [Linux/macOS] — only Cursor exports; expected Claude and Cursor -- `skill_targets_test::lifecycle_export_sweep_isolates_per_agent_failures` [Linux/macOS] — Claude failure not reported -- `skill_targets_test::uninstall_all_removes_inverse_order_legacy_orphan_and_slugged_block` [Linux/macOS] — managed-skill prompt markers unbalanced -- `skill_targets_test::uninstall_all_removes_legacy_orphan_alongside_slugged_block` [Linux/macOS] — managed-skill prompt markers unbalanced -- `skill_targets_test::uninstall_repairs_legacy_orphan_end_without_claiming_user_text` [Linux/macOS] — managed-skill prompt markers unbalanced -- `skill_usage_test::repeated_skill_patches_recommend_improvement_review` [Linux/macOS] — `"repair_candidate"` vs `"patch_review"` -- `skill_usage_test::stale_scoring_explains_archive_candidates_and_exclusions` [Linux/macOS] — `"archive_candidate"` vs `"archive_review"` -- `skill_writer::skill_writer_runner_skips_when_task_is_disabled` [Linux/macOS] — count 1 vs 0 -- `sqlite_writer_uses_production_wal_normal_policy` [Linux/macOS] — count 2 vs 1 -- `stack_snapshot_decodes_into_the_typed_journey_request` [Linux/macOS] — `InvalidSurfaceRequest` -- `stale_binding_cannot_close_or_rebind_the_registered_store` [macOS] — `Conflict` -- `stdio_bridge_exits_successfully_after_client_shutdown_and_exit` [macOS] — daemon exited status 0 before accepting connections -- `storage_resolver_test::init_and_open::trace_decay_init_registers_default_profile_shard_globally` [Linux/macOS] — legacy checkout did not migrate to durable repository identity -- `temporal_application::canonical_digest_binds_every_semantic_input_and_excludes_resume_ephemera` [Linux/macOS] — `ProfileIdentityWithoutProject` -- `temporal_derived_evidence::frozen_temporal_page_returns_projected_occurrences_and_lineage` [Linux/macOS] — count 0 vs 1 -- `tests::default_validation_uses_byte_pinned_activation_workload` [Linux/macOS] — workload hash mismatch -- `the_dashboard_work_surface_answers_who_worked_on_a_task_on_both_published_mounts` [macOS] — daemon exited status 0 before publishing authority -- `the_parity_golden_accounts_for_every_catalog_operation` [Linux/macOS] — eight application operations unaccounted -- `the_work_surface_answers_real_requests_on_both_published_mounts` [macOS] — daemon exited status 0 before publishing authority -- `tool_command::tests::array_value_collected_via_repetition` [Linux/macOS] — `--keywords` removed from context schema -- `tool_command::tests::bare_boolean_flag_at_end_of_args_defaults_to_true` [Linux/macOS] — bare `--include-code` now requires a value -- `tool_command::tests::bare_boolean_flag_before_next_flag_does_not_swallow_it` [Linux/macOS] — `raw_json` false -- `tool_command::tests::boolean_flag_with_explicit_value_after_it_is_still_consumed` [Linux/macOS] — string `"false"` vs boolean false -- `tool_command::tests::boolean_flag_with_invalid_explicit_value_still_errors` [Linux/macOS] — invalid `"maybe"` parses successfully -- `tool_command::tests::coerces_boolean_flag` [Linux/macOS] — string `"true"` vs boolean true -- `tool_command::tests::dispatch_routing_keys_bypass_unknown_key_gate` [Linux/macOS] — `--project-root` removed from `fact_store_list` -- `tool_command::tests::fact_feedback_bare_helpful_flag_does_not_swallow_note_flag` [Linux/macOS] — `--helpful` removed from `fact_feedback` -- `tool_command::tests::finalize_arrays_splits_csv` [Linux/macOS] — unwraps missing array value -- `tool_daemon_test::configuration_tool_cli_persists_effects_and_fails_on_stale_cas` [Linux/macOS] — observed configuration state never becomes available -- `tool_daemon_test::cursor_after_shell_missing_daemon_exits_promptly_without_children` [macOS] — retained project server retired during init -- `tool_daemon_test::daemon_first_touch_uses_registered_runtime_without_rewriting_legacy_config` [Linux/macOS] — daemon closes connection before result -- `tool_daemon_test::daemon_project_cache_is_scoped_by_client_identity` [Linux/macOS] — daemon closes connection before result -- `tool_daemon_test::daemon_project_handshake_uses_client_profile_identity` [Linux/macOS] — daemon closes connection before result -- `tool_daemon_test::daemon_project_handshake_uses_registered_remote_store_after_rename` [Linux/macOS] — daemon closes connection before result -- `tool_daemon_test::daemon_project_handshake_uses_registry_backed_profile_store_without_marker` [Linux/macOS] — daemon closes connection before result -- `tool_daemon_test::daemon_reuses_project_engine_across_tool_clients` [Linux/macOS] — first status call counts 2 vs 1 -- `tool_daemon_test::daemon_sigterm_exits_while_authenticated_project_client_is_connected` [Linux/macOS] — daemon socket wait times out -- `tool_daemon_test::hermes_read_only_preflight_keeps_project_lcm_grep_available` [Linux/macOS] — project open completes without publishing a server -- `tool_daemon_test::kiro_hooks_capture_prompt_boundary_and_type_post_tool_use_unsupported` [Linux/macOS] — restored Kiro handler writes daemon-unavailable counter-reset stderr -- `tool_daemon_test::status_json_requests_compact_daemon_payload_noninteractively` [Linux/macOS] — retained project server retired during init -- `tool_daemon_test::tool_cli_invokes_mcp_tool_through_daemon_socket` [macOS] — daemon authority wait times out -- `tool_daemon_test::tool_cli_rejects_truncated_json_rpc_response_without_hanging` [macOS] — retained project server retired during init -- `tool_daemon_test::tool_cli_skips_daemon_notifications_until_matching_response` [macOS] — retained project server retired during init -- `tool_first_touch_test::fact_store_creates_profile_store_on_first_touch` [Linux/macOS] — removed `fact_store` tool is unknown -- `tool_skill_coverage_test::every_mcp_tool_is_taught_by_at_least_one_bundled_skill` [Linux/macOS] — 79 MCP tools unreferenced in canonical skill view -- `tracedecay::lifecycle::tests::nonempty_wrong_schema_read_only_open_returns_reset_required` [Linux/macOS] — ResetRequired authority is not `"graph store"` -- `tracedecay_test::daemon_tool_str_replace_updates_source` [Linux/macOS] — source edit now requires fresh idempotency key and preview expected-state -- `transcript_store::concurrent_full_batches_converge_without_split_brain_or_partial_writes` [Linux/macOS] — both concurrent full batches return `Ok(())` -- `transcript_store::late_cursor_failure_rolls_back_every_transcript_write_then_retries` [Linux/macOS] — summary count 0 vs 1 after retry -- `transcript_store::stale_higher_batch_is_rejected_until_reparsed_from_durable_cursor` [Linux/macOS] — summary count 0 vs 1 after reparse -- `update_cmd::tests::canonical_component_set_hosts_are_not_refreshed_by_a_second_writer` [Linux/macOS] — Cline now owns a canonical component set -- `verified_generations_keep_old_reads_dependencies_and_leases_stable` [macOS] — `Conflict` -- `workflow_json_preserves_a_typed_application_problem_envelope` [macOS] — count 0 vs 1 -- `workload_fixture::semantic_workload_and_incremental_fixture_are_byte_exact` [Linux/macOS] — workload hash mismatch -- `worktree_canonical_root_guard_test::opening_from_linked_worktree_keeps_canonical_root_on_primary` [Linux/macOS] — incompatible database authority before canonical-root assertion -- `worktree_canonical_root_guard_test::stale_worktree_canonical_root_heals_on_next_touch` [Linux/macOS] — incompatible database authority before healing assertion diff --git a/docs/plans/tracedecay-v2/audits/lane-staleness-2026-08-13.md b/docs/plans/tracedecay-v2/audits/lane-staleness-2026-08-13.md deleted file mode 100644 index 9d37c6933a..0000000000 --- a/docs/plans/tracedecay-v2/audits/lane-staleness-2026-08-13.md +++ /dev/null @@ -1,51 +0,0 @@ -# NEXT.md "Remaining work by lane" staleness audit (2026-08-13, HEAD 38d8f266a) - -Scope: docs/plans/tracedecay-v2/NEXT.md lines 369-508 ("Remaining work by lane"), -excluding the two bullets already confirmed DONE-and-marked in the file -(schema_unavailable bindings :390-397; github_runtime stack :442-453). - -| # | Bullet (handle · NEXT.md line) | Verdict | Evidence | -|---|---|---|---| -| 1 | Work/TaskSession: evaluated query profile + task-to-session correlation via MCP+SDK, restart (:373-376) | GENUINELY-OPEN | No hits for "evaluated_query_profile" / "task-to-session correlation" anywhere in src/crates/tests. The nearby `activate_evaluated_semantic_profile` (task_session.rs) is semantic-config activation, a different feature. Nothing implements or tests this specific journey. | -| 2 | Work/TaskSession: extend dashboard journey (who worked on a task, provider-qualified evidence, exact continuation, rank-final revocation, restart, 4 temporal modes) (:377-379) | GENUINELY-OPEN | No matches for "who worked on" / "provider_qualified" in tests or dashboard/src. No dashboard journey test found beyond the already-verified task-root journey NEXT.md itself cites as the baseline. | -| 3 | Terminal propagation: keep strict core from `850265033c` (ResetRequired=Never+[Reset], PartialEffect=Never+[Reconcile], fallible envelope) (:383-385) | STALE-DONE (invariant holds, not a task) | Verified directly in `crates/tracedecay-application/src/result/problem.rs:786-792` (`reset_required()` → `retry: Never, legal_actions: [Reset]`) and `crates/tracedecay-application/src/retained_surfaces/service.rs:648-660` (`PartialEffect` → `retry: Never, legal_actions: [Reconcile]`). No regression from `850265033c`; this bullet is a standing constraint that is currently satisfied, not open work. | -| 4 | Terminal propagation: drive PartialEffect/ResetRequired through HTTP, MCP, CLI, both SDKs, survive physical restart (:386-389) | PARTIAL | Typed surface exists everywhere named: HTTP (`src/application_surface/retained_http_identity_tests.rs`), MCP (`src/mcp/tools/handlers/retained_timeout_dispatch_tests.rs`), CLI (`src/work_cli.rs:557,672-673`), Rust SDK (`crates/tracedecay-sdk/src/client.rs`, `remote_client.rs`), TS SDK (`sdks/typescript/src/{client,types,operations}.ts` + `test/client.test.ts`). **No test combines `PartialEffect`/`ResetRequired` with a physical daemon restart** — grepped every `tests/**/*.rs` file containing `PartialEffect` for "restart": zero matches. The restart-survival half is genuinely open exactly as NEXT.md states. | -| 5 | *(schema_unavailable — already marked DONE 2026-08-10/verified 2026-08-13)* | — skipped per task scope — | | -| 6 | Retained surfaces: one physical-restart journey (memory reads/effects, session-refresh begin/status/cancel, LCM retrieval, exact identity) across CLI/MCP/HTTP/both SDKs (:401-404) | CANNOT-VERIFY-CHEAPLY (code exists, unclear if it's *one* unified journey) | `tests/grafeo_restart_acceptance.rs:524` (`memory_relation_graph_survives_physical_daemon_restart_and_isolates_profile_and_projects`, added by wind-down checkpoint `29ebe000d`, not `#[ignore]`d) covers memory read/effects + restart. No evidence it also drives session-refresh begin/status/cancel + LCM retrieval *in the same journey*, nor that it's been run to green since the checkpoint (commit message has no run evidence). Running it is a multi-thread daemon-spin-up integration test — not cheap, so not executed here. | -| 7 | Retained surfaces: prove cancellation/unavailable/partial-effect/reset-required/post-restart-reconciliation as externally observed terminals in that journey (:405-408) | Same as #6 | Depends on #6's journey; same evidence gap. | -| 8 | Observability: recover/checkpoint Work lifecycle, retry/leak, blocked-interval, native-integration, fan-out, reduced-rollup emitters without duplicating authority (:412-415) | PARTIAL | `src/daemon/service/invocation/work_blocked_interval_recovery.rs` and sibling `work*.rs` files exist and are current (not stale/orphaned per `tracedecay_dead_code`-style check not run, but file is actively referenced). No evidence of a "without duplicating authority" audit pass having been checkpointed since wind-down; can't confirm the recovery/dedup half without running the full observability suite. | -| 9 | Observability: reconstruct delivery settlement checkpoint from tree `52f68b8897...` (RMCP disconnect settlement, durable hook ACK/replay, cancellation, terminal CLI ACK) (:416-419) | GENUINELY-OPEN | The cited tree hash resolves (`git cat-file -t` → `tree`) but is **unreachable from any ref** (`git rev-list --objects --all` found 0 hits) — it's an orphaned wind-down snapshot, not yet merged. Grepped current HEAD for "RMCP disconnect", "hook_ack"/"HookAck", "durable hook ACK", "CliAck": **zero matches** — this functionality does not exist in the tree yet. Confirmed genuinely open. | -| 10 | Observability: run execution-topology metrics/rollup/compaction/retry/cancellation/restart journeys, not contract inventories (:420-421) | CANNOT-VERIFY-CHEAPLY | Requires running real journeys; no single cheap probe distinguishes "journey ran" from "contract inventory only" without executing the suite. | -| 11 | Observability: wire canonical CI failure-localization owner into delivery-evidence composition (`ProjectDeliveryFailureLocalizationSourceV1`) (:422-428) | GENUINELY-OPEN (confirmed exact match to doc) | Read `crates/tracedecay-usecases/src/delivery.rs:219-226` directly: `ProjectDeliveryFailureLocalizationSourceV1` still has exactly one variant, `NotConfigured`, with the identical doc-comment NEXT.md quotes. No owner wired. | -| 12 | Wound-down handoffs: Grafeo full daemon restart/isolation journey (not the narrow registry test) (:432-435) | GENUINELY-OPEN (code exists, unexecuted per doc's own framing) | `tests/grafeo_restart_acceptance.rs:524` implements exactly this test, added in wind-down checkpoint commit `29ebe000d` ("chore(integration): checkpoint active final-v2 work" — no body, no pass evidence). This matches NEXT.md's own description ("terminated before execution") rather than contradicting it — the code is staged but not proven green. | -| 13 | Wound-down handoffs: finish Hermes plugin/unit/stock changes, fresh binary, 8-check stock rerun (:436-441) | GENUINELY-OPEN | `git log --since=2026-08-09 --grep=Hermes -i` → 0 commits. `c635423a56` / `c061d3b883` (cited as already-landed prerequisites) are both ancestors of HEAD, but no follow-up work landed since. | -| 14 | *(github_runtime stack — already marked DONE 2026-08-13)* | — skipped per task scope — | | -| 15 | Wound-down handoffs: canonical-parent cutover removing remaining `crate::application` facade imports + unreleased compat modules/aliases from daemon/project-runtime/session-sync/MCP/root (:454-458) | **STALE-DONE** | `src/application.rs` (the root facade) was deleted in `540b6a605` "refactor(usecases): remove root application facade" (2026-08-10, ancestor of HEAD); a further `05924ecdf` "refactor(core): remove internal type facade" (2026-08-12) continued the cleanup. Confirmed at HEAD: `find src -iname "application*"` shows only `application_surface.rs`/`application_output` remain (no bare `application.rs`), and `grep -rn "use crate::application\b" src` returns zero hits. This is a third stale bullet joining the two already-known ones. | -| 16 | Wound-down handoffs: semantic config table ownership — real accepted-profile/Linux evaluation + live profile-activation journey (retrieval must stay available meanwhile) (:459-463) | PARTIAL | `tests/daemon_suite/advanced_workflow_journey/task_session.rs:249` (`activate_evaluated_semantic_profile`) and `:195` (`configure_restart_and_activate_semantic_profile`) implement exactly this — reopen lifecycle owner, wait for generation, activate profile, restart. Same wind-down-checkpoint caveat as #6/#12: code exists (checkpoint `29ebe000d`/`76a5da86c`), not confirmed run-to-green since. | -| 17 | Wound-down handoffs: doctor authority-audit + clean Cursor agents/in-composer install→bump→doctor lifecycle (:464-466) | GENUINELY-OPEN | No test/commit combines "cursor" + "doctor" lifecycle. Recent Cursor commits (`812c7b7f7` "align bundled hook expectation", `a832b80c5` hook ingest budget) are narrower fixes, not the full lifecycle journey. | -| 18 | Wound-down handoffs: re-run Grafeo/feedback-SDK/workflow-metadata-privacy/structured-privacy/Costs/LSP/application-final-surface suites in the aggregate matrix (:467-470) | CANNOT-VERIFY-CHEAPLY | Explicitly requires a full aggregate-matrix run; not a single/fast test. No aggregate-run artifact found on disk. | -| 19 | Wound-down handoffs: incremental indexing through save/rename/delete/ref-switch/overflow/cancellation/restart, serve-during-refresh, exact identity (:471-473) | PARTIAL (strong code coverage, unconfirmed pass) | `tests/daemon_suite/indexing_lifecycle_test.rs` (last touched `e9b956cb4` "verify ignored dependencies survive restart") has `mounted_incremental_lifecycle_preserves_only_complete_compatible_generations` covering save (`deliver_save`), rename (`fs::rename` + assertion the old path is gone), delete (`fs::remove_file`), ref-switch (`git checkout feature/lifecycle`), overflow (`inject_overflow`/`wait_for_overflow_cadence_receipt`), and cancellation (`write_cancellation_batch`); a sibling test `ignored_dependency_admission_survives_physical_daemon_restart_without_widening` covers restart. This is a full daemon-spin-up integration test, not run here for time-budget reasons — existence and shape strongly suggest this lane is closer to done than NEXT.md implies, but "serve-during-refresh" specifically wasn't confirmed in the body excerpt read. | -| 20 | Dashboard/release: freeze Rust source, run canonical contract generator + `contracts:check` (:477-478) | CANNOT-VERIFY-CHEAPLY | Process-shaped instruction (run generator, don't hand-edit) rather than a fixed artifact to diff; needs an actual generator run to confirm zero drift. | -| 21 | Dashboard/release: re-run Automations DOM tests after scheduler schema regen (:479-481) | PARTIAL / substantially satisfied | Ran `AutomationsPage.dom.test.tsx` directly (`npx vitest run`, 2.8s): **4/4 passed**. Recent scheduler fixes landed today (`2523c857e`, `bf7312fb9`, `9db1fdc88`), but the DOM test files themselves were last touched by `ed3775692`/`98b123b37`, predating today's scheduler work — so "after schema regen" freshness isn't fully confirmed, but the suite is green on the current tree right now. | -| 22 | Dashboard/release: regenerate SDK operations/types only after Work/TaskSession/terminal-problems/source-edit/retained-surfaces/native-topology/automation compile together (:482-484) | CANNOT-VERIFY-CHEAPLY | Ordering/gating instruction; would need a full cross-crate compile + generator run to confirm the precondition and outcome. | -| 23 | Dashboard/release: fresh binary + host install/update/doctor/stock journeys (Claude, Codex, Cursor agents/in-composer, Kimi, Kiro, opencode) (:485-487) | CANNOT-VERIFY-CHEAPLY | Multi-host smoke-test matrix; not cheaply verifiable by inspection. | -| 24 | Dashboard/release: complete default package/install/start journey (npm OIDC is the only remaining operator action) (:488-489) | CANNOT-VERIFY-CHEAPLY | Same — needs an actual install/start smoke run. | -| 25 | Dashboard/release: `src/doctor.rs:953` domain-symbol-extraction-unimplemented gap — decide implement-or-retire (:490-493) | GENUINELY-OPEN (confirmed exact match) | Read `src/doctor.rs` around the cited line: `domain_symbol_rules_warning()` still emits "domain symbol extraction is unimplemented" verbatim. No decision made either way. | -| 26 | Backend perf: refresh stale perf evidence — same-host `perf-gate.sh`, repaired session benchmark `--refresh-contract`, real Work rollup latency/throughput (:497-501) | PARTIAL | Active recent benchmark work exists: `d580c2e55` "perf(observability): benchmark work rollup journey" (2026-08-11) directly targets the named gap; `93d2978ab` "benchmark LCM and work readiness" and `5a3807eb5`/`ec24fc896` "benchmark mismatch replay recovery"/"scope mismatch benchmark support" (2026-08-12/13) show ongoing perf work. No published evidence artifact found on disk (`find -iname "*perf*evidence*"` empty) and no commit evidence of an actual `perf-gate.sh` run completing — code/benchmark scaffolding is progressing but the "evidence" (run + publish) deliverable isn't confirmed done. | -| 27 | Backend perf: build real `dashboard/app-dist`, then full `cargo nextest run --workspace --all-features --no-fail-fast` + dashboard typecheck/tests/build + contract checks + SDK tests + host bundle/stock tests + commitlint + release drift + packaging/install smoke (:502-506) | CANNOT-VERIFY-CHEAPLY | This is explicitly the full verification matrix; per project convention (long-verification-timeouts memory) this needs a detached multi-hour run, out of scope for a token-frugal audit pass. | -| 28 | Backend perf: treat zero-test filters/skipped suites/partial runs/timeouts/stale artifacts/synthetic evidence as unresolved (:507-508) | N/A (policy statement, not a task) | Nothing to verify — this is a standing acceptance-criteria rule for lane 27, not itself a deliverable. | - -## Distilled genuinely-open items, ranked by RC criticality - -1. **#9 — Delivery settlement reconstruction (RMCP disconnect settlement, durable hook ACK/replay, cancellation, terminal CLI ACK)**: confirmed zero implementation at HEAD; source tree (`52f68b8897...`) is an unmerged orphan. This blocks the whole observability/delivery lane and is explicitly gated behind Work+observability stabilizing first. -2. **#4 — PartialEffect/ResetRequired physical-restart survival**: typed surface is fully built (HTTP/MCP/CLI/both SDKs) but zero test proves it survives a daemon restart. This is the one missing link for the terminal-propagation lane to be journey-complete. -3. **#1/#2 — Work/TaskSession retrieval journeys** (evaluated query profile + task-to-session correlation; dashboard "who worked on a task" extension): no code or test evidence found at all — the least-started lane of the set. -4. **#11 — CI failure-localization owner**: confirmed still `NotConfigured`-only; small, well-scoped, but blocks delivery-evidence composition from being fully truthful. -5. **#13 — Hermes fresh binary + 8-check stock rerun**: prerequisite commits landed 2+ days ago; zero follow-up since. Cheap to finish, currently just not scheduled. -6. **#17 — Cursor doctor authority-audit + full lifecycle**: no evidence of a combined journey; isolated Cursor fixes only. -7. **#25 — `doctor.rs:953` implement-or-retire decision**: trivial to resolve (a decision, not a build), still open and non-blocking per the doc's own note. -8. **#6/#7/#12/#16/#19 — restart/activation journeys with code staged but unverified-green** (retained-surfaces restart, Grafeo isolation restart, semantic-profile activation, incremental-indexing lifecycle): these are *not* zero-progress — substantial, non-trivial test code already exists from the wind-down checkpoint commits (`29ebe000d`, `76a5da86c`). The real remaining work is executing and greenlighting them, not writing them from scratch. Recommend running these as the next verification pass rather than treating them as unstarted. - -## Additional stale-bullet found (beyond the two seeded in the task) - -- **#15 — canonical-parent facade cutover**: `src/application.rs` was already deleted in `540b6a605` (2026-08-10) and `05924ecdf` (2026-08-12), both ancestors of HEAD. `crate::application`/`use crate::application` no longer appear anywhere in `src/`. This bullet should be marked DONE in NEXT.md alongside the two already-known stale entries. diff --git a/docs/plans/tracedecay-v2/audits/v2-implementation-audit-2026-08-14.md b/docs/plans/tracedecay-v2/audits/v2-implementation-audit-2026-08-14.md deleted file mode 100644 index fa2dbc6081..0000000000 --- a/docs/plans/tracedecay-v2/audits/v2-implementation-audit-2026-08-14.md +++ /dev/null @@ -1,389 +0,0 @@ -# V2 implementation audit — 2026-08-14 - -## Method - -- Date: 2026-08-14. -- Audited revision: `f478c323a` (the checker reports were produced across the - immediately preceding `42acf504b` snapshot; the only intervening commit - surfaces semantic-evaluation rejection detail and is reflected below). -- Inputs: all 45 Markdown witness reports present in - `/tmp/v2-impl-check/`: 39 distinct plan shards, one production-mount census, - and five duplicate-plan witnesses. -- Checker models: GPT-5.6 Luna and GPT-5.6 Terra. Consolidation and conflict - adjudication: GPT-5.6 Sol. -- Method: merge every non-superseded gap; deduplicate cross-plan themes; require - a non-test caller for a production mount; adjudicate disputed call edges with - `tracedecay tool callers` and targeted current-tree reads. No Cargo, npm, or - journey command was run during consolidation. -- RC authority: `NEXT.md:692-700`. `RC-BLOCKING` means code or a production - mount/typed state is absent. `RC-REQUIRED-EVIDENCE` means the implementation - is present but the required current-tree journey or measurement is not. - `POST-RC` means an active-plan ambition beyond the stated RC bar. - -The register excludes explicitly superseded items. In particular, it does not -reopen the Delivery failure-localization owner decision -(`NEXT.md:493-502`), unsafe `commit_index` publication, removed public Work -snapshot/delta/replan/accept-task routes, Plan 39's historical procedure, or -operator-owned npm trusted-publisher setup. - -## Counts - -| Section | Entries | -|---|---:| -| Mountless surfaces | 20 | -| Missing deliverables | 17 | -| Partial work themes | 17 | -| Adjudicated conflicts | 10 | -| Evidence-only gaps | 18 | - -Across the 72 unique gap/evidence entries (conflict rows only reference those -entries): 33 are `RC-BLOCKING`, 15 are `RC-REQUIRED-EVIDENCE`, 21 are -`POST-RC`, and 3 are `RECORDED` (A2, A3, A12). - -## A. MOUNTLESS SURFACES - -Per repository policy, each implemented-but-unmounted item below must be wired -to a real production caller or deleted. - -| ID | Rank | Consolidated item | Evidence and witness verdicts | -| A22 | OWNER-DESIGN | Noncooperative authoritative-effect task retention | Added 2026-08-14 (settlement wave): a permanently noncooperative authoritative effect now correctly returns typed `ResetRequired` after the response grace, but its detached task remains unjoined; safely retaining/joining that task needs explicit owner design (settlement agent's architectural handoff — see dispatch settlement commits `232ed9411`…`f5f255818`). | -| A21 | RC-BLOCKING | Advisory host-delivery consume path | Added post-consolidation (hawk rerun triage, 2026-08-14): `crates/tracedecay-usecases/src/advisory/host_delivery.rs` delivery/consume/hook-notice surface is registered at daemon startup (`src/daemon/service/invocation/registrars.rs`) but the production call site uses only `.runtime().run_once()`; the consume/deliver half has zero production callers. Wire the delivery consumption or retire that half. | -|---|---|---|---| -| A1 | RC-BLOCKING | Generalized external-source acquisition, canonical refetch, correction, and tombstone production | Host-observation specialization is mounted, but `GitHubExternalSourceAcquisitionV1` and the generalized owner remain uncalled (`crates/tracedecay-usecases/src/external_source_github.rs:213-425`; `external_source_acquisition.rs:341-510`). Plans 02 and 03 independently verdict this `IMPLEMENTED-UNMOUNTED`. | -| A2 | RECORDED | Context Scout suggestion producer parked; neither remounted nor demolished | See [A2 ruling](#a2-ruling-2026-08-14). `1caf016e5` deliberately unmounted the saved-edit/stop producer and set `claim_authority = None`. `c2c11956d6` mounted the distinct advisory/feedback successor. Remount is not a small slice; deleting only the entry points leaves `prepare_controlled` dead. Owner must choose remount or retire. | -| A3 | RECORDED | V3 evidence assembly persist contract; unused `publish_or_replay` trait deleted | See [A3 ruling](#a3-ruling-2026-08-14). The unimplemented `EvidenceAssemblyStore` façade was deleted. Persisted schema, `RepositoryWritePayloadV1::EvidenceAssembly`, rusqlite publish-or-replay, and V3 exact/Git-topology targets remain sanctioned-pending contract authority. | -| A4 | RC-BLOCKING | Exact generation-bound Git and diagnostic joins | `GitReadAuthorityV1::join_generation` has only the regression caller (`crates/tracedecay-usecases/src/git_reads.rs:429`; `tests/git_intelligence_regression.rs:679`). `GenerationDiagnosticJoinV1` is referenced by contracts/composite types and tests, not a producer (`crates/tracedecay-code-index/src/diagnostics.rs:108-315`). `25-code-intelligence.md` says both are `IMPLEMENTED-UNMOUNTED`; `25-code-index.md` inferred `IMPLEMENTED+MOUNTED` from broader Git/LSP routes. Exact caller evidence resolves both as unmounted. | -| A5 | RC-BLOCKING | Canonical index and retrieval-pipeline observability emission | `record_index` exists at `crates/tracedecay-usecases/src/observability/emit.rs:560`; `emit_retrieval_pipeline` exists at `observability/retrieval_emit.rs:466`, but its only caller is an in-module test. Query execution computes an observation without calling the canonical emitter. | -| A6 | RC-BLOCKING | Conflict-prediction and linked-outcome emitters | `WorkConflictPredictionObservedV1` and `WorkConflictOutcomeLinkedV1` are closed payload/projection contracts (`crates/tracedecay-domain/src/observability/payload.rs:41-42`) with no production producer, so their confusion matrices cannot be truthful. | -| A7 | RC-BLOCKING | Adoption/consent observability emitters | Eligibility/outcome/consent payloads and helpers exist (`crates/tracedecay-domain/src/observability/retrieval.rs:194-285`; `crates/tracedecay-usecases/src/observability/emit.rs:488-513`) with no production caller. | -| A8 | RC-BLOCKING | `NoProgressObservedV1` | The type and validation exist (`crates/tracedecay-domain/src/observability/runtime.rs:42-54`; payload variant at `observability/payload.rs:35`), but there is no deadline/frontier producer. | -| A9 | RC-BLOCKING | GitHub stack capability/drift canonical emitters | Helpers exist in `crates/tracedecay-usecases/src/observability/github_stack_emit.rs:124-542`; `record_github_stack_drifts` has only its focused test caller. The stack/advisory runtime itself is mounted, but not these canonical observations. | -| A10 | POST-RC | Independent-review/task-outcome label emission | The closed vocabulary is implemented and tested (`crates/tracedecay-domain/src/observability/review_labels.rs:93-432`) without a root emitter or Plan 24 consumer. | -| A11 | RC-BLOCKING | Remote Brain node sender and operational plane | `EnrolledRemoteClient::capture` and transfer/query/recovery methods have zero production callers (`crates/tracedecay-sdk/src/remote_client.rs:197-355`). Inbound authority routes are mounted, but project composition supplies `RemoteOperationalReadV1::Unavailable` (`src/daemon/project_composition.rs:567`); no live Settings/Dashboard/Doctor state, replica/cache refresh, or remote clean-diagnostic publisher is mounted. | -| A12 | RECORDED | Supported-host registration breadth | See [A12 ruling](#a12-ruling-2026-08-14). Codex hook seed is empty by design and filled at install; Kimi has no global-hook or non-interactive plugin CLI; Codex Core now drives `codex plugin add` / `remove`. Kiro prompt-boundary and Kimi/OpenCode capture-fast-path remain out of this slice. | -| A13 | RC-BLOCKING | General typed workflow step executor | `WorkflowStepExecutionService::execute_ready_step` is implemented at `crates/tracedecay-application/src/workflow_run.rs:546`, but all callers are in `crates/tracedecay-application/tests/workflow_dag_execution.rs`. Production workflow start uses the narrower Work fan-out path. | -| A14 | POST-RC | Nineteen per-verb LSP gateway façade methods | Methods such as `DaemonLspGateway::declaration` (`crates/tracedecay-lsp/src/gateway.rs:2377-2599`) have zero callers; the live protocol correctly uses `semantic_request` (`gateway.rs:2649-2681`). Both Plan 35 witnesses agree this parallel façade should be folded in or deleted. | -| A15 | POST-RC | Derived HTTP route documents | `http_route_documents` derives catalog-backed route documentation (`crates/tracedecay-api/src/http.rs:489-532`) but has only a catalog test caller. | -| A16 | POST-RC | Policy source-authorization replay | Exact/recorded/current-best-effort replay exists at `crates/tracedecay-policy/src/replay.rs:17-174`; all callers are policy tests. | -| A17 | POST-RC | Dashboard Work topology accounting read | `dashboard/src/workspaces/work/workTopologyAccounting.ts` explicitly states that its read model is not published; there is no generated contract or route for that advanced accounting sub-surface. | -| A18 | RC-BLOCKING | Dashboard code-index-generation event | `DashboardEventKindV1::CodeIndexGenerationPublished` is explicitly “Declared but unfed” (`crates/tracedecay-dashboard-api/src/events_api.rs:109-112`) and is only constructed in a serialization test. The scheduler's internal publication bus is a different mounted event. | -| A19 | RC-BLOCKING | Plan 24 branch-stack/integration Work surface | Integration observation/contracts exist, but `WorkOperation::ALL` has no Work integration apply/review/stack operation (`crates/tracedecay-api/src/work.rs:62-345`). Native integration exists as a separate family, so it does not mount this Work-context requirement. | -| A20 | RC-BLOCKING | Exact PR-head/manual branch activation | PR-head poll and manual branch-add activation are now wired: bootstrap injects the daemon-owned scheduler into `pr_autotrack`, `track_pr` and `activate_manual_branch_head` prepare a linked worktree then mount it through shared `activate_linked_worktree` (`src/daemon/bootstrap.rs`; `src/daemon/pr_autotrack.rs`; `src/daemon/branch_add.rs`). Public `reconcile_project` and `activate_manual_branch` stay fail-closed because those APIs have no scheduler to inject (`reconciliation_without_scheduler_fails_*`; `manual_branch_without_scheduler_fails_*`). | - -## B. MISSING DELIVERABLES - -| ID | Rank | Consolidated item | Evidence | -|---|---|---|---| -| B1 | RC-BLOCKING | Real `ResetRequired` settlement and final-shape cutover | Project-open does not settle a real reset-required store (`src/daemon/project_open_handshake.rs:229`; ignored journey `tests/typed_terminal_restart_acceptance.rs:351-415`). Live observation schema migration/backfill remains (`crates/tracedecay-global-db/src/observation/schema.rs:67-154,238-372`) and registered `migrate_and_attach*` paths remain (`registered.rs:349-388`), contradicting the final-V2 no-migration rule. | -| B2 | RC-BLOCKING | Typed auxiliary-provider catalog descriptors | The tool catalog has no descriptor model for executable/version/protocol/model/sandbox/approval/stream/resume/fallback evidence; native Claude/Codex provider runtime exists elsewhere but is not catalog-described (`crates/tracedecay-tool-catalog/src/`). | -| B3 | RC-BLOCKING | Workflow activation semantic admission | Activation calls structural `definition.validate()` (`crates/tracedecay-application/src/workflow_coordination.rs:462-480`), but the validator does not validate operation existence, schemas, capabilities, privilege/effect compatibility, or recursive execution (`crates/tracedecay-domain/src/workflow.rs:158-296`). | -| B4 | RC-BLOCKING | Provider discovery negotiated against the pinned configuration snapshot | Configuration exposes the current snapshot but does not perform the Plan 20/32 executable-capability negotiation or typed unavailable-executable decision (`crates/tracedecay-usecases/src/configuration/runtime.rs:466-483`). | -| B5 | RC-BLOCKING | Legacy-data privacy remediation authority | No production owner performs at-rest rescan, quarantine overlay, derivative rebuild, resumable checkpoints, or backup/restore replay of newer deletion/quarantine policy. Privacy-specific Doctor/UI state is consequently absent (`crates/tracedecay-sessions/src/runtime/lcm/raw.rs:463-553` only protects new ingest). | -| B6 | RC-BLOCKING | Saved multi-root collection/default/source-binding resolver | The mounted substrate is `AuthorizedScopeSet`; named `QueryCollection`/`WorkspaceCollection`, optional defaults, and Plan 20 source bindings are absent. Dashboard truthfully reports no mounted multi-root set (`crates/tracedecay-dashboard-api/src/lib.rs:1829-1841`). | -| B7 | RC-BLOCKING | Native-integration Dashboard handoff and LSP notification | CLI/MCP operations are mounted, but no Dashboard consumer or LSP notification exists (`tests/native_integration_surface_mount.rs:82-116`; no matching source under `dashboard/src` or `crates/tracedecay-lsp`). | -| B8 | RC-BLOCKING | Dashboard workflow-definition/run-control journey | HTTP, CLI, MCP, and SDK workflow routes exist, but Dashboard has no `/application/workflow` consumer or workflow definition/run-control UI. | -| B9 | POST-RC | Canonical benchmark/comparison observability events | `BenchmarkRunAttemptedV1`, `BenchmarkRunTerminalV1`, and `BenchmarkComparisonRecordedV1` (or an equivalent production family) do not exist. | -| B10 | POST-RC | Bounded policy exploration evaluator | The policy crate has no allowlist/floor/ceiling/share/rollback/circuit-breaker/propensity evaluator. | -| B11 | POST-RC | Code co-change source and Disagreement field | No file-pair co-change endpoint, provider-attributed source, or rendered disagreement field exists for Plan 11b. | -| B12 | POST-RC | Advanced Work checkpoint/discovery and controls | No Work-specific checkpoint/skill/hint/provider-discovery operation, task-title Command-K provider, expertise/calibration view, or host/LSP handoff control is mounted. | -| B13 | POST-RC | Quantitative secret-detector corpus and calibration | Assessment contracts exist, but no checked-in positive/negative corpus, precision/recall/FP/FN runner, or held-out calibration artifact exists. | -| B14 | POST-RC | Typed external extension framework | Plan 19's typed capability/revision/canonical-operation extension declaration has no production implementation. | -| B15 | POST-RC | Whole-store dashboard history | Current storage telemetry deliberately returns growth/history as unknown until a daemon-owned history exists (`crates/tracedecay-dashboard-api/src/storage_telemetry_api.rs:1-21,146-148`). | -| B16 | RC-BLOCKING | Public SDK bidirectional/enrolled-remote parity | Public SDKs provide local HTTP/SSE and a separate remote-protocol subset, not one generated operation set over an enrolled remote authority; the Plan 35 bidirectional negotiated session and complete two-token handoff journey are absent (`crates/tracedecay-sdk/src/client.rs:27-73`; `remote_client.rs:197-291`). | -| B17 | POST-RC | Multi-worktree artifact reuse and move/delete lifecycle authority | No Plan 16 production authority proves content reuse without logical identity sharing, move-preserved identity, or delete/recreate identity replacement. | - -## C. PARTIAL ITEMS BY WORK THEME - -| ID | Rank | Theme | Remaining coherent slice | -|---|---|---|---| -| P1 | RC-BLOCKING | Typed terminals and stream lifecycle | Core `ResetRequired`/`PartialEffect` validation is strict, but HTTP/MCP/SDK transport paths, SSE resume-expiry/duplicate/disconnect/drop accounting, post-commit cancellation, and reset settlement are not one complete mounted behavior. | -| P2 | RC-BLOCKING | Work and TaskSession | Dashboard positive continuation/rank-final revocation requires an activated federated authority; `Anchor` continuation is unconditionally unavailable (`src/daemon/work_evidence_retrieval.rs:225-230`). `ProfileOwnedNoGit` selection poisoning remains an unresolved owner decision (`NEXT.md:429-437`). | -| P3 | RC-BLOCKING | Multi-root federation | Scope-set CAS, LSP folders, inventory, and generic fanout are mounted, but full Plan 05 fusion/hydration, immutable distributed pagination, per-member coverage, dashboard/CLI parity, and stack-to-Git receipts are incomplete. | -| P4 | RC-BLOCKING | Code-index lifecycle and shared descriptor authority | Markdown structure is admitted but normal `.md` grep visibility remains open (`NEXT.md:582-585`); descriptor-to-analyzer/LSP authority is unproven; rewrite still shells out to host ast-grep; combined cancelled-refresh/restart/branch-split publication and disk-full/concurrent-build paths are incomplete. | -| P5 | RC-BLOCKING | Workflow ownership and control | `admit_workflow_child` internally accepts proposals/admission (`src/daemon/service/invocation/work/workflow_fan_out.rs:583-712`); the durable aggregate lacks a shared deadline/cancellation generation/budget ledger; fairness/no-progress, native approval/EffectUnknown, backup/remote-worker fencing, and registry-derived CLI deadline are incomplete. | -| P6 | RC-BLOCKING | Observability read models and production accounting | Topology metrics/rollup bounds, LSP events, provider pricing provenance, stack observations, review labels, and card-by-card population/coverage parity remain incomplete even where the canonical envelope/read service is mounted. | -| P7 | RC-BLOCKING | Hook/advisory semantics | Hook event-family breadth, revision quarantine, exact debounce/timing, rollback switch, complete failure matrix, Scout feedback delivery, and one shared suggestion channel remain incomplete; advisory successor notices do not make Scout production-ready. | -| P8 | POST-RC | Dashboard workspace product depth | Core routes/pages are mounted, but Sessions replay/raw boundaries, Agents tree/handoff frontier, Knowledge contradictions, Observatory flow/latency, Costs latency, advanced Work controls, CORTEX channels, Loom controls, and full renderer parity remain plan ambitions beyond basic RC usability. | -| P9 | RC-BLOCKING | SDK lifecycle parity | Generated schemas are current, but complete local/enrolled-remote operation parity, handoff failure cases, post-commit cancellation/reconnect, and cross-surface semantic comparison are incomplete. | -| P10 | RC-BLOCKING | Privacy sinks and analyzer isolation | New ingest is sanitized, but full taint propagation and every logs/metrics/API/UI/export/diagnostic sink are not proven; LSP authorized-analyzer and remote capability/disclosure enforcement is incomplete. | -| P11 | RC-BLOCKING | Git/native-integration product contract | Core preview/apply and native owner are mounted, but exact PR thread/comment contract equivalence, checked-out destination variants, approval-to-receipt public journey, failure injection, and Plan 16/LSP-originated selection are incomplete. | -| P12 | RC-BLOCKING | Semantic evaluation, hydration, and rollback | Runtime/vector/query fallback is mounted. `f478c323a` now preserves `SearchEvalError` detail in the typed rejection, but accepted-profile Linux evidence, correction of the surfaced evaluation failure, rollback drill, federated hydration/revocation, and split-store conformance remain incomplete. | -| P13 | RC-BLOCKING | Configuration protected changes and execution snapshot | Direct CAS/configuration is mounted, but protected preview/apply/rollback lacks a production journey; complete provider/work snapshot, mid-attempt no-reread, adapter-default rejection, unsafe-Git combinations, and requested/actual drift views are incomplete. | -| P14 | POST-RC | Policy outcome/replan/calibration | Provider-admission re-evaluation, committed-outcome-driven unapplied replan, complete cohort/horizon/error/drift calibration, self-grading separation, and exploration remain incomplete. | -| P15 | POST-RC | Storage retention operations | Retention is mounted, but the historical orphan backlog is operator-unverified and code-generation retention lacks the separately promised semantic-publication trigger. | -| P16 | POST-RC | Defragmentation and compatibility cleanup | Canonical application routing is real, but release evidence for retained delegates, broad duplicate-wrapper deletion, wildcard parent/child imports, and all ownership-boundary negatives are not complete. | -| P17 | POST-RC | Grafeo breadth after superseded landing plan | Core graph domains are mounted. Remaining historical ambitions are workflow run/attempt/handoff topology, exhaustive old graph-shaped-row deletion proof, aggregate cross-domain rerun, and a pre-Grafeo performance comparison. | - -## D. CONFLICTS ADJUDICATED - -| ID | Rank | Conflict | Ruling | -|---|---|---|---| -| D1 | RECORDED | Scout contracts/hooks “mounted” versus zero selection/owner callers | Controls, address registry, and advisory successor remain mounted; Scout envelope production remains parked. See [A2 ruling](#a2-ruling-2026-08-14). | -| D2 | RC-BLOCKING | Plan 25 duplicate witnesses disagree on Git/diagnostic joins | Broader Git and LSP routes are mounted, but the exact generation join entrypoints are not. Caller/type evidence resolves A4 as unmounted. | -| D3 | POST-RC | One Plan 35 witness reported missing host/feedback mounts; the other found the production route | Current project-open source mounts feedback/advisory and LSP semantic authorities (`src/daemon/project_open_owners/advisory_runtime.rs:291-308,423-628`; LSP protocol uses `semantic_request`). Those product paths are mounted. Only the 19 convenience façades remain unmounted (A14). | -| D4 | RC-REQUIRED-EVIDENCE | Plan 36 witnesses disagree on native approval/fanout mounting | Current source mounts the owner, six operations, exact topology, and coordinator preflight. Approval is structurally mounted; the gap is the public approval-to-receipt/restart journey, not an absent handler. Manual branch activation remains independently unavailable (A20). | -| D5 | RC-BLOCKING | Plan 37 witnesses disagree on PR auto-track and CI localization | Background discovery/stack/advisory CI localization are mounted. Delivery failure localization is intentionally `NotConfigured` and superseded by decision. PR-head poll activation and manual `branch_add` are now scheduler-mounted through the same linked-worktree path. Public reconcile and `activate_manual_branch` without a scheduler remain fail-closed. | -| D6 | RC-BLOCKING | Semantic path called “implemented-unmounted” versus Plan 31 mounted runtime | Runtime, vector publication, and query fallback are mounted. The live acceptance failure is evaluation/activation rejection, not absence of a semantic production caller. P12/E3 govern. | -| D7 | RC-BLOCKING | Plan 27 calls Kimi/OpenCode plugin artifacts mounted; mount census calls live hooks capture-only | Artifact generation/install and handler functions exist, but the pre-main capture fast path prevents live handler dispatch for those command forms. The census's executable-path evidence wins. A12's Codex/Kimi registration slice is recorded separately; this row is the capture-fast-path only. | -| D8 | POST-RC | `task_activity` listed as an unmounted conformance exception | Current daemon code publishes `ActivityFamilyV1::Task` after committed Work mutation (`src/daemon/service/invocation/work.rs:107-123`), and Dashboard subscribes. The exception is stale test/ledger maintenance, not a product gap. | -| D9 | POST-RC | Plan 34 requests read-only LSP rename candidate/preview; Plan 35 explicitly keeps rename unavailable | The current gateway intentionally returns unavailable (`crates/tracedecay-lsp/src/gateway.rs:2683-2693`) and never applies edits. Plan 35 is the more specific current LSP authority; treat Plan 34's read-only rename binding as a post-RC plan-authority reconciliation, not an RC edit-safety defect. | -| D10 | RC-BLOCKING | Plan 19 reports fresh final shape complete; Plan 12 finds live migrations/backfills | Direct current source shows live observation migration/backfill and `migrate_and_attach*`. The narrow fresh-store/read-only paths do not satisfy the universal cutover. B1 governs. | - -## E. EVIDENCE-ONLY GAPS - -These entries require current-tree runs, not new contract inventories. - -| ID | Rank | Required run | Success criterion | -|---|---|---|---| -| E1 | RC-REQUIRED-EVIDENCE | Typed-terminal physical-restart matrix | Drive real `PartialEffect` and `ResetRequired` through HTTP, MCP, Rust SDK, and TypeScript SDK; preserve receipt/legal action across daemon kill/respawn. CLI `PartialEffect` is already proven. | -| E2 | RC-REQUIRED-EVIDENCE | Retained-surfaces restart parity | One real project/profile-store journey covering memory reads/effects, session-refresh begin/status/cancel, LCM retrieval, unavailable families, reconciliation, and CLI/MCP/HTTP/both SDKs. | -| E3 | RC-REQUIRED-EVIDENCE | Plan 15 Linux semantic evaluation/activation | Run the pinned 1x/10x sanitized corpus offline; retain raw resource/quality evidence; produce a diagnosable pass/fail; activate only a passing profile; execute rollback. | -| E4 | RC-REQUIRED-EVIDENCE | Incremental-index lifecycle | Save, rename, delete, ref switch, overflow, cancellation, physical restart, serve-during-refresh, and exact compatible republish in one non-vacuous journey. | -| E5 | RC-REQUIRED-EVIDENCE | Supported-host lifecycle fleet | Fresh binary install/update/repair/Doctor/stock journeys for Claude, Codex, Cursor agents/in-composer, Kimi, Kiro, and OpenCode. Hermes is already closed and must not be rerun as an open gap. | -| E6 | RC-REQUIRED-EVIDENCE | Plan 16 same-name multi-root journey | Same-name repositories, linked worktrees, nested folders, denied sibling, immutable pagination across restart, exact anchors, and CLI/MCP/HTTP/UI/LSP parity. | -| E7 | RC-REQUIRED-EVIDENCE | Plan 36 public native-integration journey | Start from Plan 16/LSP selection; exercise pair and declared edge, checked-out/unoccupied destinations, preview/approval/apply/status/cancel, all three modes, daemon restart, and final native receipt. | -| E8 | RC-REQUIRED-EVIDENCE | Remote Brain multi-machine journey | Offline capture, authority change, transfer/replay/duplicate receipt, query coverage, diagnostics, backup, isolated restore, promotion, and old-authority rejection. | -| E9 | RC-REQUIRED-EVIDENCE | Observability topology/settlement journey | Current-tree execution-topology sampling, fanout/dedupe/drop settlement, rollup, compaction, retry/leak, blocked intervals, cancellation, restart, and cross-transport read parity. | -| E10 | RC-REQUIRED-EVIDENCE | Performance refresh | Same-host release `scripts/perf-gate.sh`; clean Linux session-temporal `--refresh-contract`; Work-rollup latency/throughput. Current CI does not mount perf-gate and no fresh artifacts exist. | -| E11 | RC-REQUIRED-EVIDENCE | Real LSP host clients | Live Claude and OpenCode negotiated lifecycle/navigation/diagnostics/cancel/reconnect plus Cursor native-diagnostic merge and exactly-one-analyzer install/repair/rollback/uninstall. | -| E12 | RC-REQUIRED-EVIDENCE | Basic real-Chrome Dashboard usability | Exercise all published routes at required viewport families, keyboard/focus, reduced motion, fallback rendering, and truthful partial/unavailable states. | -| E13 | RC-REQUIRED-EVIDENCE | Default package/install/start smoke | Fresh package artifact, isolated profile/project, daemon start, installed SDK/client operation, and uninstall/cleanup; npm publication itself remains operator-owned. | -| E14 | RC-REQUIRED-EVIDENCE | Doctor authority audit and Cursor lifecycle | Re-run the Doctor remediation/re-observation journey and clean Cursor agents/in-composer install → version bump → Doctor, preserving drift versus ownership-conflict states. | -| E15 | RC-REQUIRED-EVIDENCE | Final RC aggregate gate | Build Dashboard assets, then non-vacuous workspace all-feature nextest, Dashboard typecheck/tests/build, contract and SDK drift checks, host bundle/stock checks, commitlint, release drift, and packaging smoke. | -| E16 | POST-RC | Manual assistive-technology and usability study | Manual NVDA/VoiceOver and the plan's multi-participant study are not part of the basic RC usability bar. | -| E17 | POST-RC | Oldest-supported/Windows SDK matrix | Installed Rust and TypeScript package combinations against current and oldest supported daemons on Linux/Windows. | -| E18 | POST-RC | Grafeo aggregate/performance provenance | Full cross-domain Grafeo journey plus p50/p95/p99, RSS, bytes, write amplification, and reopen comparison; Plan 39's historical task list is superseded, so this is diagnostic/post-RC evidence. | - -## Recommended next fix dispatches - -Each task is deliberately single-concern. - -1. Make project-open settle one incompatible store as typed `ResetRequired`; - unignore only that exact restart regression. -2. Closed: A2. Do not reconstruct the deleted hook-cycle mount and do not - demolish the parked Scout runtime. Owner chooses remount (restore - `run_production_hook_cycle` + lifecycle + claim authority) or retire - (delete producer entry points and `prepare_controlled` together). -3. Register one generalized GitHub external-source acquisition owner at - project-open; prove one canonical refetch reaches the existing store. -4. Closed: A3. Do not invent a V3 evidence-assembly producer. Plan 23 owns - retriever-contribution publication; Git-topology V3 producers remain the - owning plans in Plan 13's pending-producer inventory. -5. Mount an `EnrolledRemoteClient` node-side capture/transfer scheduler without - changing the authority-side protocol. -6. Route production workflow ready-step execution through - `WorkflowStepExecutionService`, or delete that parallel executor and its - advertised general-step claim. -7. Add pre-activation workflow operation/schema/capability/effect validation - against the executable catalog. -8. Remove the Kimi/OpenCode capture-fast-path bypass so their installed native - events reach the existing live handlers. -9. Produce and persist one conflict-prediction plus linked-outcome observation - from existing Work/native evidence. -10. Run the Plan 15 Linux evaluation and fix the first surfaced - `SearchEvalError`, rather than widening deadlines or weakening the oracle. - -## A2 ruling (2026-08-14) - -Decision: **RECORD-RULING**. The producer mount was deliberately parked, not -abandoned mid-build. **WIRE** and **DELETE** are both rejected without an -owner remount-or-retire choice. A2 stays parked, not silently green. - -### Caller-intent evidence - -- Before `1caf016e5` (`refactor(runtime): complete V2 authority cutovers`, - 2026-08-09) the producer was mounted. `run_production_hook_cycle` in - `src/daemon/project_open_owners.rs` mapped SavedEdit/Stop/Explicit to - `ContextScoutCanonicalInputV1::selection_input` and - `ProjectContextScoutOwnerV1::prepare_configured`, then mounted claim - authority on enqueue. `src/daemon/project_open_owners/scout_journey_tests.rs` - (378 lines) covered that journey. -- `1caf016e5` deleted that mount on purpose: `context_scout_lifecycle.rs` - (−1303), the scout journey tests, and ~2203 lines from - `project_open_owners.rs` including `run_production_hook_cycle`. The same - commit replaced live claim-authority resolution - (`resolve_current_context_scout_claim_authority`) with - `let claim_authority = None` in - `src/mcp/tools/handlers/hook_runtime/admission.rs`. That assignment is - why the ready-guidance branch is unreachable: it is a cutover disable, - not a forgotten `None`. -- Current `admit_hook_orchestration` - (`src/daemon/service/invocation/types.rs`) has no success path. SavedEdit, - session End/TurnComplete, and explicit prepare all return `Unavailable`; - every other event returns `UnsupportedTrigger`. `hook_v2_scout_prepare` - only calls that stub. This is a parking brake, not a missing wire. -- `c2c11956d6` (`feat(advisory): mount production feedback runtime`, - 2026-08-12) mounted the hook-notice successor. Admission now peeks - `peek_advisory_hook_notice`. That surface is distinct from Scout - suggestion envelopes (D1). -- `tracedecay tool callers` on `prepare_configured` - (`method:db3b71de8fa41fdfb4e5aa1f22ab09e9`) and `selection_input` - (`method:a3e64357a89b62413d0774fe9ef8d57b`) returns none. - `bind_and_assemble` and `ContextScoutCanonicalInputAssemblerV1` have no - production callers. `run_production_hook_cycle` no longer exists. - `resolve_current_context_scout_claim_authority` no longer exists. -- NEXT.md has no Scout remount or retire ruling. Plan 22 still describes a - saved-edit/stop envelope journey; a plan ambition is not a mount. - -### Why not WIRE - -The “smallest honest slice” is not a hook-to-`prepare_configured` call. -`selection_input` requires a fully assembled canonical packet (address -registry bind, authority pin, `RequestContext`, lifecycle, committed -publication, candidates). That assembler was fed by the deleted lifecycle -and `run_production_hook_cycle`. Reconstructing those is a product remount, -not a one-call connect. Wiring a thinner envelope from the hook alone -would fabricate Scout evidence. - -### Why not DELETE - -The stated delete set (`prepare_configured`, `selection_input`, dead -assembler ports) is not warning-free. The only non-test caller of -`ContextScoutDurableRuntimeV1::prepare_controlled` is `prepare_configured`. -Removing the entry points leaves the deterministic runtime unused under -`cargo check --lib`. Deleting `prepare_controlled` as well demolishes the -Plan 22 producer while mounted controls, durable store, address registry, -and claim/delivery/feedback remain. That is a retire decision, not a dead- -port cleanup. `allow(dead_code)` is forbidden. - -### Action taken - -- No producer, advisory, hook-cmd, or daemon-reset code was changed. -- Parking is now an explicit register ruling: owner must remount the - deleted hook-cycle/lifecycle/claim-authority path, or retire the - producer entry points together with `prepare_controlled`. -- Advisory/feedback (`c2c11956d6`) remains the live hook-notice successor - and is not a Scout envelope producer. - -## A3 ruling (2026-08-14) - -Decision: **RECORD** the persist/V3-target contract; **DELETE** the unused -`EvidenceAssemblyStore` trait. **WIRE** is rejected. - -### Caller-intent evidence - -- Plan 13 names `PublishEvidenceAssembly::execute` as - `EvidenceAssemblyStore::publish_or_replay` and says Plan 23 emits - `RetrieverContributionRecordV1` after it freezes scope, temporal mode, and - watermarks. That producer does not exist. Plan 23's live - `RetrieverContribution` / `RetrieverContributionV1` types are application - and temporal-query ranking records, not `EvidenceAssemblyWriteV1`. -- The only historical production-shaped caller was - `RuntimeEvidenceAssemblyStore` in `crates/tracedecay-usecases/src/evidence_assembly.rs`. - It shipped under `#![allow(dead_code)]` and was removed in `a2fea0e7a` - (`refactor(evidence): remove unmounted duplicate adapters`). The usecases - seam note kept "canonical store and runtime capabilities" and deleted the - adapter "until a production journey needs them." -- `tracedecay tool callers` on `publish_or_replay` and on - `RepositoryWritePayloadV1::EvidenceAssembly` returns no production - constructor. The only write-payload construction site is - `crates/tracedecay-rusqlite-runtime/src/writer/tests/authority.rs`. -- The trait had **zero implementors**. Real publish-or-replay is - `EvidenceAssemblyExecutor::execute_write` - (`crates/tracedecay-rusqlite-runtime/src/repository/evidence_assembly/mod.rs`), - already covered by `publish_replay_conflict_and_drilldown_are_atomic`. -- Work evidence retrieval uses a different application - `RetrieverContribution`. GitHub stack publication uses the mounted V2 path - in `stack_anchors.rs`. Observation, diagnostic, CI, resolution, tombstone, - and UI anchors are mounted separately. None of those paths publish - equivalent V3 evidence assemblies, so the persist contract is not - superseded-in-place. -- Git-topology V3 targets are already **SANCTIONED-PENDING** in Plan 13 - (2026-08-07 pending-producer inventory). Owning plans: 36/27/37/24/32/16/03. - Plan 13 forbids deleting those targets as unused breadth. - -### Why not WIRE - -No current retrieval, stack, or work path can construct a honest -`EvidenceAssemblyWriteV1` (occurrence set, verified ordering proof, dual -sanitization receipts, catalog binding, retriever contribution). Wiring a -call without that producer would fabricate evidence. - -### Why not delete the persist stack - -Deletion of the unused trait does **not** destroy schema authority. Deletion -of the persist stack would. Other code reads that authority: - -- `EVIDENCE_ASSEMBLY_SCHEMA` / `EVIDENCE_ASSEMBLY_IMMUTABILITY` are part of - the final-shape expected schema - (`crates/tracedecay-runtime-core/src/db/migrations/final_shape.rs`). -- `RepositoryWritePayloadV1::EvidenceAssembly` and - `ProjectReadOperationV1::EvidenceAssembly` are live store-protocol variants - dispatched by rusqlite `execute`. -- `RetrievalAnchorTargetV3::{ExactSourceOccurrence, ExactEvidenceSpan, - RetrieverContribution}` and the Git-topology target family are the - contracts later producers must bind to. - -### Action taken - -- Deleted the unimplemented `EvidenceAssemblyStore` trait and the - trait-only `EvidenceAssemblyPublicationOutcomeV1` enum. -- Left write/read types, rusqlite executor, final-shape tables, and V3 - target contracts in place for the owning plans. - -## A12 ruling (2026-08-14) - -Decision: **RECORD** the empty Codex hook seed and Kimi's missing global-hook -form; **WIRE** Codex Core activation through `codex plugin add` / `remove`. -Filling `hooks-codex.json` or inventing a Kimi global-hook file is rejected. - -### (a) Codex `hooks-codex.json` is an empty `{}` - -- The source seed at `plugin/hooks/hooks-codex.json` is an empty `hooks` - object by design. `plugin/README.md` and - `codex_plugin_hooks_fills_empty_seed_and_preserves_strict_schema` pin that - the global renderer mutates the seed in place from `CODEX_MANAGED_HOOKS`. -- Handlers exist and are already registered at install time: - `SessionStart` → `hook-codex-session-start`, `UserPromptSubmit` → - `hook-codex-user-prompt-submit`, plus SubagentStart / PostToolUse / - PostCompact / Stop. Repo-local bundles ship no hooks - (`CodexBundlePolicy::include_hooks` is Global-only). -- Codex honors plugin `hooks/hooks.json` after `codex plugin add`. It does - **not** honor a TraceDecay-authored `~/.codex/hooks.json` or forged - `[hooks.state]` trust hashes. Isolated-HOME probe of Codex CLI 0.147.0 - (`codex plugin add tracedecay@personal --json`) wrote activation only; - hook trust stayed empty. -- Filling the source seed would duplicate `CODEX_MANAGED_HOOKS`, break the - empty-seed contract, and leak hooks into repo-local bundles. - -### (b) Kimi has no global-hook form - -- Re-probed `kimi --help` on 2026-08-14: command set is still - `export, provider, acp, web, server, login, doctor, vis, migrate, upgrade`. - No `mcp`, `plugin`, or `hooks` subcommand. Matches the 2026-08-08 Plan 27 - MANUAL-ONLY (a) verdict and `kimi.rs` module ruling. -- Plugin-manifest hooks (`PostToolUse` + `Stop`) are already rendered by - `render_kimi_hook_commands` into `.kimi-plugin/plugin.json`. Those become - live only after the operator runs interactive `/plugins install `. -- There is no documented Kimi global hooks.json, settings hook table, or - non-interactive registration command. Inventing `hooks-kimi.json` would - be a staged half-form the host cannot load. - -### (c) Codex Core plugin activation - -- Plan 27's 2026-08-08 `(a)` verdict is reopened. Codex CLI 0.147.0 - publishes non-interactive `codex plugin add` / `remove` / `list` / - `marketplace`. Isolated-HOME evidence: add exits 0 without a TTY, writes - `[plugins."tracedecay@personal"] enabled = true`, and copies the staged - source into `~/.codex/plugins/cache/personal/tracedecay/`. -- `activate_deployed_host_registration` now drives that CLI (same - host-capability pattern as `codex mcp add`). - `interactive_activation_guidance` is `None` so the catalog transaction - actually calls activate instead of returning `UnsupportedCapability`. -- Hook trust remains interactive (`/hooks`). Doctor reports it; TraceDecay - still never authors `[hooks.state]`. - -### Why not WIRE (a) or (b) - -The host either already fills the registration (Codex global renderer) or -has no registration surface (Kimi). Wiring a file the host does not read -would be a staged half-form. - -### Action taken - -- Added `agents/codex/plugin_registry.rs` and wired Core activate/deactivate - / prepare / update through `codex plugin add` / `remove`. -- Left `hooks-codex.json` as the empty seed; left Kimi plugin-manifest - hooks and interactive `/plugins` deferral in place. -- Kiro prompt-boundary and Kimi/OpenCode capture-fast-path stay on D7 / - their own items; they were not this slice. diff --git a/docs/superpowers/plans/2026-07-31-one-shot-crate-split.md b/docs/superpowers/plans/2026-07-31-one-shot-crate-split.md deleted file mode 100644 index fd1b5a6d0d..0000000000 --- a/docs/superpowers/plans/2026-07-31-one-shot-crate-split.md +++ /dev/null @@ -1,94 +0,0 @@ -# One-Shot Crate Split (owner decision 2026-07-31) - -> **SUPERSEDED BY LANDING (2026-08-14).** The target map below shipped: -> `tracedecay-agent-hosts`, `tracedecay-application`, `tracedecay-dashboard-api`, -> `tracedecay-global-db`, `tracedecay-runtime-core`, `tracedecay-sessions`, and -> `tracedecay-usecases` are all workspace members with clean -> `cargo check --workspace`; `tracedecay-migrate` was deleted outright -> (`923816ed3`) rather than landed, folding its surface into `global-db` and -> `runtime-core`. The "scar cleanup" item this plan called for — delete each -> mover's `SEAMS.md` as its rows resolve — is done: all seven were retired, -> with their few still-durable contracts (fail-open/fail-closed port -> semantics, dependency/forbidden-edge proofs, sealed benchmark provenance, -> the `tracedecay-application`-vs-`tracedecay-usecases` layer-naming split) -> folded into each crate's `lib.rs` module doc. Treat the target map and -> execution-model sections below as historical planning, not open work. - -Supersedes the phased breakup plans (deleted). Owner rulings: **no phases** — -one mass move of all root subsystems into workspace crates; **breakage during -the move is acceptable** ("move all crate code first, then deal with the -aftermath once the builds will no longer be slow"); validation is the whole -product end to end, not per-move gates. - -## Why - -`src/` is ~700K lines in one crate — every edit recompiles a 1.3 GB rlib that -~56 test binaries relink. Measured duplication with the 21 existing crates is -~0.9%; the mass is genuinely unsplit subsystems. Only moving them out shrinks -the serial build tail. - -## Target map (one landing) - -| From src/ | To crate | Notes | -|---|---|---| -| semantic_code | tracedecay-semantic | DONE (landed) | -| search_eval | tracedecay-search-eval (new) | in flight; bins stay root | -| mcp/core_hooks.rs | tracedecay-hooks | in flight (de-knot) | -| mcp/transport.rs | tracedecay-jsonrpc (new) | in flight (de-knot) | -| mcp CodeIndexSearch types | tracedecay-query | in flight (de-knot) | -| DaemonInvocation* types | tracedecay-application | in flight (de-knot) + ratchet guard | -| sessions/ | tracedecay-sessions (exists, façade) | mover assigned | -| migrate/ | tracedecay-migrate (exists) | mover assigned | -| global_db/ | tracedecay-global-db (new) | mover assigned | -| agents/ + automation/ | tracedecay-agent-hosts (new) | move together (mutual recursion); embedded assets need crate-local build.rs/paths | -| dashboard/ (minus assets.rs) | tracedecay-dashboard-api (new) | assets.rs stays (OUT_DIR embed) | -| errors, types, timeutil, storage, db, store, memory, sync, git, worktree, branch_meta, lifecycle_lease, sqlite_read_snapshot, path_scope, privacy, redundancy, runtime_identity, serde_util, text, os_str_bytes, windows_file, open_store_holders | tracedecay-runtime-core (new) | kernel; verifier report refines composition | -| mcp/ (rest) | tracedecay-mcp (new) | after de-knot lands | -| daemon/ + daemon.rs | tracedecay-daemon (new) | after de-knot lands | -| application/ (rest) + application_surface | tracedecay-application / tracedecay-api | after de-knot lands | - -Root keeps: main.rs, cli*, commands/, *_cmd.rs adapters, bin/, config, -dashboard/assets.rs, hooks/ daemon-side handlers (cycle with daemon), and thin -`pub use` shims for every moved path so tests/ imports survive. - -## Execution model - -- One mover agent per subsystem in an isolated worktree; `git mv` whole - modules; shim files at old paths; each mover compiles only ITS crate - (best-effort) — a red root is acceptable and expected mid-landing. -- Lead octopus-merges all mover branches into the split landing, resolves - Cargo.toml/lock unions, then runs the single mass fix-to-green campaign - (fleet of fixer agents on compile errors, then whole-product validation: - build, isolated release validation, CI). -- Cycle edges break by moving shared pure-data types DOWN (never by adding - upward deps). The architecture ratchet tests (compile_isolation + the new - mcp/daemon direction guards) are the only per-move gates that must stay - green at the END of the landing. - -## Aftermath queue (fix after the move) - -Compile errors from split seams (each mover's SEAMS.md is the work order); -feature forwarding (production, test-transport, semantic-fastembed, -lite/full); embedded asset paths (include_bytes/include_str) in agent-hosts; -scripts that name old paths (check-distribution-acceptance.sh); doc -references. All adjudicated by the whole-product gate, not per-crate -ceremony. - -### Test relocation and import taming (owner order 2026-07-31) - -- **Tests move with their subjects.** Unit tests live in the crate that owns - the code; root integration tests that exercise one crate's surface migrate - into that crate's tests/ (with their fixtures, fixing the cargo-package - escapes the movers cataloged). The ~56 root test binaries shrink to the - cross-crate journeys only — that is where the relink win lives. -- **Tame the imports.** The root shims (`pub use tracedecay_x::*`) are - transitional scaffolding, not architecture. After green, ast-grep sweeps - repoint root and test call sites from shim paths (`crate::sessions::X`) - to direct crate imports (`tracedecay_sessions::X`), then the glob shims - shrink and die. No permanent double-path for the same item. -- **Scar cleanup once settled.** Delete each SEAMS.md as its rows resolve; - re-seal the benchmark manifests whose digests pinned old harness paths; - update docs citing src/ paths; audit the mass pub-widenings (613 sessions, - 491 dashboard, 15 agent-hosts) and narrow anything no external caller - names; remove the `// SEAM(...)` markers; drop root Cargo.toml include - entries that moved with their crates. diff --git a/docs/superpowers/plans/2026-08-01-test-support-features.md b/docs/superpowers/plans/2026-08-01-test-support-features.md index 21160faad1..9c3e383e3a 100644 --- a/docs/superpowers/plans/2026-08-01-test-support-features.md +++ b/docs/superpowers/plans/2026-08-01-test-support-features.md @@ -1,8 +1,8 @@ # Test-support features for the crate split (2026-08-01) -Companion to `2026-07-31-one-shot-crate-split.md`. That plan moved subsystems -out of `src/` and accepted breakage; lib targets converged first. This plan -covers the **`--all-targets` aftermath**: upstream test-only surfaces are +The one-shot crate split moved subsystems out of `src/` and accepted breakage; +lib targets converged first. This note covers the **`--all-targets` aftermath**: +upstream test-only surfaces are `#[cfg(test)]`-gated, so they vanish at the crate boundary and every downstream crate's `lib test` target fails to resolve them. diff --git a/docs/superpowers/plans/2026-08-08-v2-rc-recovery.md b/docs/superpowers/plans/2026-08-08-v2-rc-recovery.md deleted file mode 100644 index c706f53e81..0000000000 --- a/docs/superpowers/plans/2026-08-08-v2-rc-recovery.md +++ /dev/null @@ -1,454 +0,0 @@ -# TraceDecay V2 RC Recovery Implementation Plan - -> **RECONCILED INTO NEXT.md (2026-08-13).** This recovery plan's checkboxes -> were never ticked here; its live remainder is carried by -> `docs/plans/tracedecay-v2/NEXT.md` ("Remaining work by lane"), which has -> been kept current through 2026-08-13. Do not treat the 55 unchecked boxes -> below as independently open work — consult NEXT.md for what actually -> remains. - -**Goal:** Convert the interrupted Claude checkout into a fully wired, truthfully typed V2 release-candidate branch with direct production-journey and aggregate test evidence. - -**Architecture:** Preserve the canonical Rust domain/application authorities, finish one production vertical at a time, and derive MCP, HTTP, SDK, host, and dashboard surfaces from those authorities. Stabilize replay, identity, security, and typed-state boundaries first; mount runtime producers and consumers next; regenerate wire artifacts only after Rust contracts stop moving. - -**Tech Stack:** Rust 2024 workspace, rusqlite, axum/HTTP, MCP, TypeScript SDK, React/rsbuild, Vitest, schemars contract generation, GitHub Actions. - -## Global Constraints - -- `docs/plans/tracedecay-v2/00-plan-set-index.md` is the sole roadmap and acceptance authority; `NEXT.md` records current outcomes only. -- Preserve the two pre-recovery local feature commits and all peer-owned dirty files until their owning task either integrates or deliberately removes them. -- Complete every cutover in one delivery slice; do not add compatibility aliases for branch-local or unreleased shapes. -- A capability is available only when a production caller can exercise it; otherwise return its existing typed unavailable/unsupported/denied state. -- Add no production `unwrap`, `expect`, `panic`, silent fallback, fabricated timestamp/default, empty success, swallowed error, dead-code allowance, or test-only production port. -- No new hand-written file may exceed 1,000 lines, and a touched oversized file must not grow; extract a cohesive responsibility where safe. -- Canonical Rust schemas generate dashboard and SDK wire types. Never hand-edit `dashboard/src/contracts/generated.ts`. -- Every behavioral change uses RED → GREEN → REFACTOR and records the focused failing and passing commands. Generated output uses generator drift checks instead of hand-written unit tests. -- Named libtest runs must be non-vacuous; use full module paths or `scripts/require-exact-test.sh` and confirm the executed count is non-zero. -- Preserve byte-exact identity, replay, staleness, denial, isolation, cancellation, and rollback contracts; do not weaken assertions or raise timeouts to mask defects. -- Audit all supported host integrations whenever shared host lifecycle behavior changes. -- Workers share one dirty checkout: re-read before editing, own only named files, never revert peer edits, stage explicit paths, and create coherent conventional commits. -- npm OIDC, live operator-host runs, Plan 15 semantic evaluation, Plan 25 cadence re-observation, and Plan 38 large-store observation are external evidence gates, not permission to fabricate repository evidence. -- Kiro scope is limited to completing and verifying Claude's current CLI-lifecycle changes; do not redesign it as a Power, OpenVSX extension, MCP-only adapter, or new shared bundle system. -- Manual screen-reader polish is not an RC blocker; functional keyboard/DOM behavior and existing automated accessibility checks remain in scope. - ---- - -### Task 1: Make work synthesis replay atomic and byte-stable - -**Files:** -- Modify: `crates/tracedecay-application/src/work_synthesis.rs` -- Modify: `crates/tracedecay-application/src/work_attempt.rs` -- Modify: `crates/tracedecay-application/tests/work_synthesis_service.rs` -- Modify: `crates/tracedecay-rusqlite-runtime/src/work_attempt.rs` -- Modify: `crates/tracedecay-rusqlite-runtime/tests/work_attempt_storage.rs` - -**Interfaces:** -- Consumes: existing synthesis admission/source/draft authorities and work-attempt start port. -- Produces: one durably persisted admitted synthesis result that identical replays return byte-for-byte; changed replay material yields the existing typed conflict. - -- [ ] **Step 1: Write the failing replay test** - -Add a real service/storage test that admits synthesis while one source is `Unknown`, changes that source to `Succeeded`, repeats the identical request, and asserts the second result is byte-identical to the first. Add a second test that changes request identity material and asserts the typed conflict without mutation. - -```rust -let first = service.synthesize(request.clone()).await?; -sources.mark_succeeded(source_id).await?; -let replay = service.synthesize(request.clone()).await?; -assert_eq!(serde_json::to_vec(&replay)?, serde_json::to_vec(&first)?); -assert_eq!(store.committed_result_count(run_id).await?, 1); -``` - -- [ ] **Step 2: Verify RED** - -Run the narrow application test with `scripts/require-exact-test.sh`; expect the replay assertion to fail because source/draft data is recomputed. - -- [ ] **Step 3: Persist complete admission atomically** - -Move replay authority to the durable synthesis record. Store request identity plus the complete returned result in the same transaction that creates the admitted attempt. On duplicate identity return the stored result; on mismatched identity return a typed conflict. Do not re-read mutable sources during replay. - -- [ ] **Step 4: Verify GREEN and regressions** - -Run the focused application test, the selected rusqlite adapter tests, and all work-synthesis tests; require non-zero passing counts and pristine output. - -- [ ] **Step 5: Commit** - -Commit only the synthesis service, port/adapter, and tests as `fix(work): make synthesis replay byte-stable`. - -### Task 2: Bind run admission and provider execution to admitted authority - -**Files:** -- Modify: `crates/tracedecay-rusqlite-runtime/src/work_run_control.rs` -- Modify: `crates/tracedecay-rusqlite-runtime/tests/work_run_control_storage.rs` -- Modify: `crates/tracedecay-domain/src/work_execution_snapshot.rs` -- Modify: `crates/tracedecay-application/src/work_attempt.rs` -- Modify: `crates/tracedecay-application/tests/work_attempt_service.rs` -- Modify: `src/daemon/service/invocation/work.rs` -- Modify: `src/daemon/service/invocation/work_attempt_exec.rs` -- Modify: `src/daemon/service/invocation/work_attempt_exec/tests.rs` - -**Interfaces:** -- Consumes: registered workflow/application topology and daemon environment snapshot authorities. -- Produces: first-admission run deadline/topology persistence, mismatch refusal for every later attempt, and an environment-cleared provider child restored only from admitted variables. - -- [ ] **Step 1: Write three failing tests** - -Add literal behavior tests for: attempt `attempt-2` admitted with deadline D1 followed by `attempt-10` with D2 returns typed conflict and leaves D1 intact; caller-supplied topology that differs from registered topology is refused before provider launch; and a fake child observes an admitted sentinel while an ambient secret is absent. - -```rust -assert_eq!(admit("attempt-2", d1)?.run_deadline, d1); -assert!(matches!(admit("attempt-10", d2), Err(RunControlError::AdmissionConflict { .. }))); -assert_eq!(read_run(run_id)?.deadline, d1); -``` - -- [ ] **Step 2: Verify RED** - -Run the exact rusqlite run-control test, application attempt test, and daemon child-process test; expect deadline/topology/environment assertions to fail for their intended reasons. - -- [ ] **Step 3: Implement durable admission and clean spawning** - -Persist run deadline and topology identity on first admission under the existing transaction/CAS boundary. Compare every later admission to those stored values. Resolve topology from registered authority before `WorkAttemptService::start`. Call `env_clear()` for provider children and add only the admitted snapshot plus unavoidable platform process variables explicitly selected by the existing host policy. - -- [ ] **Step 4: Verify GREEN** - -Run all three focused families, then `cargo check -p tracedecay --lib --tests --all-features`. - -- [ ] **Step 5: Commit** - -Commit as `fix(work): bind attempts to admitted run authority`. - -### Task 3: Replace fabricated operational states with typed truth - -**Files:** -- Modify: `src/tracedecay/lifecycle/mod.rs` -- Modify: `src/daemon/project_open_handshake.rs` -- Modify: `src/daemon/core_doctor.rs` -- Modify: `crates/tracedecay-global-db/src/session_temporal/operations/sources.rs` -- Modify: `crates/tracedecay-global-db/src/session_temporal/operations/message_anchor.rs` -- Modify: `crates/tracedecay-global-db/src/session_temporal/retrieval/records/relations.rs` -- Modify: `crates/tracedecay-global-db/src/session_temporal/retrieval/tests/relation_graph_tests.rs` -- Test: lifecycle, Doctor, session-temporal, and graph projection suites - -**Interfaces:** -- Consumes: canonical store schema, metadata, owner-store hydration, and generation authorities. -- Produces: typed reset-required/unobserved/unavailable/absent-owner/stale-generation outcomes without synthetic schema, size, timestamp, or anchor values. - -- [ ] **Step 1: Write failing boundary tests** - -Cover: a nonempty wrong-schema project open returns the reset-required typed state before normal I/O; a route-live Doctor request with failed metadata/integrity observation returns unavailable fields rather than compiled schema or size `0`; missing message ownership and malformed timestamps do not insert a legacy anchor; stale graph generation cannot satisfy a current read. - -```rust -assert!(matches!(open_result, Err(ProjectOpenError::ResetRequired { .. }))); -assert_eq!(doctor.database.observed_size_bytes, None); -assert!(matches!(source_result, Err(SourceError::OwnerUnavailable { .. }))); -``` - -- [ ] **Step 2: Verify RED** - -Run one exact test in each family and confirm each fails on the fabricated current behavior. - -- [ ] **Step 3: Route through canonical observations** - -Remove defaulting branches. Propagate typed states through daemon/API serialization. Hydrate temporal sources only through each message's owning store and parse authoritative timestamps. Require the read generation to match the canonical current generation. - -- [ ] **Step 4: Verify GREEN** - -Run the full lifecycle/Doctor/session-temporal/relation-graph focused suites and their crate checks. - -- [ ] **Step 5: Commit** - -Commit as `fix(runtime): preserve truthful operational states`. - -### Task 4: Mount the complete Work MCP surface - -**Files:** -- Modify: `src/mcp/tools/definitions.rs` -- Integrate: `src/mcp/tools/definitions/work.rs` -- Modify: `src/mcp/tools/binding.rs` -- Modify: `src/mcp/tools/handlers/mod.rs` -- Create: `src/mcp/tools/handlers/work.rs` -- Modify: `src/application_surface.rs` -- Modify: `crates/tracedecay-application/src/work_catalog.rs` -- Modify: `plugin/README-cursor.md` -- Test: MCP definition, binding, dispatch, API catalog, and live daemon Work tests - -**Interfaces:** -- Consumes: canonical `work_executable_binding_registry` and existing application Work dispatcher. -- Produces: exactly 26 advertised Work tools with definitions, lifecycle annotations, bindings, deadlines, dispatch handlers, typed results, and discovery parity; exactly 11 read-only operations including `Topology`. - -- [ ] **Step 1: Write failing registry and live-dispatch tests** - -Extend existing maximal-registry, every-definition-has-binding, every-binding-has-dispatch, and read-only tests to include the 26 canonical Work operations. Add a live MCP test that invokes one read and one mutation through the daemon and observes the same typed Work result as HTTP. - -```rust -assert_eq!(work_definitions.len(), 26); -assert_eq!(work_definitions.iter().filter(|d| d.read_only).count(), 11); -assert_eq!(mcp_result, http_result); -``` - -- [ ] **Step 2: Verify RED** - -Run the exact catalog/definition/binding tests; expect missing module, binding, or dispatch coverage rather than a compile-only failure. - -- [ ] **Step 3: Derive Work mounting from the canonical registry** - -Register definitions, add a distinct Work dispatch group or reusable Work adapter, and route all tools through the existing application owner. Do not duplicate request/result DTOs or maintain a second operation list. - -- [ ] **Step 4: Verify GREEN** - -Run all MCP tool-definition/binding/handler tests, API application parity, runtime surface acceptance, and the live Work MCP/HTTP journey. - -- [ ] **Step 5: Commit** - -Commit as `feat(mcp): mount the canonical Work surface`. - -### Task 5: Finish worktree, fan-out, checkpoint, and handoff runtime journeys - -**Files:** -- Integrate/refactor: `crates/tracedecay-application/src/worktree_catalog.rs` -- Integrate/refactor: `crates/tracedecay-application/src/worktree_inventory.rs` -- Integrate/refactor: `crates/tracedecay-application/src/worktree_cleanup.rs` -- Integrate/refactor: `crates/tracedecay-application/src/workflow_fan_out.rs` -- Integrate or delete: `crates/tracedecay-domain/src/work_checkpoint.rs` -- Modify: `src/daemon/service/invocation/handoff.rs` -- Modify: `src/daemon/service/invocation/work.rs` -- Modify: `src/application_surface.rs` -- Modify: `crates/tracedecay-application/src/work_catalog.rs` -- Test: application service tests and `tests/work_loop_journey.rs` - -**Interfaces:** -- Consumes: registered project/worktree identity, workflow run-control fence, provider placement, handoff token, and durable Work storage. -- Produces: explicit-root inventory/cleanup, fenced bounded fan-out, durable checkpoint retrieval/handoff when justified, and task-token redemption. - -- [ ] **Step 1: Split oversized dirty modules before adding behavior** - -Extract DTO validation, inventory projection, cleanup planning, and cleanup execution into focused modules so no new hand-written file exceeds 1,000 lines. Replace static-constructor `unwrap`/`expect` with typed initialization results. - -- [ ] **Step 2: Write failing real-journey tests** - -Add one daemon journey covering explicit roots, present/stale/partial/foreign inventory, cleanup denial, inspect→confirm→remove→reconcile, bounded fan-out where observed concurrent children never exceed `max_parallel`, lease/fence loss, restart/replay, and task handoff redemption. Assert stale+present coverage is partial, not complete. - -```rust -assert_eq!(inventory.coverage.completeness, Completeness::Partial); -assert!(max_observed_children <= request.topology.max_parallel); -assert_eq!(redeemed.task_id, issued.task_id); -``` - -- [ ] **Step 3: Verify RED** - -Run exact service tests and the selected work-loop journey; expect unmounted adapter, missing bound, stale coverage, and unavailable handoff failures. - -- [ ] **Step 4: Mount canonical adapters** - -Implement real filesystem/Git worktree adapters with registered identity, use run-control for fan-out admission and concurrency, validate request fences and placement, and make checkpoint state durable only if the handoff/retrieval journey consumes it. If no production consumer is justified by the V2 authority, delete the checkpoint contract and tests instead of staging dead code. - -- [ ] **Step 5: Verify GREEN and commit** - -Run application, rusqlite, daemon Work, host handoff, and work-loop journeys. Commit as `feat(work): complete workflow and worktree journeys`. - -### Task 6: Emit and project execution topology through one generation - -**Files:** -- Refactor/integrate: `crates/tracedecay-application/src/execution_topology_metrics.rs` -- Modify: canonical run/attempt/fan-out/handoff event producers -- Modify: canonical observability persistence/query adapter -- Modify: Work topology HTTP/application catalog route -- Test: application projector and live topology route tests - -**Interfaces:** -- Consumes: 11 canonical execution-topology event kinds from actual owner transitions. -- Produces: persisted, scope-isolated, generation-bound metrics/read model with explicit unknown/partial coverage. - -- [ ] **Step 1: Split the 2,500-line dirty projector** - -Separate event normalization, aggregation, pagination, and public projection into focused modules under 1,000 lines without changing behavior. - -- [ ] **Step 2: Write failing emission and read-model tests** - -Exercise a real provider run with fan-out/retry/handoff and assert expected event kinds are emitted exactly once, the route rejects a mismatched `scope_ref`, two source generations cannot be joined, and unavailable storage returns a typed unavailable state rather than empty metrics. - -- [ ] **Step 3: Verify RED** - -Run the exact application projector and daemon route tests; expect no production emission/adapter/route. - -- [ ] **Step 4: Mount producers and query authority** - -Emit at owner transitions, persist through the canonical observability store, bind one generation into the query envelope and payload, and expose one application/HTTP Work topology read. - -- [ ] **Step 5: Verify GREEN and commit** - -Run focused projection, route, scope-isolation, and work-loop tests. Commit as `feat(observability): publish execution topology`. - -### Task 7: Complete retained context, Context Scout, policy replay, and remote surfaces - -**Files:** -- Modify: `crates/tracedecay-application/src/context_scout.rs` and its production executor/bindings -- Modify: retained application services and root MCP/CLI callers -- Modify: `crates/tracedecay-policy/src/replay.rs` and its production authorization caller -- Modify: remote operation catalog, CLI, MCP, Rust SDK, TypeScript SDK schema authority, and dashboard route ownership -- Modify: `crates/tracedecay-application/src/sdk_catalog.rs` -- Modify: `crates/tracedecay-sdk/src/operations.rs` through its generator -- Modify: `sdks/typescript/src/operations.ts` through its generator -- Test: API/MCP/CLI parity plus retained, Scout, policy, remote, handoff, and multi-root journeys - -**Interfaces:** -- Consumes: canonical retained-content hydration/redaction, source authorization, remote protocol, and mounted application registries. -- Produces: equivalent typed behavior through CLI/MCP/HTTP/SDK/dashboard and accurate SDK availability for all promised V2 operations. - -- [ ] **Step 1: Write failing production-parity tests** - -Cover: the same retained fact and temporal message through CLI/MCP/HTTP/dashboard; Context Scout saved-edit→stop→restart/dedupe/overlay; replay authorization exact/recorded/best-effort unavailable input without mutation; remote offline→replay→backup→restore→fenced failover; SDK reachability for Work, Handoff, Multi-root, Scout, retained, and remote operations. - -- [ ] **Step 2: Verify RED** - -Run each narrow journey and record missing executors/routes or truthful unavailable states. - -- [ ] **Step 3: Mount canonical services** - -Replace direct root handlers with typed application services, mount Scout through durable runtime receipts, call replay authorization from the production authorization path, and add transport adapters over the existing remote protocol. For each of the 38 currently unavailable SDK entries, either mount the canonical request/result schema and production executor or retain a typed unavailable entry with a specific roadmap-sanctioned reason; do not resurrect superseded legacy aliases. - -- [ ] **Step 4: Verify GREEN and commit** - -Run application/API parity, MCP, CLI, SDK generation/conformance, retained, Scout, policy, remote, handoff, and multi-root tests. Commit as `feat(application): complete V2 service parity`. - -### Task 8: Harden shared host lifecycle, privacy, and LSP advisory behavior - -**Files:** -- Modify: shared host CLI runner and `crates/tracedecay-agent-hosts/src/agents/kiro.rs` -- Modify: `crates/tracedecay-agent-hosts/src/agents/kiro/tests.rs` -- Modify: `docs/KIRO-INTEGRATION.md` -- Refactor/modify: `crates/tracedecay-runtime-core/src/privacy/detect.rs` -- Integrate: `crates/tracedecay-runtime-core/src/privacy/structured_text.rs` -- Modify: GitHub/fact/session metadata sinks -- Modify: `crates/tracedecay-lsp/src/capabilities.rs`, `context.rs`, `gateway.rs`, protocol controller, and daemon LSP source -- Test: all-host lifecycle, privacy ingress, and LSP saved-edit journeys - -**Interfaces:** -- Consumes: shared declarative host bundle install/update/uninstall contract, structured sanitizer, and daemon projection authority. -- Produces: operator-state isolation and rollback for every host, a safe completion of Claude's current Kiro CLI lifecycle, structured metadata privacy, real advisory registrations, and distinct absent/unsupported/denied/scope-denied states. - -- [ ] **Step 1: Write failing host tests** - -Use an isolated HOME plus an ambient operator `KIRO_HOME`; assert the operator sentinel is unchanged, peer MCP entries survive install/update/uninstall, CLI failure rolls back byte-for-byte, and the child runs in the admitted working directory/environment. Apply the shared preservation assertions to Claude, Codex, Cursor, Kimi, OpenCode, and every supported host target. - -- [ ] **Step 2: Write failing privacy and LSP tests** - -Ingress nested JSON/YAML-like provider metadata containing `vault_passphrase` through real GitHub, fact, and session sinks and assert sanitized/quarantined output. Exercise saved-edit→LSP advisory projection/clear/authorized expansion and assert negotiated-but-denied maps to a denial reason distinct from `CapabilityNotNegotiated`. - -- [ ] **Step 3: Verify RED** - -Run focused host, privacy, and LSP tests and confirm the expected isolation, leakage, registration, and typed-state failures. - -- [ ] **Step 4: Implement shared boundaries** - -Complete Claude's dirty `kiro-cli mcp add/remove` path without changing the integration model: clear the child environment, restore only admitted variables, set the admitted working directory, preserve peer configuration, and use the existing host transaction/rollback authority. Do not add a Kiro Power, OpenVSX extension, MCP-only rewrite, or new bundle layer. Parse before sanitizing provider metadata, and derive LSP registrations/snapshots from production daemon sources. Extract cohesive privacy/Kiro/LSP responsibilities rather than growing files already above 1,000 lines. - -- [ ] **Step 5: Verify GREEN and commit** - -Run every supported host's lifecycle/bundle tests, runtime privacy suite, LSP crate tests, and the saved-edit integration journey. Commit as `fix(hosts): preserve isolated V2 integration state`. - -### Task 9: Regenerate contracts and mount the final dashboard V2 surface - -**Files:** -- Modify generator authority: `crates/tracedecay-dashboard-api/src/contract_schema.rs` -- Generate: `dashboard/codegen/schemas/dashboard-contracts.schema.json` -- Generate: `dashboard/src/contracts/generated.ts` -- Generate: `crates/tracedecay-sdk/src/operations.rs` -- Generate: `sdks/typescript/src/operations.ts` -- Modify: `dashboard/src/test/workAttemptFixture.ts` -- Integrate: recovered Observatory adoption/outcome/retrieval/family-ledger files -- Integrate: Work topology accounting files and routes -- Modify: dashboard navigation/page owners and DOM tests - -**Interfaces:** -- Consumes: stable canonical Rust Work/topology/retained/remote schemas and one generation-bound topology route. -- Produces: drift-free generated contracts and reachable dashboard views whose partial/unknown semantics match backend coverage. - -- [ ] **Step 1: Add failing dashboard behavior tests** - -Add DOM tests that navigate to each recovered Observatory view and Work topology accounting, render real generated fixture shapes, refuse mixed-generation joins, and display a partial/floor label when a denominator is capped. - -- [ ] **Step 2: Verify RED** - -Run the targeted Vitest files; expect unmounted views, obsolete topology fixture shape, missing generated route/types, and mixed-generation behavior. - -- [ ] **Step 3: Export and regenerate canonical artifacts** - -Register every mounted dashboard contract including the complete synthesis result and execution topology view. Run `npm run contracts:generate`, SDK generation, and formatters. Update fixtures to embed `topology: WorkTopologyPolicyV1`; never hand-edit generated outputs. - -- [ ] **Step 4: Mount views and enforce snapshot semantics** - -Route recovered Observatory and Work components through the existing workspace owners, bind every joined query to one generation, and make capped denominators explicitly partial. Keep automated user-visible behavior in scope; do not add manual screen-reader release bureaucracy. - -- [ ] **Step 5: Verify GREEN and commit** - -Run `npm run contracts:check`, `npm run typecheck`, targeted tests, full `npm test`, and `npm run build` from `dashboard/`; run SDK conformance from the repository root. Commit as `feat(dashboard): complete the V2 product surface`. - -### Task 10: Complete architectural cutovers and remove dead scaffolding - -**Files:** -- Remove after migration: `src/application.rs` -- Modify: every remaining `crate::application::` caller -- Move root HTTP/SPA ownership into `crates/tracedecay-api` where required by Plan 10 -- Refactor touched oversized modules identified by `scripts/check-handwritten-file-size.sh` or repository equivalent -- Remove stale flags, aliases, unmounted declarations, source-shape gates, and docs contradicted by production behavior -- Test: API ownership, SSE resume/terminal, package/bundle, and repository hygiene checks - -**Interfaces:** -- Consumes: the mounted services and stable contracts from Tasks 1–9. -- Produces: one application/API authority without root compatibility façades or dead branch-local contracts. - -- [ ] **Step 1: Write failing ownership tests** - -Exercise API-owned `/`, static assets/cache, API boundary, and SSE resume/terminal behavior. Add behavior tests at the first real consumer for each migrated root caller; do not add source-string scans that merely assert a file or symbol is absent. - -- [ ] **Step 2: Verify RED** - -Run API/dashboard route tests and confirm ownership remains in the root shim where the plan requires the crate boundary. - -- [ ] **Step 3: Migrate callers and delete superseded code** - -Move every root caller to explicit application/API crate imports, then delete the shim and branch-local compatibility. Split touched oversized modules by cohesive responsibility, remove unused dependencies with their last caller, and update durable capability documentation. - -- [ ] **Step 4: Verify GREEN and commit** - -Run API/dashboard/SSE tests, `cargo check --workspace --all-targets --all-features`, dead-code/size/dependency hygiene scripts, and bundle checks. Commit as `refactor(application): complete the V2 cutover`. - -### Task 11: Converge all local tests and produce RC evidence - -**Files:** -- Modify: only source/tests required to root-cause fresh failures -- Modify: `docs/plans/tracedecay-v2/NEXT.md` with current measured outcomes only -- Create: one current RC evidence document under `docs/reports/` -- Modify: release metadata only if the existing release authority requires a repository-side change - -**Interfaces:** -- Consumes: completed Tasks 1–10. -- Produces: zero unclassified repository failures and an exact ledger of external gates without publishing or tagging. - -- [ ] **Step 1: Run formatting and generated-drift gates** - -Run `cargo fmt --all -- --check`, dashboard contract check, SDK generation/conformance, plugin bundle validation, and release-drift checks. Fix root causes and rerun each failing gate to exit 0. - -- [ ] **Step 2: Run compiler and lint gates** - -Run `cargo check --workspace --all-targets --all-features` and the repository's CI-equivalent clippy command with warnings denied. Fix every fresh failure without suppressing lints or adding dead-code allowances. - -- [ ] **Step 3: Run dashboard and focused production journeys** - -Run dashboard typecheck/tests/build; Work/workflow/provider/cancellation/retry/review/replan/no-Git/Git/lease-loss/restart journeys; retained/Scout/remote/LSP/privacy journeys; and every supported host bundle/lifecycle journey. Require direct exit 0 and non-zero test counts. - -- [ ] **Step 4: Run aggregate Rust verification** - -Run `cargo nextest run --workspace --all-features --no-fail-fast --no-tests=fail` or the exact CI-equivalent shards when platform constraints require it. Classify every failure by root cause, fix it with a failing regression test, and repeat until the local supported matrix is green. - -- [ ] **Step 5: Run final review and current CI** - -Dispatch a whole-branch semantic review against the integration floor, fix its complete load-bearing finding set in one wave, rerun scoped review, push the branch, and watch the final commit's CI/SDK/plugin workflows to terminal status. Do not reuse CI from an older SHA. - -- [ ] **Step 6: Record RC evidence and external gates** - -Update `NEXT.md` only with measured current outcomes. Record command, timestamp, commit SHA, executed counts, and exit status for every gate. List npm OIDC, live semantic evaluation, operator host runs, cadence re-observation, and large-store GC separately with no fabricated pass. - -- [ ] **Step 7: Commit** - -Commit evidence and any final root-cause fixes as coherent conventional commits; do not create a tag, GitHub Release, or package publication without explicit user authorization. diff --git a/docs/superpowers/plans/2026-08-23-pr663-agent-handoff-prompt.md b/docs/superpowers/plans/2026-08-23-pr663-agent-handoff-prompt.md deleted file mode 100644 index b2f1696116..0000000000 --- a/docs/superpowers/plans/2026-08-23-pr663-agent-handoff-prompt.md +++ /dev/null @@ -1,204 +0,0 @@ -# Copy/Paste Agent Handoff — Finish PR #663 and Remove Runtime Bottlenecks - -Copy everything below the divider into a fresh agent chat. - ---- - -Work in `/fast/projects/tracedecay` and continue the existing TraceDecay delivery. Read `AGENTS.md` and then read the full plan at: - -`/fast/projects/tracedecay/docs/superpowers/plans/2026-08-23-pr663-performance-recovery.md` - -Use `superpowers:executing-plans` to execute that plan task by task, plus `tracedecay:using-tracedecay`, `tracedecay:reviewing-changes`, `superpowers:test-driven-development`, `performance-profiling`, and `superpowers:verification-before-completion` as applicable. Keep the plan checkboxes and live snapshot current as evidence changes. - -## Objective - -1. Safely review and checkpoint the active dirty work on PR #663. -2. Resolve every current and delayed review comment with behavioral evidence. -3. Make PR #663 genuinely green and merge-ready. -4. Merge PR #663 only into `codex/tracedecay-total-redesign-plan` (PR #421's integration branch). -5. Never merge PR #421 into `master`, never merge PR #559, and never force-push. -6. Build and install the exact post-merge beta, restart the daemon once, then dogfood CLI/MCP on the real corpus. -7. Profile and fix root performance bottlenecks. Do not mask them with retries, longer timeouts, weaker assertions, or reduced work. - -## Exact starting snapshot - -The snapshot below was current at 2026-08-23 05:25 UTC. Refresh every value before editing: - -```text -checkout: /fast/projects/tracedecay -branch: cursor/simplify-pr421-hot-paths -PR: https://github.com/ScriptedAlchemy/tracedecay/pull/663 -head/local/remote: c7eb51ffc7919457a905537774a1000bd96193f3 -target/merge-base: 31d7949c1e298b324931132be8031bd92e64eec4 -target branch: codex/tracedecay-total-redesign-plan -PR state: OPEN, non-draft, MERGEABLE, UNSTABLE -dirty state: 120 modified tracked files plus untracked peer backend_identity.rs and the two untracked plan files -peer owner: Claude root PID 1464997; PID 1746348 is an until-cargo-check retry loop -review state: five original threads resolved; no delayed thread at 05:25 UTC, but delayed review may still add more -``` - -Immediately run: - -```bash -git branch --show-current -git rev-parse HEAD -git rev-parse origin/cursor/simplify-pr421-hot-paths -git rev-parse origin/codex/tracedecay-total-redesign-plan -git merge-base HEAD origin/codex/tracedecay-total-redesign-plan -git status --short -git diff --check -ps -eo pid,ppid,etimes,pcpu,pmem,rss,stat,args --sort=-rss \ - | awk 'BEGIN{IGNORECASE=1} /cargo|rustc|claude/ && $0 !~ /awk/ {print}' \ - | head -n 120 -gh pr view 663 --json state,isDraft,mergeable,mergeStateStatus,baseRefOid,headRefOid,url -gh pr checks 663 --json name,bucket,state,workflow,link -``` - -If refs or dirty ownership changed, update the plan before proceeding. Never reset, stash, format, stage, or commit another owner's work. Re-read each file immediately before editing. Do not launch Cargo while an equivalent peer build is active, and never kill peer builds or the live daemon. - -## Work already completed — do not redo it - -- `150ac00b5 test(release): verify canonical provenance helper` - - Behavioral release-helper guard is fixed. - - `tests/release_safety_test.sh`, `tests/release_drift_check_test.sh`, and hosted `Release Version Drift` are green. -- `4ae63542a test(dashboard): align agents fixture with diagnostics contract` - - Focused Agents dashboard Vitest is 7/7 and dashboard typecheck is green. -- `b8ea46cec fix(automation): restore skill receipt digest authority` - - Restores the canonical `sha256_json` import without a shadow helper. - - Focused `tracedecay-agent-hosts` library check is green. -- `c7eb51ffc fix(dashboard): type hook analytics window bounds` - - Restores explicit `Option` timestamp accumulator types. - - Focused `tracedecay-dashboard-api` library check is green. -- All five original automated review findings were tested, replied to, and resolved: - - released byte-array spool recovery 1/1; - - replay cleanup combined failure 1/1; - - Explorer transient polling 6/6; - - tombstone probe 4/4; - - typed clock failure 1/1. - -Re-query those threads after every push, but do not reimplement them unless new evidence invalidates the receipts. - -## Immediate critical path - -### 1. Wait for and review the peer dirty checkpoint - -The shared checkout contains a broad active Rust rewrite. Do not sweep it into one commit. Determine whether PID 1464997 or its descendants are still changing paths. Once the owner creates coherent commits, review each commit semantically and run the smallest affected tests. Preserve ownership boundaries. - -The highest-risk dirty draft is `crates/tracedecay-agent-hosts/src/automation/backend_identity.rs` plus its scheduler/lifecycle callers. Before accepting it, require: - -- backend identity bound to opened executable revision/content, not only resolved path; -- same-path executable replacement re-admits work; -- `Unavailable` and `Denied` are recoverable/cooldown states, not indefinite permanent suppression; -- only a truly typed deterministic protocol/config failure is permanently suppressed; -- no string-matched error classification or shadow identity authority. - -### 2. Finish the remaining root compilation repairs - -The former shared `sha256_json` blocker is already fixed and pushed. Do not redo it. The next two committed defects are: - -```text -src/daemon/session_sync/git_topology.rs and work.rs -references to removed SESSION_SYNC_POLL_INTERVAL - -src/mcp/server.rs:951-952 -AtomicU64::new(persisted) with no persisted binding -``` - -For session sync, do not blindly restore a global 10ms polling tick. Keep a dedicated bounded journal poll only if no completion notification exists; await `CancellationSignal::cancelled()` for request cancellation; use a single deadline sleep; and connect daemon shutdown to the existing `shutdown_notify` or one canonical async `ObservationCancellation` authority. Add behavioral cancellation/deadline/shutdown tests. - -For MCP token accounting, read persisted tokens exactly once before `Arc::new_cyclic`; use the same successful value for both atomics and the optional accounting upsert. A failed read must not fabricate or upsert zero. `src/mcp/server.rs` also has a peer-owned rustfmt hunk, so stage only the accounting repair. - -There is also a self-owned uncommitted test in `tests/hooks_lsp_suite/hooks_test.rs` for the shipped `codex_additional_context_json` compatibility alias. Preserve it, add a thin production delegation plus re-export, and obtain a non-vacuous RED/GREEN once root compilation reaches the test. - -### 3. Clear independent CI blockers - -- Format: CI reports rustfmt drift across much of the peer-owned dirty Rust sweep. Format only after coherent peer checkpointing. Run `cargo fmt --all -- --check`; commit formatting with the semantic slice it belongs to, not as an unexplained 100-file sweep. -- SDK packages: the job is failed at `c7eb51ffc` while its workflow is still running, so logs are not yet downloadable. Prior canonical generation changed only `sdks/typescript/src/operations.ts`. After root compilation is clean, run `sdks/codegen/generate.sh` followed by `scripts/check-sdk-codegen.sh`; never hand-edit generated operations. -- Stable Clippy after compilation: - - three needless borrows in `crates/tracedecay-code-extraction/src/c_extractor.rs`; - - collapsible nested `if` in `crates/tracedecay-code-extraction/src/common.rs` and `elixir_extractor.rs`; - - unused `encode_tagged_lowercase_hex` in code-generation retention; - - unused-must-use in `lsp_runtime.rs` must be handled truthfully, not discarded blindly. -- Hawk/dead surface after compilation: - - `canonical_session_metadata`; - - `load_relation_by_edge`; - - `load_relation_by_locator`; - - `semantic_lane_readiness_for_request`. - Delete only after exact caller proof; otherwise wire the actual production consumer. -- Windows compatibility: replace unstable Windows `MetadataExt` file identity calls in code-generation retention with `tracedecay_runtime_core::windows_file::information(&File)`, retaining opened-handle identity and replacement refusal. -- Shipped API compatibility: `origin/master` exposed `tracedecay::hooks::codex_additional_context_json`. Restore it as a thin alias to the canonical formatter, re-export it, and add a byte-equality test. - -### 4. Re-run checks from root causes outward - -Use narrow checks first. Confirm nonzero test counts. Then rerun affected packages and the failed hosted jobs. Do not count skipped checks as green and do not debug router/MCP behavior until the shared compile blocker is gone. - -After every push: - -1. verify local and remote head equality; -2. query all review threads with GraphQL; -3. wait several minutes for delayed automated review; -4. address every current comment with RED/GREEN behavioral evidence; -5. reply with exact commit/test evidence, then resolve; -6. refresh CI and distinguish root failure from cascade. - -Only merge #663 when its exact pair is reviewed, required checks are green, no unresolved comment exists, and mergeability is clean. Merge normally into `codex/tracedecay-total-redesign-plan`; do not merge #421 itself. - -## Post-merge beta and performance phase - -Do not profile the dirty source tree or the old binary as if it contained the fixes. First build/install the exact merged integration-branch binary and verify its version/commit. Preserve the old daemon evidence, then restart once. - -Current old-binary baseline: - -```text -version: 0.1.0-beta.37+31d7949c1e29 -PID: 2682560 -CPU: 143% -RSS: 4,440,940 KiB -process swap: 4,150,216 KiB -threads: 118 -physical reads: 1,594,551,470,080 bytes -writes: 135,648,800,204 bytes -tracedecay status: 1.80 seconds, exit 0, graph not ready -retention: processed 104, deferred 149, succeeded=false -historical writer telemetry: 557,009 operations / 556,981 commits -queue wait: ~3,939 seconds -transaction time: ~3,354 seconds -``` - -The leading performance hypothesis is repeated unchanged transcript discovery/read work plus nearly one SQLite commit per admitted operation. Validate it on the installed merged binary before changing code. - -Measure identical cold/warm journeys for `status`, `runtime`, `active_project`, context, grep, and session/message retrieval. Capture wall/user/system time, exit and typed state, daemon CPU/RSS/swap/read bytes, opened transcript files, writer operations/commits/WAL bytes, queue wait, and transaction time. Use bounded `pidstat`, `iostat`, `perf`, and file-open tracing where available. - -Then implement only measured root-cause fixes: - -1. Persist per-source file identity, byte cursor, and directory discovery watermark so unchanged historical transcript files are not reopened after catch-up. -2. Batch bounded independent admissions in one canonical writer transaction while preserving ordered cursor/CAS, cancellation, rollback, memory limits, and digest authority. -3. Parallelize only independent per-file/per-source parsing and hashing; do not create parallel writers for one store or weaken serial digest chains. -4. Keep foreground CLI/MCP responsive through prioritization and bounded background slices, not retries or longer deadlines. - -Acceptance on the same machine/corpus: - -```text -unchanged historical transcript opens after catch-up: 0 in a bounded idle sample -commits per admitted historical record: at least 10x lower than the old baseline -foreground status/runtime p95 while warming: <= 2 seconds -no swap growth or unbounded RSS slope during catch-up -all typed cancellation/restart/authority tests remain equal or stronger -``` - -Commit every proven optimization separately with its baseline/RED, production change, GREEN test, and before/after profile receipt. Never declare victory from lower CPU alone if the same work merely moved to retries or was skipped. - -## Reporting - -Send concise progress updates at meaningful boundaries. Final handoff must include: - -- exact base/head/merge-base and branch target; -- commits and owned paths; -- review-thread state and delayed-review check; -- CI root causes versus cascades; -- exact focused and broader test receipts; -- exact installed binary version/commit; -- before/after performance table; -- unresolved risks, without hiding them behind retries, timeout increases, or skipped work. - ---- diff --git a/docs/superpowers/plans/2026-08-23-pr663-performance-recovery.md b/docs/superpowers/plans/2026-08-23-pr663-performance-recovery.md deleted file mode 100644 index 0a788953dc..0000000000 --- a/docs/superpowers/plans/2026-08-23-pr663-performance-recovery.md +++ /dev/null @@ -1,449 +0,0 @@ -# PR 663 Completion and Runtime Performance Recovery Plan - -> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. - -**Goal:** Safely turn the active dirty `cursor/simplify-pr421-hot-paths` checkout into a verified PR #663, merge only that PR into `codex/tracedecay-total-redesign-plan`, release and install the resulting beta, then profile and remove the remaining TraceDecay CLI/MCP bottlenecks with measured root-cause fixes. - -**Architecture:** Treat branch completion and performance work as separate evidence phases. First preserve current shared-checkout ownership, checkpoint coherent correctness slices, fix the known CI and review blockers, and merge PR #663 into the PR #421 integration branch without merging #421 itself. Only then build/install the exact resulting binary and compare identical before/after workloads; optimize transcript discovery and storage batching where the measurements show repeated work. - -**Tech Stack:** Rust 2024 workspace, Tokio, rusqlite, React/Vitest dashboard, GitHub Actions/`gh`, TraceDecay MCP/CLI, Linux `perf`/`pidstat`/`iostat`/`strace` where available. - -**Spec:** `docs/plans/tracedecay-v2/00-plan-set-index.md` - -## Live Handoff Snapshot — 2026-08-23 05:25 UTC - -- Checkout: `/fast/projects/tracedecay` -- Branch: `cursor/simplify-pr421-hot-paths` -- PR: [#663](https://github.com/ScriptedAlchemy/tracedecay/pull/663), open, non-draft, mergeable, currently `UNSTABLE` -- Exact local/remote head: `c7eb51ffc7919457a905537774a1000bd96193f3` -- Exact target/merge-base: `31d7949c1e298b324931132be8031bd92e64eec4` (`codex/tracedecay-total-redesign-plan`) -- Shared checkout: 120 modified tracked files plus three untracked paths: peer file `crates/tracedecay-agent-hosts/src/automation/backend_identity.rs` and the two handoff-plan documents in this directory. -- Active peer owner: Claude root PID `1464997`. No `cargo`/`rustc` was active at this snapshot, but PID `1746348` is an `until cargo check --workspace` retry loop and may relaunch at any time. Recheck immediately before every Cargo command. -- Review threads: five total, all replied to with behavioral evidence and resolved; a fresh GraphQL query at this head found no delayed thread. Re-query after every later push because automated review may still arrive. - -Already landed and pushed on the exact head: - -1. `150ac00b5 test(release): verify canonical provenance helper` - - RED: the stale release test required an inline `gh attestation verify` command even though all workflows call the canonical helper. - - GREEN: `tests/release_safety_test.sh` now behaviorally executes `scripts/verify-retained-release-assets.sh`, verifies its exact provenance flags and failure propagation, and `Release Version Drift` is green. -2. `4ae63542a test(dashboard): align agents fixture with diagnostics contract` - - RED: the old fixture fabricated `event_count=0` while supplying four diagnostics composition rows. - - GREEN: usage remains unknown while diagnostics uses the canonical count of four; focused Vitest is 7/7 and dashboard typecheck is green. -3. `b8ea46cec fix(automation): restore skill receipt digest authority` - - Restores the canonical `crate::automation::artifacts::sha256_json` import without adding a shadow helper. - - `TRACEDECAY_SKIP_DASHBOARD_BUILD=1 cargo check -p tracedecay-agent-hosts --lib --locked` is green. -4. `c7eb51ffc fix(dashboard): type hook analytics window bounds` - - Adds explicit `Option` types to the hook analytics oldest/newest timestamp accumulator rather than relying on an unconstrained closure inference. - - `TRACEDECAY_SKIP_DASHBOARD_BUILD=1 cargo check -p tracedecay-dashboard-api --lib --locked` is green. -5. All five original review findings have clean-head focused receipts: spool upgrade 1/1, replay cleanup 1/1, Explorer polling 6/6, tombstone probe 4/4, and typed clock failure 1/1. - -Current root CI classification: - -| Check | Classification | Required action | -| --- | --- | --- | -| Clippy / root compilation | Two remaining committed compile defects, then lint blockers | Repair the session-sync wake design instead of blindly restoring a global 10ms poll constant; initialize MCP `persisted` tokens from the canonical typed accounting read. Then fix the reported Clippy findings without allows. | -| Hawk | Root compile cascade plus real dead/unmounted warnings | After compilation, audit `canonical_session_metadata`, `load_relation_by_edge`, `load_relation_by_locator`, and `semantic_lane_readiness_for_request`; delete only after exact caller proof or wire the production caller. | -| MCP conformance / production-router | Current reruns in progress | The shared `sha256_json` blocker is fixed. Interpret any new failure only after the session-sync and MCP-server compile defects are removed. | -| Format | Real broad dirty-tree drift | CI reports rustfmt diffs across the active peer-owned Rust sweep. Format only after the peer checkpoints a coherent slice; never commit a blind shared-checkout sweep. | -| SDK packages | Failed again at `c7eb51ffc`; logs unavailable until the still-running workflow completes | Prior evidence showed canonical generated drift only in `sdks/typescript/src/operations.ts`; regenerate via `sdks/codegen/generate.sh`, then run `scripts/check-sdk-codegen.sh`. | -| Dashboard / manifest / Claude plugin / publish policy | Green at snapshot | Preserve these receipts; do not rerun unrelated work locally while the shared Cargo lane is contended. | -| Windows / Linux / macOS / remaining integrations | Not all jobs materialized yet | Wait for terminal results and classify root failures separately from compile cascades. | - -Current old-binary performance baseline, captured before restart: - -```text -binary: tracedecay 0.1.0-beta.37+31d7949c1e29 -daemon PID: 2682560, started 2026-08-23 01:09:01 UTC -CPU: 143% -RSS: 4,440,940 KiB -swap attributed to process: 4,150,216 KiB -threads: 118 -/proc read_bytes: 1,594,551,470,080 -/proc write_bytes: 135,648,800,204 -status latency: 1.80 seconds, exit 0 -status state: indexing; graph exact_scope_generation_not_ready -retention log: succeeded=false, processed_stores=104, deferred_stores=149 -retention degradation: semantic configuration/vector authority unavailable and vector census incomplete -``` - -Earlier writer telemetry from the same old daemon showed 557,009 admitted operations and 556,981 commits, with approximately 3,939 seconds of queue wait and 3,354 seconds of transaction time. Treat this as the leading hypothesis—unchanged transcript rescans plus nearly one SQLite commit per operation—not as proof until the exact merged binary is installed and the identical workload is measured again. - -## Global Constraints - -- Never merge PR #421 into `master`; merge PR #663 only into `codex/tracedecay-total-redesign-plan`. -- Never merge PR #559. -- Never force-push, reset, discard, or sweep in another owner's dirty work. -- The primary checkout is shared. At the 2026-08-23 05:25 UTC snapshot it had 120 modified tracked files, one peer untracked source file, and two untracked handoff documents, with another Claude session owning a workspace-check retry loop. -- Before every Cargo launch, inspect active `cargo`/`rustc` processes and wait for equivalent peer work rather than competing or killing it. -- Use TraceDecay graph/context tools before native source search; if the graph returns typed `generation_rebuilding`, use the TraceDecay CLI fallback, then narrow working-tree reads. Never query `.tracedecay` databases directly. -- Preserve typed failures and exact authority/identity contracts. Do not raise timeouts, add retries, weaken assertions, or treat a stale binary as test evidence. -- Use `apply_patch` for edits, conventional commits, narrow anti-vacuous tests, and a fresh final-head review before merge. -- Generated dashboard contracts and SDK files are regenerated through their canonical generators; never hand-edit them. - ---- - -### Task 1: Establish an ownership-safe checkpoint - -**Files:** -- Inspect only: all current dirty paths -- Do not create or repurpose a worktree without operator approval - -**Interfaces:** -- Consumes: current shared checkout and active owner processes -- Produces: an exact ownership map and a clean semantic checkpoint boundary - -- [ ] **Step 1: Refresh exact refs and dirty state** - -Run: - -```bash -git branch --show-current -git rev-parse HEAD -git rev-parse origin/cursor/simplify-pr421-hot-paths -git rev-parse origin/codex/tracedecay-total-redesign-plan -git merge-base HEAD origin/codex/tracedecay-total-redesign-plan -git status --short -git diff --check -gh pr view 663 --json state,isDraft,mergeable,mergeStateStatus,baseRefOid,headRefOid,url -``` - -Expected at the refreshed handoff snapshot: branch `cursor/simplify-pr421-hot-paths`, local/remote head `c7eb51ffc7919457a905537774a1000bd96193f3`, base/merge-base `31d7949c1e298b324931132be8031bd92e64eec4`, and PR #663 open. Treat any difference as new evidence and update the plan before editing. - -- [ ] **Step 2: Find active owners and builds** - -Run: - -```bash -ps -eo pid,ppid,etimes,pcpu,pmem,rss,stat,args --sort=-rss \ - | awk 'BEGIN{IGNORECASE=1} /cargo|rustc|claude/ && $0 !~ /awk/ {print}' \ - | head -n 120 -git status --porcelain=v1 | cut -c4- | while IFS= read -r p; do - test -e "$p" && stat -c '%Y %y %n' "$p" -done | sort -nr | head -n 80 -``` - -Do not edit a path whose mtime or owner process is still advancing. Wait for the active owner to commit/push a coherent slice, then review that commit. Do not count output from a script that continues after `cargo build` fails or reuses a pre-existing binary. - -- [ ] **Step 3: Partition the dirty diff by behavior** - -Use these initial clusters, adjusting to the refreshed diff: - -1. Codex app-server lifetime and automation backend failure settlement. -2. Session checkout authorization and retrieval. -3. CLI/MCP application-error exit status and proxy shutdown/error propagation. -4. Mechanical hot-path simplifications and format/Clippy cleanup. -5. Untouched CI compatibility fixes. - -Each cluster must compile and carry its own focused tests before commit. Never commit all 121 entries as one unexplained sweep. - ---- - -### Task 2: Make the active correctness slices truthful before checkpointing - -**Files:** -- Modify: `crates/tracedecay-sessions/src/runtime/codex_app_server.rs` -- Modify: `crates/tracedecay-agent-hosts/src/automation/backend_identity.rs` -- Modify: `crates/tracedecay-agent-hosts/src/automation/scheduler.rs` -- Modify: `crates/tracedecay-agent-hosts/src/automation/lifecycle.rs` -- Modify: `src/daemon/session_retrieval/admitted.rs` -- Modify: `src/tool_command.rs` -- Modify: `src/tool_command/tests.rs` -- Modify: `src/daemon/core_client.rs` -- Modify: `src/daemon/core_proxy.rs` -- Test: existing in-file and integration tests adjacent to these paths - -**Interfaces:** -- Consumes: `AutomationConfig`, `AgentTaskFailureClass`, `ResolvedScope`, MCP `isError` -- Produces: bounded app-server lifetime, truthful suppression identity, checkout-scoped retrieval, and CLI/MCP error parity - -- [ ] **Step 1: Preserve the app-server stdin lifetime regression** - -Keep stdin open through `turn/completed`; close it only after `wait_for_turn_summary` returns. The focused test must launch the real configured app-server protocol boundary and prove a turn cannot be cancelled merely because the client sent no more requests. - -- [ ] **Step 2: Fix backend suppression identity before committing it** - -The current draft's `backend_executable_identity` is only the resolved path. Replace it with an identity derived from the opened executable, including stable file identity and content/revision evidence, so replacing or upgrading a binary at the same path changes the digest. Reuse existing canonical file/digest authorities; avoid hashing the binary on every scheduler tick by retaining the identity at the configuration/runtime boundary. - -Add a regression that writes backend bytes at one path, records a deterministic failure, replaces the bytes at the same path, and proves the next scheduler decision is `due`, not `backend_identity_suppressed`. - -- [ ] **Step 3: Narrow permanent suppression to truly deterministic classes** - -Do not indefinitely suppress `Unavailable` or `Denied`: installation, credentials, provider policy, and service state can change without the automation config changing. Do not suppress a generic `Disconnected` unless the failure is represented by a distinct typed protocol-contract class. Prefer adding/using a typed protocol violation over matching an error string. Retain cooldown behavior for transient classes. - -Tests must cover: - -```text -same executable + same config + typed permanent protocol failure => suppressed -same-path executable replacement => re-admitted -Unavailable => ordinary cooldown, then re-admitted -Denied => ordinary cooldown, then re-admitted -Timeout/Retryable => ordinary cooldown -``` - -- [ ] **Step 4: Keep session authorization checkout-scoped** - -The request and mounted session scopes may carry different branch refs while naming the same project/repository/worktree. Keep `ResolvedScope::identifies_same_checkout` at the admission boundary and retain the foreign-worktree refusal test. - -- [ ] **Step 5: Keep CLI and MCP failure semantics aligned** - -For compatibility tool dispatch, print the exact daemon payload, then exit nonzero only when the daemon sets top-level `isError: true`. Warming/partial/unavailable typed payloads without `isError` remain exit 0. Test both JSON and markdown payloads, stdout preservation, and nonzero application failure. - -- [ ] **Step 6: Run focused tests and commit by slice** - -Wait for the Cargo lane, then run anti-vacuous focused tests for the files above. Commit at least the app-server/automation, retrieval, and CLI/proxy changes separately with conventional messages. - ---- - -### Task 3: Restore compilation and platform/API compatibility - -**Files:** -- Modify: `crates/tracedecay-agent-hosts/src/automation/runner/skill_writer.rs` -- Modify: `src/daemon/session_sync.rs` -- Modify: `src/daemon/session_sync/work.rs` -- Modify: `src/daemon/session_sync/git_topology.rs` -- Modify: `src/mcp/server.rs` -- Modify: `crates/tracedecay-usecases/src/retention/code_index_generations.rs` -- Modify: `crates/tracedecay-usecases/src/retention/code_index_generations/scope_quarantine.rs` -- Modify: `src/hooks/codex.rs` -- Modify: `src/hooks/mod.rs` -- Modify: `tests/hooks_lsp_suite/hooks_test.rs` - -**Interfaces:** -- Consumes: canonical JSON digest helper, cancellation/deadline wake authorities, persisted token accounting, `tracedecay_runtime_core::windows_file::information`, shared hook JSON formatter -- Produces: event-driven session interruption, truthful MCP accounting initialization, Linux/Windows compilation, and preserved shipped Rust API - -- [x] **Step 1: Restore the canonical skill receipt digest import** - -Completed in `b8ea46cec`. The dirty `skill_writer.rs` now imports the canonical helper from the existing automation artifact authority, the peer formatting hunk remains unstaged, and the focused agent-hosts library check is green. - -- [ ] **Step 2: Repair session-sync interruption waits without restoring hot polling globally** - -Commit `cce801d93 perf(daemon): wait on session-sync permits without polling` removed `SESSION_SYNC_POLL_INTERVAL`, but `session_sync/work.rs` and `session_sync/git_topology.rs` still reference it. Do not simply restore a shared 10ms tick and call the regression fixed. - -Classify the four waits separately: - -1. Coalesced journal completion may retain one dedicated bounded poll only if no durable completion notification exists. -2. Request cancellation must await the existing `CancellationSignal::cancelled()` future. -3. Deadline waits must use one deadline sleep rather than repeated `now_micros()` polling. -4. Daemon shutdown must use `shutdown_notify` or a canonical async extension of `ObservationCancellation`; do not add a second shutdown authority. - -Add cancellation/deadline/shutdown regressions that prove prompt interruption without measuring a magic poll count, then run the smallest session-sync compilation/test slice. - -- [ ] **Step 3: Initialize MCP token accounting from the canonical typed read** - -`src/mcp/server.rs` constructs `tokens_saved` and `last_flushed_tokens` from an undefined `persisted`. Read tokens once before `Arc::new_cyclic`, use that exact result for both atomics and the optional accounting upsert, and preserve the existing rule that a failed read must not fabricate or upsert zero. Prefer one typed read with explicit unavailable behavior over two reads or `unwrap_or(0)`. - -The file also contains an unstaged peer rustfmt hunk around line 646. Stage only the accounting fix and leave peer formatting ownership intact. - -- [ ] **Step 4: Replace unstable Windows metadata APIs** - -Do not use `std::os::windows::fs::MetadataExt::{volume_serial_number,file_index,number_of_links}`. Open the path and compare retained handles using: - -```rust -tracedecay_runtime_core::windows_file::information(&file) -``` - -Compare `volume_serial_number`, `file_index`, and `number_of_links`; keep length/type/modified-time checks as appropriate. Hold the original file handle across verification and reopen the current named path before comparison, preserving rename/replacement refusal. Remove the now-unused Windows `MetadataExt`, the unused `encode_tagged_lowercase_hex`, and the Windows-only unused `OpenOptions` import. - -Run when the Cargo lane is free: - -```bash -TRACEDECAY_SKIP_DASHBOARD_BUILD=1 \ - cargo check -p tracedecay-usecases --lib --target x86_64-pc-windows-gnu --locked -``` - -- [ ] **Step 5: Restore the shipped hook compatibility alias** - -`origin/master` shipped `tracedecay::hooks::codex_additional_context_json`; PR #663 removed it. Restore: - -```rust -pub fn codex_additional_context_json(event_name: &str, additional_context: &str) -> String { - super::additional_context_json(event_name, additional_context) -} -``` - -Re-export it from `src/hooks/mod.rs` and test that it is byte-identical to `additional_context_json`. Do not restore duplicate formatting logic. The test-side import/assertion is already staged only in the working tree at `tests/hooks_lsp_suite/hooks_test.rs`; production alias/re-export and a valid non-vacuous RED/GREEN receipt remain outstanding. - -- [ ] **Step 6: Commit compatibility fixes as coherent slices** - -Run focused session-sync, MCP-server, Windows, and hook checks; rustfmt only exact owned paths; run `git diff --check`; then commit each coherent slice separately. - ---- - -### Task 4: Fix non-cascading CI blockers at their authority - -**Files:** -- Modify: `tests/release_safety_test.sh` -- Verify: `scripts/verify-retained-release-assets.sh` -- Modify: `dashboard/src/workspaces/agents/AgentsPage.dom.test.tsx` -- Regenerate: `sdks/typescript/src/**` through `sdks/codegen/generate.sh` -- Modify/delete only proven dead symbols reported by Clippy/Hawk - -**Interfaces:** -- Consumes: canonical release verification helper, Testing Library query semantics, SDK generator -- Produces: release provenance guard, deterministic dashboard assertion, generated SDK parity, clean Clippy/Hawk - -- [x] **Step 1: Fix the release guard, not the release provenance behavior** - -The workflow already invokes `scripts/verify-retained-release-assets.sh`, and that helper contains `gh attestation verify`, `--signer-workflow`, `--source-ref`, `--source-digest`, and `--deny-self-hosted-runners`. Change `tests/release_safety_test.sh` so workflows must invoke the canonical helper, then separately read and assert those exact provenance properties in the helper. Do not duplicate `gh attestation verify` in three workflow steps and do not satisfy the guard with a comment. - -Run: - -```bash -bash tests/release_safety_test.sh -bash tests/release_drift_check_test.sh -``` - -Completed in `150ac00b5`; both local scripts and hosted `Release Version Drift` are green. - -- [x] **Step 2: Fix the dashboard diagnostics fixture and assertion** - -The first failure was a multiple-match Testing Library query, but tightening it exposed the real defect: the test supplied four canonical diagnostics composition rows while defaulting the required diagnostics `event_count` to zero. Keep usage telemetry unknown, set diagnostics `event_count` to four, assert both charts use `share of 4`, and assert no fabricated `share of 0` appears. - -```ts -expect(screen.queryAllByText(/share of 0$/)).toHaveLength(0); -``` - -Run the exact Vitest file first, then dashboard typecheck and full tests. - -Completed in `4ae63542a`; focused Vitest is 7/7, `npm run typecheck` is green, and the hosted dashboard artifact job is green at `c7eb51ffc`. - -- [ ] **Step 3: Regenerate SDK clients canonically** - -Run: - -```bash -sdks/codegen/generate.sh -scripts/check-sdk-codegen.sh -``` - -Review and commit only the generator-authorized output. Never hand-edit `sdks/typescript/src/operations.ts`. - -- [ ] **Step 4: Clear Clippy/Hawk/format findings semantically** - -Collapse the five reported nested `if` blocks without `allow` attributes. Remove `canonical_session_metadata`, `load_relation_by_edge`, and `load_relation_by_locator` only after an exact caller search confirms they are truly dead; otherwise wire the production caller. Run scoped Clippy before workspace Clippy, then Hawk and rustfmt. - ---- - -### Task 5: Close review threads and make PR #663 merge-ready - -**Files:** -- Review only: PR #663 exact final pair - -**Interfaces:** -- Consumes: final commits, focused test evidence, GitHub review threads/checks -- Produces: zero unresolved threads and green required checks - -- [x] **Step 1: Re-query and substantively close the five existing review threads** - -At the snapshot there were five unresolved threads: one current P1 on released byte-array spool append intents and four outdated P2s on replay cleanup, Explorer polling, tombstone scanning, and invalid clocks. For each, verify final-head behavior with a falsifiable test, reply with the exact commit/test receipt, then resolve. Do not resolve based only on the thread becoming outdated. - -All five are resolved with the receipts listed in the live snapshot. They remain resolved after pushes through `c7eb51ffc`; a fresh GraphQL query at 05:25 UTC found no delayed thread. This checkbox covers only the existing threads; Step 3 remains mandatory after every future push. - -- [ ] **Step 2: Refresh and clear CI** - -Use: - -```bash -gh pr checks 663 --json name,bucket,state,workflow,link -``` - -Distinguish root failures from compile cascades. Re-run only after the corresponding fix is pushed. Do not claim skipped checks as green. - -- [ ] **Step 3: Wait for delayed automated review** - -After the final push and green focused gates, wait several minutes, then query review threads again. Address any new current comment before merge. - -- [ ] **Step 4: Merge only PR #663** - -Revalidate exact base/head, mergeability, zero unresolved comments, and green required checks. Use a normal GitHub merge into `codex/tracedecay-total-redesign-plan`. Do not merge that integration branch into `master` and do not touch PR #559. - ---- - -### Task 6: Build, install, and validate the exact post-merge beta - -**Files:** -- Follow: canonical beta-release/release documentation and workflows -- Install target: `~/.local/bin/tracedecay` - -**Interfaces:** -- Consumes: exact merged `codex/tracedecay-total-redesign-plan` commit -- Produces: version-bound installed binary and before/after daemon receipt - -- [ ] **Step 1: Capture the pre-restart evidence** - -Record daemon PID/version, RSS, CPU, thread count, runtime writer counters, WAL bytes, swap, and representative CLI/MCP latencies. Do not kill the old process until its evidence is durable. - -- [ ] **Step 2: Build from the exact merged commit** - -Use the repository's canonical beta process. Stop immediately on build failure, delete or ignore any old output, and verify the produced binary reports the exact expected version/commit before installation. A script that checks only whether `target/.../tracedecay` exists is invalid. - -- [ ] **Step 3: Install and restart safely** - -Install the verified binary to `~/.local/bin/tracedecay`, restart the daemon once, and confirm startup health plus exact version. Preserve typed startup failure; do not loop restarts. - ---- - -### Task 7: Profile the real CLI/MCP workload before optimizing - -**Files:** -- Add or modify only after profiling identifies the production owners -- Prefer an existing benchmark location under `benchmark_data/runtime/` for reproducible receipts - -**Interfaces:** -- Consumes: installed post-merge beta and real Cursor/Codex session corpus -- Produces: reproducible latency, I/O, CPU, memory, file-open, and writer-amplification baseline - -- [ ] **Step 1: Run identical cold/warm command journeys** - -Measure at least `status`, `runtime`, `active_project`, `context`, `grep`, and session/message retrieval. Capture wall/user/system time, exit code, typed result, p50/p95, daemon CPU/RSS, disk reads, and writer counters. Do not interpret a typed warming/unavailable response as transport failure. - -- [ ] **Step 2: Attribute I/O and CPU** - -Use bounded samples (`pidstat`, `iostat`, `perf record`, and file-open tracing where available) around one known command and one background catch-up interval. Confirm whether unchanged historical transcript files are reopened and reread. - -- [ ] **Step 3: Quantify writer amplification** - -For each store shard, record admitted operations, committed batches, WAL bytes, queue-wait microseconds, transaction microseconds, and messages/records advanced. The pre-fix snapshot was 557,009 admitted operations and 556,981 commits: almost one transaction per operation, with ~3,939 seconds queue wait and ~3,354 seconds transaction time. - ---- - -### Task 8: Remove measured bottlenecks and prove end-to-end gains - -**Files:** -- Likely modify: session provider discovery/cursor owners, runtime writer batching, and scheduler admission paths identified by Task 7 -- Test/benchmark: focused production journeys and `benchmark_data/runtime/` - -**Interfaces:** -- Consumes: Task 7 profile and existing per-source cursor/CAS authorities -- Produces: bounded incremental ingest, batched durable writes, responsive foreground tools, and before/after receipts - -- [ ] **Step 1: Eliminate unchanged transcript rescans** - -Persist and honor per-source file identity, byte cursor, and directory discovery watermark. After catch-up, unchanged historical day directories/files must not be reopened on every tick. Read only appended bytes and invalidate only when stable identity or size regresses. Preserve source-specific ordering and typed replacement/corruption states. - -- [ ] **Step 2: Batch storage admissions** - -Keep one canonical writer/CAS authority per store, but admit a bounded chunk of independent records per transaction instead of committing each message separately. Preserve ordered cursor advancement, cancellation checkpoints, memory budgets, and rollback. Do not create parallel writers or weaken digest chains. - -- [ ] **Step 3: Parallelize only independent work** - -Use bounded parallel extraction across files/sources that own independent cursors. Keep cumulative sealing and same-store cursor/CAS steps ordered. Wide CPU use should come from independent parse/hash work, not concurrent writes to one authority. - -- [ ] **Step 4: Meet falsifiable acceptance criteria** - -On the same corpus and machine: - -```text -unchanged transcript opens after catch-up: zero during a bounded idle sample -committed batches per admitted historical record: at least 10x lower than baseline -foreground status/runtime p95 while warming: <= 2 seconds -no swap growth and no unbounded RSS slope during catch-up -typed correctness/cancellation/restart tests: unchanged or stronger -``` - -If a target is missed, retain the measurement and continue diagnosis; do not raise deadlines or add retry loops. - -- [ ] **Step 5: Commit each proven optimization separately** - -Each commit includes its RED/baseline, production change, GREEN behavior, and before/after profile receipt. Finish with focused tests, broader affected-package gates, Clippy, rustfmt, diff-check, and an independent semantic review. diff --git a/docs/superpowers/plans/v2/pr16-remote-brain.md b/docs/superpowers/plans/v2/pr16-remote-brain.md deleted file mode 100644 index 293b0107fe..0000000000 --- a/docs/superpowers/plans/v2/pr16-remote-brain.md +++ /dev/null @@ -1,56 +0,0 @@ -# PR16 Remote Shared Brain Plan - -> **Archived provenance — not current requirements.** This document records -> historical planning and execution evidence. Current scope and acceptance come -> only from [`00-plan-set-index.md`](../../../plans/tracedecay-v2/00-plan-set-index.md), -> [`NEXT.md`](../../../plans/tracedecay-v2/NEXT.md), and the applicable numbered -> V2 plan. Do not recreate its task checklists, file inventories, -> branch/worktree/SHA or commit protocol, Gate A/B, timing/JUnit receipts, exact -> test names/counts, generated-byte/source-shape checks, PR closure gates, or -> platform gate lattice. -> Historical version/compatibility/migration language cannot resurrect -> branch-only transient scaffolding. Potentially persisted enrollment files, -> spools, replica journals, backups, checkpoints, and receipts keep -> backward-read/replay/recovery until a separately authorized machine/profile -> census proves absence. - -**Goal:** Add enrolled, offline-capable, fenced remote Brain operation with one -writer per mutable shard and verified backup/failover. - -## Historical file and interface inventory - -- Domain/application remote identity and epoch contracts. -- Daemon enrollment, encrypted/offline spool, replay, query coverage, replica, - backup/restore, promotion/failover, Doctor/API/dashboard surfaces. - -Interfaces: `BrainId`, `NodeId`, `ShardId`, `PlacementRevision`, `Epoch`, -`EnrollmentGrant`, `OfflineEnvelope`, `ReplayReceipt`, `ReplicaWatermark`, -`BackupManifest`, and `PromotionReceipt`. - -Writer key is exactly -`(BrainId, shard, generation, placement_revision, epoch)`. Overlays remain -node-local and never become shared mutable authority. - -## Historical ordered slices - -1. Remote contracts and monotone epoch ledger. -2. Enrollment, credential rotation, bounded encrypted offline spool. -3. Fenced duplicate-tolerant replay and cross-node query coverage. -4. Verified replica, backup, staged restore, and integrity receipts. -5. Promotion/failover plus API, Doctor, dashboard, and direct journey. - -## Product outcome contributed - -The work contributed enrolled, offline-capable remote operation with fenced -single-writer authority, duplicate-tolerant replay, query coverage, and -verified backup/failover behavior. Current direct behavior and acceptance live -in the applicable numbered V2 plan. - -## Historical migration, rollback, measurement, and deletion notes - -Enroll remote capability without changing local authority. Restore and verify -before promotion; rollback occurs before promotion and never through -multi-primary fallback. Measure offline append/replay, query latency/coverage, -backup/restore, promotion RTO/RPO, and event-to-ready. Delete ad hoc remote -paths only after fencing, duplicate tolerance, failover, backup/restore, -cross-node query, Doctor/dashboard, and normal CI evidence pass. diff --git a/docs/superpowers/plans/v2/pr18-public-sdks.md b/docs/superpowers/plans/v2/pr18-public-sdks.md deleted file mode 100644 index 3041ebe8c4..0000000000 --- a/docs/superpowers/plans/v2/pr18-public-sdks.md +++ /dev/null @@ -1,67 +0,0 @@ -# PR18 Public SDK Plan - -> **Archived provenance — not current requirements.** This document records -> historical planning and execution evidence. Current scope and acceptance come -> only from [`00-plan-set-index.md`](../../../plans/tracedecay-v2/00-plan-set-index.md), -> [`NEXT.md`](../../../plans/tracedecay-v2/NEXT.md), and the applicable numbered -> V2 plan. Do not recreate its task checklists, file inventories, -> branch/worktree/SHA or commit protocol, Gate A/B, timing/JUnit receipts, exact -> test names/counts, generated-byte/source-shape checks, PR closure gates, or -> platform gate lattice. -> Historical version/compatibility/migration language cannot resurrect -> source-only/internal branch scaffolding. Only an actually independently -> released public API/schema revision may retain protocol compatibility. -> Persisted cursors, idempotency keys, journals, checkpoints, and receipts -> accept only their exact final shape; every other database, store, spool, file, -> or projection returns typed `ResetRequired` and requires explicit reset or -> recreation. No storage reader, migration, backfill, dual write, or census -> path exists. - -**Goal:** Publish Rust and TypeScript SDKs for accepted PR12–PR17 -operations without inventing lifecycle semantics. (The originally planned -Python SDK was dropped: delivery is TypeScript-first plus a retained Rust -SDK for native consumers, with no Python package.) - -## Historical file and interface inventory - -- Rust workspace SDK crate and generated wire types. -- TypeScript package root, generators, conformance fixtures, - package metadata, examples, and release CI. -- Bind local daemon and PR16 remote transports to one operation catalog. - -Generated types cover wire schemas. Handwritten façades cover authentication, -`RequestContext`, paging/cursors, SSE reconnect, cancellation, resume, -idempotency, typed errors, operation receipts, `TaskHandoffToken`, and host -handoff tokens. Names freeze only after each operation's production journey is -accepted. - -## Historical ordered slices - -1. Freeze accepted operation/schema manifest. -2. Generate Rust/TS wire models deterministically. -3. Implement lifecycle façades and local transport. -4. Implement remote transport with identical semantics. -5. Add examples and cross-language golden conformance. -6. Package/install/publish dry runs and compatibility policy. - -## Product outcome contributed - -The work contributed Rust and TypeScript SDK façades over one operation -catalog with equivalent authentication, scope, lifecycle, paging/SSE, -cancellation, idempotency, and typed outcomes. Current direct behavior and -acceptance live in the applicable numbered V2 plan. - -## Historical release, reset, measurement, and deletion notes - -Before first publication, generated schemas change in place. V2 branch-local -data uses the fresh-store cutover: only the exact final persisted shape is -accepted, and every other shape returns typed `ResetRequired` for explicit -reset or recreation. No storage reader, migration, backfill, dual write, or -census path survives. After an actual independent package release, public -schemas follow the accepted major-version compatibility policy. Rollback -unpublishes or yanks a package release according to registry -policy but never changes server semantics. Measure generation, package size, -startup, paging/SSE overhead, and conformance duration. Delete private client -wrappers and aliases only after two-language (Rust and TypeScript) -local/remote conformance, -examples, package/install gates, semver review, and normal CI pass. diff --git a/docs/superpowers/plans/v2/pr19-cutover-runtime.md b/docs/superpowers/plans/v2/pr19-cutover-runtime.md deleted file mode 100644 index 26dfc18c31..0000000000 --- a/docs/superpowers/plans/v2/pr19-cutover-runtime.md +++ /dev/null @@ -1,49 +0,0 @@ -# PR19 Runtime Fresh-Store Reset - -> Historical planning evidence only. Current scope and acceptance come from -> [`00-plan-set-index.md`](../../../plans/tracedecay-v2/00-plan-set-index.md), -> [`NEXT.md`](../../../plans/tracedecay-v2/NEXT.md), and the applicable numbered -> V2 plan. Do not recreate branch/worktree/SHA protocols, gate lattices, -> generated-byte/source-shape checks, or transition-era inventories. - -**Goal:** Admit one exact final V2 persisted shape, return `ResetRequired` for -every other shape, and retain only independently released public API protocol -compatibility. - -Every TraceDecay database, store, spool, file, journal, checkpoint, receipt, -and projection accepts only its exact final shape. A non-final shape is refused -before interpretation and requires an explicit reset or recreation. There is -no stored-data reader, conversion, backfill, dual write, shadow read, census, -or recovery path, including for data written by an older installed binary. - -## Scope - -- Validate final-shape admission at each persisted-state open boundary. -- Preserve one fenced writer and canonical daemon route for a valid final - store. -- Provide an explicit reset/recreation action scoped to a refused target. -- Delete storage-transition code and source-only aliases after internal callers - move. -- Retain a public protocol façade only with evidence of an actual independent - package or API release; it delegates to the canonical operation and owns no - storage or lifecycle behavior. - -## Direct acceptance - -- Exact-final fixtures admit through the canonical daemon route. -- Every older, partial, unknown, unversioned, or foreign persisted fixture - returns `ResetRequired` before read, write, replay, or projection. -- Explicit reset/recreation creates a clean final store without consuming old - bytes. -- Tests prove no stored-data reader, converter, backfill, dual write, shadow - read, census, or recovery route remains. -- Retained public protocol compatibility is independently release-evidenced - and preserves canonical authorization, errors, redaction, effects, - pagination, streaming, cancellation, and retry behavior. - -## Not in PR19 - -- Persisted-data conversion, rollback, retention, or recovery workflows. -- Memory special handling. -- Transition dashboards, execution ledgers, schema-only conformance suites, or - placeholder acceptance baselines. diff --git a/docs/superpowers/plans/v2/pr20-performance.md b/docs/superpowers/plans/v2/pr20-performance.md deleted file mode 100644 index b79edfebb7..0000000000 --- a/docs/superpowers/plans/v2/pr20-performance.md +++ /dev/null @@ -1,44 +0,0 @@ -# PR20 Measured Performance and Cleanup Plan - -> **Archived provenance — not current requirements.** This document records -> historical planning and execution evidence. Current scope and acceptance come -> only from [`00-plan-set-index.md`](../../../plans/tracedecay-v2/00-plan-set-index.md), -> [`NEXT.md`](../../../plans/tracedecay-v2/NEXT.md), and the applicable numbered -> V2 plan. Do not recreate its task checklists, file inventories, -> branch/worktree/SHA or commit protocol, Gate A/B, timing/JUnit receipts, exact -> test names/counts, generated-byte/source-shape checks, PR closure gates, or -> platform gate lattice. - -**Outcome contributed:** Measure and improve production event-to-ready time and -other user-visible bottlenecks while preserving equivalent product behavior, -truthful unavailable metrics, and bounded storage. - -## Retired measurement framework - -`EditClassReceipt`, boundary receipts, Gate A/B dispositions, JUnit/timing -receipt schemas, exact test-count or slow-test gates, generated-byte/source-shape -checks, and the default/all/no-default/lite/package/platform gate lattice are -retired. Historical receipts may preserve those fields as provenance, but they -are not prerequisites and must not be recreated. - -## Historical work areas - -- Measure representative production capture/edit/event-to-ready journeys under - comparable conditions. -- Optimize measured runtime/query/index bottlenecks without semantic drift. -- Keep retention/storage behavior bounded and surface missing measurements - truthfully. -- Remove superseded paths when the applicable numbered V2 plan permits it. - -## Product outcome contributed - -The enduring outcome is measured event-to-ready improvement with equivalent -behavior. Current representative journeys, equivalence criteria, storage -bounds, and acceptance are defined by the applicable numbered V2 plans. - -## Historical measurement notes - -Historical experiments isolated optimizations, compared like-for-like samples, -and rejected semantic drift or false claims from unavailable data. Their -thresholds, receipt types, boundary dispositions, platform matrix, and deletion -choreography do not define current closure.